Bring in mcp-skills-plugins branch: marketplace, plugins, skills, OpenCredits, and image previews

Major release adding:
- MCP marketplace with curated registry and search across multiple registries
- Plugins and skills marketplace integration
- OpenCredits payment integration with model selection and checkout flow
- Image preview before sending (paste, file picker)
- Self-hosted Umami analytics with custom events
- Support feedback modal with Discord webhook
- Local OpenAI to Anthropic router for model routing
- Inline stop button replacing send during processing
- Improved install flow requiring Node.js 18+
- WSL env var passthrough fixes
- Better error handling and Windows compatibility

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
andrepimenta
2026-04-11 23:54:13 +01:00
parent b527b6f4c9
commit deca7de8d5
25 changed files with 7994 additions and 641 deletions

5
.claude/settings.json Normal file
View File

@@ -0,0 +1,5 @@
{
"enabledPlugins": {
"frontend-design@claude-plugins-official": true
}
}

View File

@@ -5,9 +5,13 @@
"Bash(grep:*)",
"Bash(sed:*)",
"Bash(rg:*)",
"Bash(npx tsc:*)"
"Bash(npx tsc:*)",
"mcp__ide__getDiagnostics",
"Bash(curl -s 'https://skills.sh/' -H 'user-agent: Mozilla/5.0')",
"Bash(curl -s 'https://skills.sh/' -H 'user-agent: Mozilla/5.0' -o /tmp/skills_page.html)",
"Bash(python3 /tmp/skills_parse.py)"
],
"deny": []
},
"enableAllProjectMcpServers": false
}
}

3
.gitignore vendored
View File

@@ -3,4 +3,5 @@ dist
node_modules
.vscode-test/
*.vsix
backup
backup
backup-files

View File

@@ -14,4 +14,5 @@ backup
claude-code-chat-permissions-mcp/**
node_modules
mcp-permissions.js
build
backup-files
build

21
backup.sh Executable file
View File

@@ -0,0 +1,21 @@
#!/bin/bash
# Backup script for src folder
# Get the directory where the script is located
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
# Create backup directory if it doesn't exist
BACKUP_DIR="$SCRIPT_DIR/backup-files"
mkdir -p "$BACKUP_DIR"
# Generate timestamp
TIMESTAMP=$(date +"%Y-%m-%d_%H-%M-%S")
# Create backup filename
BACKUP_NAME="src-backup-$TIMESTAMP"
# Copy src folder to backup
cp -r "$SCRIPT_DIR/src" "$BACKUP_DIR/$BACKUP_NAME"
echo "Backup created: $BACKUP_DIR/$BACKUP_NAME"

View File

@@ -2,7 +2,7 @@
"name": "claude-code-chat",
"displayName": "Chat for Claude Code",
"description": "Beautiful Claude Code Chat Interface for VS Code",
"version": "1.1.0",
"version": "2.0.0",
"publisher": "AndrePimenta",
"author": "Andre Pimenta",
"repository": {
@@ -185,6 +185,26 @@
"type": "boolean",
"default": false,
"description": "Enable Yolo Mode to skip all permission checks. Use with caution as Claude can execute any command without asking."
},
"claudeCodeChat.executable.path": {
"type": "string",
"default": "",
"description": "Custom path to the Claude Code executable. Leave empty to use the default 'claude' command."
},
"claudeCodeChat.environment.variables": {
"type": "object",
"default": {},
"description": "Custom environment variables to pass to Claude Code. Example: {\"ANTHROPIC_API_KEY\": \"sk-...\"}"
},
"claudeCodeChat.environment.disabled": {
"type": "boolean",
"default": false,
"description": "When enabled, custom environment variables are not passed to Claude Code."
},
"claudeCodeChat.router.enabled": {
"type": "boolean",
"default": false,
"description": "Enable the local router to convert OpenAI format to Anthropic format. Required for providers that use OpenAI-compatible APIs."
}
}
}

File diff suppressed because it is too large Load Diff

148
src/model-updater.ts Normal file
View File

@@ -0,0 +1,148 @@
interface ApiModel {
id: string;
name?: string;
description?: string;
pricing?: { prompt: number; completion: number; currency?: string; unit?: string };
context_length?: number;
max_output_tokens?: number;
[key: string]: any;
}
interface BundledModel {
id: string;
name: string;
description?: string;
provider: string;
quickLabel?: string;
context_length?: number;
max_output_tokens?: number;
tierModels?: { sonnet: string; opus: string; haiku: string };
[key: string]: any;
}
interface ProviderResolver {
main: RegExp;
opus?: RegExp;
haiku?: RegExp;
}
function parseVersion(ver: string): number[] {
return ver.split('.').map(Number);
}
function compareVersions(a: string, b: string): number {
const va = parseVersion(a);
const vb = parseVersion(b);
for (let i = 0; i < Math.max(va.length, vb.length); i++) {
const na = va[i] || 0;
const nb = vb[i] || 0;
if (na !== nb) { return na - nb; }
}
return 0;
}
function findHighestMatch(apiModels: ApiModel[], regex: RegExp): ApiModel | null {
let best: ApiModel | null = null;
let bestVer: string | null = null;
for (const m of apiModels) {
const match = regex.exec(m.id);
if (match) {
const ver = match[1] || '0';
if (!bestVer || compareVersions(ver, bestVer) > 0) {
bestVer = ver;
best = m;
}
}
}
return best;
}
const providerResolvers: Record<string, ProviderResolver> = {
'zai/glm-': {
main: /^zai\/glm-(\d+(?:\.\d+)?)$/,
haiku: /^zai\/GLM-([\d.]+)-(?:Air|Flash)$/i
},
'openai/gpt-': {
main: /^openai\/gpt-([\d.]+)-codex$/,
haiku: /^openai\/gpt-([\d.]+)-codex-mini$/
},
'gemini-': {
main: /^(?:google\/)?gemini-([\d.]+)-pro-preview$/,
opus: /^(?:google\/)?gemini-([\d.]+)-pro-preview-thinking$/,
haiku: /^(?:google\/)?gemini-([\d.]+)-flash(?:-preview)?$/
},
'deepseek/deepseek-': {
main: /^deepseek\/deepseek-v([\d.]+)[-:]thinking$/
},
'minimax/minimax-': {
main: /^minimax\/minimax-m([\d.]+)$/
},
'moonshotai/kimi-': {
main: /^moonshotai\/kimi-k([\d.]+)$/,
haiku: /^moonshotai\/kimi-k([\d.]+)-turbo$/
}
};
export function resolveLatestModels(apiModels: ApiModel[], bundledModels: BundledModel[]): BundledModel[] {
return bundledModels.map(bundled => {
const b: BundledModel = JSON.parse(JSON.stringify(bundled));
let resolver: ProviderResolver | null = null;
for (const prefix of Object.keys(providerResolvers)) {
if (b.id.toLowerCase().startsWith(prefix)) {
resolver = providerResolvers[prefix];
break;
}
}
if (!resolver) { return b; }
// Resolve main (sonnet-tier) model
const mainMatch = findHighestMatch(apiModels, resolver.main);
if (mainMatch) {
b.id = mainMatch.id;
b.name = mainMatch.name || b.name;
b.description = mainMatch.description || b.description;
b.context_length = mainMatch.context_length || b.context_length;
b.max_output_tokens = mainMatch.max_output_tokens || b.max_output_tokens;
if (b.tierModels) {
b.tierModels.sonnet = mainMatch.id;
if (!resolver.opus) {
b.tierModels.opus = mainMatch.id;
}
}
}
// Resolve opus-tier model (e.g. Gemini thinking variant)
if (resolver.opus && b.tierModels) {
const opusMatch = findHighestMatch(apiModels, resolver.opus);
if (opusMatch) {
b.tierModels.opus = opusMatch.id;
}
}
// Resolve haiku-tier model
if (resolver.haiku && b.tierModels) {
const haikuMatch = findHighestMatch(apiModels, resolver.haiku);
if (haikuMatch) {
b.tierModels.haiku = haikuMatch.id;
}
}
return b;
});
}
export async function fetchAndResolveModels(bundledModels: BundledModel[], apiBaseUrl: string = 'https://ccc.api.opencredits.ai'): Promise<BundledModel[] | null> {
try {
const response = await fetch(apiBaseUrl + '/v1/models');
const data: any = await response.json();
const apiModels: ApiModel[] = data.data || data;
if (!Array.isArray(apiModels) || apiModels.length === 0) {
return null;
}
return resolveLatestModels(apiModels, bundledModels);
} catch (e) {
console.log('Auto-update models failed:', e);
return null;
}
}

150
src/plugins-script.ts Normal file
View File

@@ -0,0 +1,150 @@
const getPluginsScript = () => `
// ─── Plugins ───
var topPlugins = (window.__topPlugins || []);
var pluginsDisplayedList = null;
function formatPluginName(name) {
return name.replace(/-/g, ' ').replace(/\\b\\w/g, function(c) { return c.toUpperCase(); });
}
function showPluginsModal() {
document.getElementById('pluginsModal').style.display = 'flex';
loadInstalledPlugins();
renderAvailablePlugins(topPlugins);
}
function hidePluginsModal() {
document.getElementById('pluginsModal').style.display = 'none';
}
function loadInstalledPlugins() {
vscode.postMessage({ type: 'loadPlugins' });
}
function displayPlugins(data) {
var pluginsList = document.getElementById('pluginsList');
pluginsList.innerHTML = '';
var enabled = data.enabled || {};
var keys = Object.keys(enabled);
if (keys.length === 0) {
pluginsList.innerHTML = '<div class="no-servers">' +
'<div class="no-servers-icon"><svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M21 16V8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73l7 4a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16z"/><polyline points="3.27 6.96 12 12.01 20.73 6.96"/><line x1="12" y1="22.08" x2="12" y2="12"/></svg></div>' +
'<div class="no-servers-text">No plugins enabled</div>' +
'</div>';
return;
}
keys.forEach(function(installId) {
var isEnabled = enabled[installId];
var name = installId.replace(/@.*$/, '');
var displayName = formatPluginName(name);
var plugin = topPlugins.find(function(p) { return p.installId === installId; });
var desc = plugin ? plugin.description : '';
var verified = plugin ? plugin.verified : false;
var item = document.createElement('div');
item.className = 'mcp-server-item';
var verifiedHtml = verified ? '<span class="marketplace-item-verified" title="Anthropic verified">&#10003;</span>' : '';
var statusHtml = isEnabled ? '<span class="server-type" style="background:rgba(0,122,204,0.2);color:var(--vscode-charts-blue);">enabled</span>' : '<span class="server-type">disabled</span>';
item.innerHTML = '<div class="server-info" style="min-width:0;overflow:hidden;">' +
'<div class="server-name">' + escapeHtml(displayName) + verifiedHtml + ' ' + statusHtml + '</div>' +
(desc ? '<div class="server-config" style="overflow:hidden;text-overflow:ellipsis;white-space:nowrap;">' + escapeHtml(desc) + '</div>' : '') +
'</div>' +
'<div class="server-actions" style="flex-shrink:0;">' +
'<button class="btn outlined server-delete-btn" data-plugin="' + escapeHtml(installId) + '" onclick="removePlugin(this.dataset.plugin)">Remove</button>' +
'</div>';
pluginsList.appendChild(item);
});
}
function renderAvailablePlugins(plugins) {
var grid = document.getElementById('pluginsGrid');
if (!grid) return;
if (!plugins || plugins.length === 0) {
grid.innerHTML = '<div class="marketplace-loading">No plugins found.</div>';
return;
}
var html = '';
plugins.forEach(function(plugin) {
var name = plugin.name || 'Unknown';
var displayName = formatPluginName(name);
var desc = escapeHtml(plugin.description || 'No description');
var verified = plugin.verified;
var safeId = escapeHtml(plugin.installId || name).replace(/'/g, '&#39;');
html += '<div class="marketplace-item" data-plugin-id="' + safeId + '" onclick="showPluginDetail(this.dataset.pluginId)">' +
'<div class="marketplace-item-header">' +
'<div class="marketplace-item-icon-placeholder">' + escapeHtml(displayName.charAt(0).toUpperCase()) + '</div>' +
'<div class="marketplace-item-info">' +
'<div class="marketplace-item-name">' + escapeHtml(displayName) + '</div>' +
'</div>' +
'</div>' +
'<div class="marketplace-item-desc">' + desc + '</div>' +
'</div>';
});
grid.innerHTML = html;
}
function searchPlugins(query) {
if (!query) {
renderAvailablePlugins(topPlugins);
return;
}
var q = query.toLowerCase();
var filtered = topPlugins.filter(function(p) {
return (p.name && p.name.toLowerCase().indexOf(q) >= 0) ||
(p.description && p.description.toLowerCase().indexOf(q) >= 0);
});
renderAvailablePlugins(filtered);
}
function showPluginDetail(installId) {
var plugin = topPlugins.find(function(p) { return p.installId === installId; });
if (!plugin) return;
var name = plugin.name || 'Unknown';
var displayName = formatPluginName(name);
var desc = plugin.description || 'No description available.';
var verified = plugin.verified;
var verifiedHtml = verified ? '<span class="marketplace-item-verified" title="Anthropic verified">&#10003; Anthropic verified</span>' : '';
var grid = document.getElementById('pluginsGrid');
pluginsDisplayedList = grid.innerHTML;
grid.innerHTML = '<div class="marketplace-detail">' +
'<button class="marketplace-back-btn" onclick="backToPluginsList()">&#8592; Back</button>' +
'<div class="marketplace-detail-header">' +
'<div class="marketplace-item-icon-placeholder" style="width:40px;height:40px;font-size:18px;">' + escapeHtml(displayName.charAt(0).toUpperCase()) + '</div>' +
'<div class="marketplace-detail-header-info">' +
'<div class="marketplace-detail-name">' + escapeHtml(displayName) + '</div>' +
'<div class="marketplace-detail-header-meta">' + verifiedHtml + '</div>' +
'</div>' +
'<button class="btn marketplace-install-btn" data-plugin="' + escapeHtml(installId) + '" onclick="installPlugin(this.dataset.plugin)">Enable</button>' +
'</div>' +
'<div class="marketplace-detail-desc">' + escapeHtml(desc) + '</div>' +
'<div class="marketplace-detail-row"><a href="https://github.com/anthropics/claude-plugins-official/tree/main/' + (plugin.type === 'official' ? 'plugins' : 'external_plugins') + '/' + escapeHtml(name) + '" target="_blank" class="marketplace-detail-link">View on GitHub</a></div>' +
'<div style="font-size:11px;color:var(--vscode-descriptionForeground);margin-top:4px;">Adds <code style="font-size:10px;">' + escapeHtml(installId) + '</code> to .claude/settings.json</div>' +
'</div>';
}
function backToPluginsList() {
var grid = document.getElementById('pluginsGrid');
if (pluginsDisplayedList) {
grid.innerHTML = pluginsDisplayedList;
} else {
renderAvailablePlugins(topPlugins);
}
}
function installPlugin(installId) {
vscode.postMessage({ type: 'installPlugin', installId: installId });
hidePluginsModal();
}
function removePlugin(installId) {
vscode.postMessage({ type: 'removePlugin', installId: installId });
}
`;
export default getPluginsScript;

26
src/plugins-ui.ts Normal file
View File

@@ -0,0 +1,26 @@
const getPluginsHtml = () => `
<!-- Plugins modal -->
<div id="pluginsModal" class="tools-modal" style="display: none;">
<div class="tools-modal-content">
<div class="tools-modal-header">
<span>Plugins</span>
<button class="tools-close-btn" onclick="hidePluginsModal()">✕</button>
</div>
<div class="tools-list">
<div class="mcp-servers-list" id="pluginsList">
<!-- Installed plugins will be loaded here -->
</div>
<div class="mcp-popular-servers" id="pluginsMarketplace">
<h4>Available Plugins</h4>
<div class="marketplace-search">
<input type="text" id="pluginsSearch" placeholder="Search plugins..." oninput="searchPlugins(this.value)" />
</div>
<div class="marketplace-grid" id="pluginsGrid">
</div>
</div>
</div>
</div>
</div>
`;
export default getPluginsHtml;

View File

@@ -0,0 +1,66 @@
[
{
"id": "openai/gpt-5.3-codex",
"name": "GPT 5.3 Codex",
"description": "Coding-focused GPT-5.3 variant with optimized routing.",
"context_length": 400000,
"max_output_tokens": 128000,
"credits_per_request": 4.921875,
"provider": "OpenAI",
"quickLabel": "GPT",
"tierModels": { "sonnet": "openai/gpt-5.3-codex", "opus": "openai/gpt-5.3-codex", "haiku": "openai/gpt-5.1-codex-mini" }
},
{
"id": "google/gemini-3.1-pro-preview",
"name": "Gemini 3.1 Pro Preview",
"description": "Google's Gemini 3.1 Pro with enhanced reasoning and multimodal support.",
"context_length": 1000000,
"max_output_tokens": 64000,
"credits_per_request": 4.375,
"provider": "Google",
"quickLabel": "Gemini",
"tierModels": { "sonnet": "google/gemini-3.1-pro-preview", "opus": "google/gemini-3.1-pro-preview", "haiku": "google/gemini-3-flash" }
},
{
"id": "minimax/minimax-m2.7",
"name": "Minimax M2.7",
"description": "MiniMax M2.7 with enhanced context understanding and improved complex tool use. Optimized for agentic workflows and long-horizon tasks.",
"context_length": 204800,
"max_output_tokens": 131000,
"credits_per_request": 0.46875,
"provider": "MiniMax",
"quickLabel": "MiniMax"
},
{
"id": "moonshotai/kimi-k2.5",
"name": "Kimi K2.5",
"description": "Kimi K2.5 is Moonshot AI's native multimodal model with strong general reasoning, visual coding, and agentic tool-calling.",
"context_length": 262114,
"max_output_tokens": 262114,
"credits_per_request": 1.125,
"provider": "Moonshot AI",
"quickLabel": "Kimi",
"tierModels": { "sonnet": "moonshotai/kimi-k2.5", "opus": "moonshotai/kimi-k2.5", "haiku": "moonshotai/kimi-k2-turbo" }
},
{
"id": "zai/glm-5",
"name": "GLM 5",
"description": "GLM-5 is the latest GLM series text model with stronger reasoning, long-context chat, and reliable tool use.",
"context_length": 202800,
"max_output_tokens": 131100,
"credits_per_request": 1.3125,
"provider": "Zhipu AI",
"quickLabel": "GLM",
"tierModels": { "sonnet": "zai/glm-5", "opus": "zai/glm-5", "haiku": "zai/glm-4.7-flash" }
},
{
"id": "deepseek/deepseek-v3.2-thinking",
"name": "DeepSeek V3.2 Thinking",
"description": "DeepSeek V3.2 thinking/reasoner mode. Reasoning-first model built for agents. First DeepSeek model with thinking-in-tool-use capability.",
"context_length": 128000,
"max_output_tokens": 64000,
"credits_per_request": 0.21875,
"provider": "DeepSeek",
"quickLabel": "DeepSeek"
}
]

265
src/router/formatRequest.ts Normal file
View File

@@ -0,0 +1,265 @@
interface MessageCreateParamsBase {
model: string;
messages: any[];
system?: any;
temperature?: number;
tools?: any[];
stream?: boolean;
}
/**
* Validates OpenAI format messages to ensure complete tool_calls/tool message pairing.
* Requires tool messages to immediately follow assistant messages with tool_calls.
* Enforces strict immediate following sequence between tool_calls and tool messages.
*/
function validateOpenAIToolCalls(messages: any[]): any[] {
const validatedMessages: any[] = [];
for (let i = 0; i < messages.length; i++) {
const currentMessage = { ...messages[i] };
// Process assistant messages with tool_calls
if (currentMessage.role === "assistant" && currentMessage.tool_calls) {
const validToolCalls: any[] = [];
const removedToolCallIds: string[] = [];
// Collect all immediately following tool messages
const immediateToolMessages: any[] = [];
let j = i + 1;
while (j < messages.length && messages[j].role === "tool") {
immediateToolMessages.push(messages[j]);
j++;
}
// For each tool_call, check if there's an immediately following tool message
currentMessage.tool_calls.forEach((toolCall: any) => {
const hasImmediateToolMessage = immediateToolMessages.some(toolMsg =>
toolMsg.tool_call_id === toolCall.id
);
if (hasImmediateToolMessage) {
validToolCalls.push(toolCall);
} else {
removedToolCallIds.push(toolCall.id);
}
});
// Update the assistant message
if (validToolCalls.length > 0) {
currentMessage.tool_calls = validToolCalls;
} else {
delete currentMessage.tool_calls;
}
// Only include message if it has content or valid tool_calls
if (currentMessage.content || currentMessage.tool_calls) {
validatedMessages.push(currentMessage);
}
}
// Process tool messages
else if (currentMessage.role === "tool") {
let hasImmediateToolCall = false;
// Check if the immediately preceding assistant message has matching tool_call
if (i > 0) {
const prevMessage = messages[i - 1];
if (prevMessage.role === "assistant" && prevMessage.tool_calls) {
hasImmediateToolCall = prevMessage.tool_calls.some((toolCall: any) =>
toolCall.id === currentMessage.tool_call_id
);
} else if (prevMessage.role === "tool") {
// Check for assistant message before the sequence of tool messages
for (let k = i - 1; k >= 0; k--) {
if (messages[k].role === "tool") continue;
if (messages[k].role === "assistant" && messages[k].tool_calls) {
hasImmediateToolCall = messages[k].tool_calls.some((toolCall: any) =>
toolCall.id === currentMessage.tool_call_id
);
}
break;
}
}
}
if (hasImmediateToolCall) {
validatedMessages.push(currentMessage);
}
}
// For all other message types, include as-is
else {
validatedMessages.push(currentMessage);
}
}
return validatedMessages;
}
// Model configuration - set from extension
interface ModelConfig {
haikuModel: string;
sonnetModel: string;
opusModel: string;
}
let modelConfig: ModelConfig | null = null;
export function setModelConfig(config: ModelConfig): void {
modelConfig = config;
console.log('[Router] Model config updated:', config);
}
export function mapModel(anthropicModel: string): string {
console.log('[Router] Mapping model:', anthropicModel);
// If model already contains '/', it's already a provider model ID - return as-is
if (anthropicModel.includes('/')) {
console.log(`[Router] Model already has provider prefix, passing through: ${anthropicModel}`);
return anthropicModel;
}
if (!modelConfig) {
console.log('[Router] No model config set, returning as-is');
return anthropicModel;
}
if (anthropicModel.includes('haiku') && modelConfig.haikuModel) {
console.log(`[Router] Mapping haiku -> ${modelConfig.haikuModel}`);
return modelConfig.haikuModel;
} else if (anthropicModel.includes('sonnet') && modelConfig.sonnetModel) {
console.log(`[Router] Mapping sonnet -> ${modelConfig.sonnetModel}`);
return modelConfig.sonnetModel;
} else if (anthropicModel.includes('opus') && modelConfig.opusModel) {
console.log(`[Router] Mapping opus -> ${modelConfig.opusModel}`);
return modelConfig.opusModel;
}
console.log(`[Router] No mapping found for model: ${anthropicModel}, passing through`);
return anthropicModel;
}
export function formatAnthropicToOpenAI(body: MessageCreateParamsBase): any {
const { model, messages, system = [], temperature, tools, stream } = body;
const openAIMessages = Array.isArray(messages)
? messages.flatMap((anthropicMessage) => {
const openAiMessagesFromThisAnthropicMessage: any[] = [];
if (!Array.isArray(anthropicMessage.content)) {
if (typeof anthropicMessage.content === "string") {
openAiMessagesFromThisAnthropicMessage.push({
role: anthropicMessage.role,
content: anthropicMessage.content,
});
}
return openAiMessagesFromThisAnthropicMessage;
}
if (anthropicMessage.role === "assistant") {
const assistantMessage: any = {
role: "assistant",
content: null,
};
let textContent = "";
const toolCalls: any[] = [];
anthropicMessage.content.forEach((contentPart: any) => {
if (contentPart.type === "text") {
textContent += (typeof contentPart.text === "string"
? contentPart.text
: JSON.stringify(contentPart.text)) + "\n";
} else if (contentPart.type === "tool_use") {
toolCalls.push({
id: contentPart.id,
type: "function",
function: {
name: contentPart.name,
arguments: JSON.stringify(contentPart.input),
},
});
}
});
const trimmedTextContent = textContent.trim();
if (trimmedTextContent.length > 0) {
assistantMessage.content = trimmedTextContent;
}
if (toolCalls.length > 0) {
assistantMessage.tool_calls = toolCalls;
}
if (assistantMessage.content || (assistantMessage.tool_calls && assistantMessage.tool_calls.length > 0)) {
openAiMessagesFromThisAnthropicMessage.push(assistantMessage);
}
} else if (anthropicMessage.role === "user") {
let userTextMessageContent = "";
const subsequentToolMessages: any[] = [];
anthropicMessage.content.forEach((contentPart: any) => {
if (contentPart.type === "text") {
userTextMessageContent += (typeof contentPart.text === "string"
? contentPart.text
: JSON.stringify(contentPart.text)) + "\n";
} else if (contentPart.type === "tool_result") {
subsequentToolMessages.push({
role: "tool",
tool_call_id: contentPart.tool_use_id,
content: typeof contentPart.content === "string"
? contentPart.content
: JSON.stringify(contentPart.content),
});
}
});
const trimmedUserText = userTextMessageContent.trim();
if (trimmedUserText.length > 0) {
openAiMessagesFromThisAnthropicMessage.push({
role: "user",
content: trimmedUserText,
});
}
openAiMessagesFromThisAnthropicMessage.push(...subsequentToolMessages);
}
return openAiMessagesFromThisAnthropicMessage;
})
: [];
const systemMessages = Array.isArray(system)
? system.map((item) => ({
role: "system",
content: typeof item === "string" ? item : item.text
}))
: typeof system === "string" && system.length > 0
? [{ role: "system", content: system }]
: [];
const data: any = {
model: mapModel(model),
messages: [...systemMessages, ...openAIMessages],
temperature,
stream,
};
// Request usage stats in streaming responses
if (stream) {
data.stream_options = { include_usage: true };
}
if (tools) {
data.tools = tools.map((item: any) => ({
type: "function",
function: {
name: item.name,
description: item.description,
parameters: item.input_schema,
},
}));
}
// Validate OpenAI messages to ensure complete tool_calls/tool message pairing
data.messages = [...systemMessages, ...validateOpenAIToolCalls(openAIMessages)];
return data;
}

View File

@@ -0,0 +1,37 @@
export function formatOpenAIToAnthropic(completion: any, model: string): any {
const messageId = "msg_" + Date.now();
const message = completion.choices[0].message;
const content: any[] = [];
if (message.content) {
content.push({ text: message.content, type: "text" });
}
if (message.tool_calls) {
for (const item of message.tool_calls) {
content.push({
type: 'tool_use',
id: item.id,
name: item.function?.name,
input: item.function?.arguments ? JSON.parse(item.function.arguments) : {},
});
}
}
const hasToolUse = message.tool_calls && message.tool_calls.length > 0;
const usage = completion.usage || {};
const result = {
id: messageId,
type: "message",
role: "assistant",
content: content,
stop_reason: hasToolUse ? "tool_use" : "end_turn",
stop_sequence: null,
model,
usage: {
input_tokens: usage.prompt_tokens || 0,
output_tokens: usage.completion_tokens || 0,
},
};
return result;
}

2
src/router/index.ts Normal file
View File

@@ -0,0 +1,2 @@
export { startRouter, stopRouter, isRouterRunning, getRouterPort, setBaseUrl } from './server';
export { setModelConfig } from './formatRequest';

220
src/router/server.ts Normal file
View File

@@ -0,0 +1,220 @@
import * as http from 'http';
import { formatAnthropicToOpenAI } from './formatRequest';
import { streamOpenAIToAnthropic } from './streamResponse';
import { formatOpenAIToAnthropic } from './formatResponse';
const DEFAULT_PORT = 31548;
const DEFAULT_BASE_URL = "http://localhost:8787/v1";
let server: http.Server | null = null;
let currentPort: number = DEFAULT_PORT;
let baseUrl: string = DEFAULT_BASE_URL;
export function setBaseUrl(url: string): void {
baseUrl = url || DEFAULT_BASE_URL;
console.log('[Router] Base URL set to:', baseUrl);
}
// Helper to parse JSON body
async function parseBody(req: http.IncomingMessage): Promise<any> {
return new Promise((resolve, reject) => {
let body = '';
req.on('data', chunk => {
body += chunk.toString();
// Prevent payload too large (50MB limit)
if (body.length > 50 * 1024 * 1024) {
req.destroy();
reject(new Error('Payload too large'));
}
});
req.on('end', () => {
try {
resolve(body ? JSON.parse(body) : {});
} catch (e) {
reject(new Error('Invalid JSON'));
}
});
req.on('error', reject);
});
}
function createServer(): http.Server {
return http.createServer(async (req, res) => {
const url = new URL(req.url || '/', `http://${req.headers.host}`);
const method = req.method || 'GET';
try {
// POST /v1/messages
if (url.pathname === '/v1/messages' && method === 'POST') {
console.log('[Router] 📥 Received request to /v1/messages');
const anthropicRequest = await parseBody(req);
const openaiRequest = formatAnthropicToOpenAI(anthropicRequest);
console.log('[Router] 🔄 Converted to OpenAI format:', {
model: openaiRequest.model,
stream: openaiRequest.stream,
messageCount: openaiRequest.messages?.length
});
const bearerToken = (req.headers['x-api-key'] as string) ||
(req.headers.authorization as string)?.replace("Bearer ", "").replace("bearer ", "");
if (!bearerToken || bearerToken.trim() === '') {
console.log('[Router] ❌ No bearer token found');
res.writeHead(401, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({
type: 'error',
error: {
type: 'authentication_error',
message: 'No API key provided. Please configure your OpenCredits user key in environment variables.'
}
}));
return;
}
const fetchHeaders = {
"Content-Type": "application/json",
"Authorization": `Bearer ${bearerToken}`,
"HTTP-Referer": "https://claude-code-chat.local",
"X-Title": "Claude-Code-Chat-Router"
};
const openaiResponse = await fetch(`${baseUrl}/chat/completions`, {
method: "POST",
headers: fetchHeaders,
body: JSON.stringify(openaiRequest),
});
console.log('[Router] 📥 Response status:', openaiResponse.status);
if (!openaiResponse.ok) {
const errorText = await openaiResponse.text();
console.log('[Router] ❌ Error:', errorText);
// Try to parse as JSON, otherwise use raw text
let errorMessage = errorText;
try {
const parsed = JSON.parse(errorText);
errorMessage = parsed.error?.message || parsed.message || errorText;
} catch {}
res.writeHead(openaiResponse.status, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({
type: 'error',
error: {
type: openaiResponse.status === 401 ? 'authentication_error' : 'api_error',
message: `[Router] ${errorMessage}`
}
}));
return;
}
if (openaiRequest.stream) {
console.log('[Router] 🌊 Starting stream response');
const anthropicStream = streamOpenAIToAnthropic(
openaiResponse.body as ReadableStream,
openaiRequest.model
);
res.writeHead(200, {
'Content-Type': 'text/event-stream',
'Cache-Control': 'no-cache',
'Connection': 'keep-alive',
});
const reader = anthropicStream.getReader();
const pump = async () => {
try {
while (true) {
const { done, value } = await reader.read();
if (done) {
res.end();
break;
}
res.write(value);
}
} catch (error) {
console.error('[Router] Stream error:', error);
res.end();
}
};
pump();
} else {
const openaiData = await openaiResponse.json();
const anthropicResponse = formatOpenAIToAnthropic(openaiData, openaiRequest.model);
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify(anthropicResponse));
}
return;
}
// 404 Not Found
res.writeHead(404, { 'Content-Type': 'text/plain' });
res.end('Not Found');
} catch (error) {
console.error('[Router] Error processing request:', error);
res.writeHead(500, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({
type: 'error',
error: {
type: 'api_error',
message: `[Router] Internal error: ${(error as Error).message}`
}
}));
}
});
}
export function startRouter(port: number = DEFAULT_PORT): Promise<number> {
return new Promise((resolve, reject) => {
if (server) {
console.log('[Router] Already running on port', currentPort);
resolve(currentPort);
return;
}
server = createServer();
server.on('error', (err: NodeJS.ErrnoException) => {
if (err.code === 'EADDRINUSE') {
console.log(`[Router] Port ${port} in use, trying ${port + 1}`);
server = null;
startRouter(port + 1).then(resolve).catch(reject);
} else {
reject(err);
}
});
server.listen(port, () => {
currentPort = port;
console.log(`[Router] 🚀 Running on http://localhost:${port}`);
resolve(port);
});
});
}
export function stopRouter(): Promise<void> {
return new Promise((resolve) => {
if (!server) {
resolve();
return;
}
server.close(() => {
console.log('[Router] Stopped');
server = null;
resolve();
});
});
}
export function isRouterRunning(): boolean {
return server !== null;
}
export function getRouterPort(): number {
return currentPort;
}

View File

@@ -0,0 +1,219 @@
export function streamOpenAIToAnthropic(openaiStream: ReadableStream, model: string): ReadableStream {
const messageId = "msg_" + Date.now();
const enqueueSSE = (controller: ReadableStreamDefaultController, eventType: string, data: any) => {
const sseMessage = `event: ${eventType}\ndata: ${JSON.stringify(data)}\n\n`;
controller.enqueue(new TextEncoder().encode(sseMessage));
};
return new ReadableStream({
async start(controller) {
// Send message_start event
const messageStart = {
type: "message_start",
message: {
id: messageId,
type: "message",
role: "assistant",
content: [],
model,
stop_reason: null,
stop_sequence: null,
usage: { input_tokens: 0, output_tokens: 0 },
},
};
enqueueSSE(controller, "message_start", messageStart);
let contentBlockIndex = 0;
let hasAnyBlock = false;
let hasStartedTextBlock = false;
let isToolUse = false;
let currentToolCallId: string | null = null;
let toolCallJsonMap = new Map<string, string>();
let streamUsage: { input_tokens: number; output_tokens: number } | null = null;
const reader = openaiStream.getReader();
const decoder = new TextDecoder();
let buffer = '';
try {
while (true) {
const { done, value } = await reader.read();
if (done) {
// Process any remaining data in buffer
if (buffer.trim()) {
const lines = buffer.split('\n');
for (const line of lines) {
if (line.trim() && line.startsWith('data: ')) {
const data = line.slice(6).trim();
if (data === '[DONE]') continue;
try {
const parsed = JSON.parse(data);
processStreamChunk(parsed);
} catch (e) {
// Parse error
}
}
}
}
break;
}
// Decode chunk and add to buffer
const chunk = decoder.decode(value, { stream: true });
buffer += chunk;
// Process complete lines from buffer
const lines = buffer.split('\n');
// Keep the last potentially incomplete line in buffer
buffer = lines.pop() || '';
// Process complete lines in order
for (const line of lines) {
if (line.trim() && line.startsWith('data: ')) {
const data = line.slice(6).trim();
if (data === '[DONE]') continue;
try {
const parsed = JSON.parse(data);
processStreamChunk(parsed);
} catch (e) {
// Parse error
continue;
}
}
}
}
} finally {
reader.releaseLock();
}
function processStreamChunk(parsed: any) {
// Capture usage from the chunk if available
if (parsed.usage) {
streamUsage = {
input_tokens: parsed.usage.prompt_tokens || 0,
output_tokens: parsed.usage.completion_tokens || 0,
};
}
const delta = parsed.choices?.[0]?.delta;
if (delta) {
processStreamDelta(delta);
}
}
function closeCurrentBlock() {
if (hasAnyBlock) {
enqueueSSE(controller, "content_block_stop", {
type: "content_block_stop",
index: contentBlockIndex,
});
contentBlockIndex++;
}
hasAnyBlock = true;
}
function processStreamDelta(delta: any) {
// Handle tool calls
if (delta.tool_calls?.length > 0) {
for (const toolCall of delta.tool_calls) {
const toolCallId = toolCall.id;
if (toolCallId && toolCallId !== currentToolCallId) {
closeCurrentBlock();
isToolUse = true;
hasStartedTextBlock = false;
currentToolCallId = toolCallId;
toolCallJsonMap.set(toolCallId, "");
const toolBlock = {
type: "tool_use",
id: toolCallId,
name: toolCall.function?.name,
input: {},
};
enqueueSSE(controller, "content_block_start", {
type: "content_block_start",
index: contentBlockIndex,
content_block: toolBlock,
});
}
if (toolCall.function?.arguments && currentToolCallId) {
const currentJson = toolCallJsonMap.get(currentToolCallId) || "";
toolCallJsonMap.set(currentToolCallId, currentJson + toolCall.function.arguments);
enqueueSSE(controller, "content_block_delta", {
type: "content_block_delta",
index: contentBlockIndex,
delta: {
type: "input_json_delta",
partial_json: toolCall.function.arguments,
},
});
}
}
} else if (delta.content) {
if (isToolUse) {
closeCurrentBlock();
isToolUse = false;
currentToolCallId = null;
}
if (!hasStartedTextBlock) {
if (!hasAnyBlock) {
hasAnyBlock = true;
}
enqueueSSE(controller, "content_block_start", {
type: "content_block_start",
index: contentBlockIndex,
content_block: {
type: "text",
text: "",
},
});
hasStartedTextBlock = true;
}
enqueueSSE(controller, "content_block_delta", {
type: "content_block_delta",
index: contentBlockIndex,
delta: {
type: "text_delta",
text: delta.content,
},
});
}
}
// Close last content block
if (hasAnyBlock) {
enqueueSSE(controller, "content_block_stop", {
type: "content_block_stop",
index: contentBlockIndex,
});
}
// Send message_delta and message_stop
enqueueSSE(controller, "message_delta", {
type: "message_delta",
delta: {
stop_reason: isToolUse ? "tool_use" : "end_turn",
stop_sequence: null,
},
usage: streamUsage || { input_tokens: 0, output_tokens: 0 },
});
enqueueSSE(controller, "message_stop", {
type: "message_stop",
});
controller.close();
},
});
}

File diff suppressed because it is too large Load Diff

286
src/skills-script.ts Normal file
View File

@@ -0,0 +1,286 @@
const getSkillsScript = () => `
// ─── Skills ───
var skillsSearchTimeout = null;
var skillsCache = null;
var topSkills = (window.__topSkills || []);
function showSkillsModal() {
document.getElementById('skillsModal').style.display = 'flex';
loadInstalledSkills();
if (topSkills.length > 0) {
renderFeaturedSkills(topSkills);
}
}
function renderFeaturedSkills(skills) {
var grid = document.getElementById('skillsGrid');
if (!grid) return;
var html = '';
skills.forEach(function(skill) {
var name = skill.name || 'Unknown';
var installs = skill.installs || 0;
var source = skill.source || '';
var installsHtml = installs > 0 ? '<span class="marketplace-item-stars">' + (installs >= 1000 ? (Math.round(installs / 100) / 10) + 'k' : installs) + ' installs</span>' : '';
var safeId = escapeHtml(skill.id || name).replace(/'/g, '&#39;');
var rawUrl = skill.rawUrl || '';
var installsText = installs >= 1000 ? (Math.round(installs / 100) / 10) + 'k installs' : (installs > 0 ? installs + ' installs' : '');
html += '<div class="marketplace-item" data-skill-id="' + safeId + '" data-skill-source="' + escapeHtml(source) + '" data-skill-name="' + escapeHtml(name) + '" data-skill-rawurl="' + escapeHtml(rawUrl) + '" data-skill-installs="' + escapeHtml(installsText) + '" onclick="installSkillFromMarketplace(this)">' +
'<div class="marketplace-item-header">' +
'<div class="marketplace-item-icon-placeholder">' + escapeHtml(name.charAt(0).toUpperCase()) + '</div>' +
'<div class="marketplace-item-info">' +
'<div class="marketplace-item-name">' + escapeHtml(name) + '</div>' +
'<div class="marketplace-item-meta">' + installsHtml + '</div>' +
'</div>' +
'</div>' +
'<div class="marketplace-item-desc">' + escapeHtml(source) + '</div>' +
'</div>';
});
grid.innerHTML = html;
}
function hideSkillsModal() {
document.getElementById('skillsModal').style.display = 'none';
}
function loadInstalledSkills() {
vscode.postMessage({ type: 'loadSkills' });
}
function displaySkills(skills) {
var skillsList = document.getElementById('skillsList');
skillsList.innerHTML = '';
if (!skills || skills.length === 0) {
skillsList.innerHTML = '<div class="no-servers">' +
'<div class="no-servers-icon"><svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M13 2L3 14h9l-1 8 10-12h-9l1-8z"/></svg></div>' +
'<div class="no-servers-text">No skills installed</div>' +
'<button class="btn outlined no-servers-btn" onclick="showSkillAddForm()">+ Create skill</button>' +
'</div>';
return;
}
skills.forEach(function(skill, idx) {
var item = document.createElement('div');
item.className = 'mcp-server-item';
item.style.flexDirection = 'column';
item.style.alignItems = 'stretch';
var desc = skill.description || 'No description';
var content = skill.content || '';
var detailId = 'skill-detail-' + idx;
item.innerHTML = '<div class="skill-item-row">' +
'<div class="skill-item-info">' +
'<div class="server-name">' + escapeHtml(skill.name) + ' <span class="server-type">' + escapeHtml(skill.scope) + '</span></div>' +
'<div class="skill-item-desc">' + escapeHtml(desc) + '</div>' +
'</div>' +
'<div class="server-actions" style="flex-shrink:0;">' +
'<button class="btn outlined" style="font-size:11px;padding:3px 8px;" onclick="toggleSkillDetail(\\'' + detailId + '\\')">Details</button>' +
'<button class="btn outlined server-delete-btn" data-skill="' + escapeHtml(skill.name) + '" data-scope="' + escapeHtml(skill.scope) + '" onclick="deleteSkill(this.dataset.skill, this.dataset.scope)">Delete</button>' +
'</div>' +
'</div>' +
'<div id="' + detailId + '" class="skill-detail-content" style="display:none;">' +
'<pre style="white-space:pre-wrap;font-size:11px;color:var(--vscode-descriptionForeground);margin:8px 0 0;max-height:200px;overflow-y:auto;">' + escapeHtml(content) + '</pre>' +
'</div>';
skillsList.appendChild(item);
});
// Add create button at bottom
var addDiv = document.createElement('div');
addDiv.className = 'mcp-add-server';
addDiv.innerHTML = '<button class="btn outlined" onclick="showSkillAddForm()">+ Create skill</button>';
skillsList.appendChild(addDiv);
}
function showSkillAddForm() {
document.getElementById('skillsList').style.display = 'none';
document.getElementById('skillsMarketplace').style.display = 'none';
document.getElementById('skillAddForm').style.display = 'block';
// Clear form
document.getElementById('skillName').value = '';
document.getElementById('skillDescription').value = '';
document.getElementById('skillContent').value = '';
document.getElementById('skillName').disabled = false;
}
function hideSkillAddForm() {
document.getElementById('skillsList').style.display = '';
document.getElementById('skillsMarketplace').style.display = 'block';
document.getElementById('skillAddForm').style.display = 'none';
loadInstalledSkills();
}
function saveSkill() {
var name = document.getElementById('skillName').value.trim();
var description = document.getElementById('skillDescription').value.trim();
var scope = document.getElementById('skillScope').value;
var content = document.getElementById('skillContent').value;
if (!name) return;
// Build SKILL.md content
var skillMd = '---\\n';
skillMd += 'name: ' + name + '\\n';
if (description) {
skillMd += 'description: ' + description + '\\n';
}
skillMd += '---\\n\\n';
skillMd += content || '';
vscode.postMessage({
type: 'saveSkill',
name: name,
scope: scope,
content: skillMd
});
hideSkillAddForm();
}
function deleteSkill(name, scope) {
vscode.postMessage({
type: 'deleteSkill',
name: name,
scope: scope
});
}
function searchSkills(query) {
clearTimeout(skillsSearchTimeout);
skillsSearchTimeout = setTimeout(function() {
if (!query || query.length < 2) {
renderFeaturedSkills(topSkills);
return;
}
// Filter featured locally first
var q = query.toLowerCase();
var local = topSkills.filter(function(s) {
return (s.name && s.name.toLowerCase().indexOf(q) >= 0) ||
(s.source && s.source.toLowerCase().indexOf(q) >= 0);
});
if (local.length > 0) {
renderFeaturedSkills(local);
} else {
var grid = document.getElementById('skillsGrid');
grid.innerHTML = '<div class="marketplace-loading">Searching...</div>';
}
// Also search API
vscode.postMessage({ type: 'searchSkills', query: query });
}, 300);
}
function handleSkillsSearchResponse(data) {
var grid = document.getElementById('skillsGrid');
if (!grid) return;
var skills = data.skills || [];
if (skills.length === 0) {
grid.innerHTML = '<div class="marketplace-loading">No skills found.</div>';
return;
}
var html = '';
skills.forEach(function(skill) {
var name = skill.name || skill.skillId || 'Unknown';
var installs = skill.installs || 0;
var source = skill.source || '';
var safeId = escapeHtml(skill.id || name).replace(/'/g, '&#39;');
var installsHtml = installs > 0 ? '<span class="marketplace-item-stars">' + (installs >= 1000 ? (Math.round(installs / 100) / 10) + 'k' : installs) + ' installs</span>' : '';
var rawUrl = skill.rawUrl || '';
var installsText = installs >= 1000 ? (Math.round(installs / 100) / 10) + 'k installs' : (installs > 0 ? installs + ' installs' : '');
html += '<div class="marketplace-item" data-skill-id="' + safeId + '" data-skill-source="' + escapeHtml(source) + '" data-skill-name="' + escapeHtml(name) + '" data-skill-rawurl="' + escapeHtml(rawUrl) + '" data-skill-installs="' + escapeHtml(installsText) + '" onclick="installSkillFromMarketplace(this)">' +
'<div class="marketplace-item-header">' +
'<div class="marketplace-item-icon-placeholder">' + escapeHtml(name.charAt(0).toUpperCase()) + '</div>' +
'<div class="marketplace-item-info">' +
'<div class="marketplace-item-name">' + escapeHtml(name) + '</div>' +
'<div class="marketplace-item-meta">' + installsHtml + '</div>' +
'</div>' +
'</div>' +
'<div class="marketplace-item-desc">' + escapeHtml(source) + '</div>' +
'</div>';
});
grid.innerHTML = html;
}
var skillsDisplayedList = null;
function installSkillFromMarketplace(el) {
var source = el.dataset.skillSource;
var name = el.dataset.skillName;
var installs = el.dataset.skillInstalls || '';
if (!source || !name) return;
var repoUrl = 'https://github.com/' + source.replace(/^github\\//, '');
var installsHtml = installs ? '<span class="marketplace-item-stars">' + installs + '</span>' : '';
var grid = document.getElementById('skillsGrid');
// Save current grid content to restore on back
skillsDisplayedList = grid.innerHTML;
grid.innerHTML = '<div class="marketplace-detail">' +
'<button class="marketplace-back-btn" onclick="backToSkillsList()">&#8592; Back</button>' +
'<div class="marketplace-detail-header">' +
'<div class="marketplace-item-icon-placeholder" style="width:40px;height:40px;font-size:18px;">' + escapeHtml(name.charAt(0).toUpperCase()) + '</div>' +
'<div class="marketplace-detail-header-info">' +
'<div class="marketplace-detail-name">' + escapeHtml(name) + '</div>' +
'<div class="marketplace-detail-header-meta">' +
installsHtml +
'<a href="' + escapeHtml(repoUrl) + '" target="_blank" class="marketplace-detail-link">GitHub</a>' +
'</div>' +
'</div>' +
'</div>' +
'<div class="marketplace-detail-desc">' + escapeHtml('Source: ' + source) + '</div>' +
'<div class="marketplace-detail-config">' +
'<div class="marketplace-detail-section-title">Install to</div>' +
'<div class="form-group" style="margin:0;">' +
'<select id="skillInstallScope">' +
'<option value="project">Project (.claude/skills/)</option>' +
'<option value="global">Global (~/.claude/skills/)</option>' +
'</select>' +
'</div>' +
'</div>' +
'<div class="marketplace-detail-actions" style="margin-top:12px;">' +
'<button class="btn" data-source="' + escapeHtml(source) + '" data-name="' + escapeHtml(name) + '" onclick="confirmSkillInstall(this)">Install</button>' +
'<div style="font-size:11px;color:var(--vscode-descriptionForeground);margin-top:6px;">Opens a terminal running <code style="font-size:10px;">npx skills add</code> via <a href="https://skills.sh" target="_blank" class="marketplace-detail-link">skills.sh</a></div>' +
'</div>' +
'</div>';
}
function backToSkillsList() {
var grid = document.getElementById('skillsGrid');
if (skillsDisplayedList) {
grid.innerHTML = skillsDisplayedList;
} else {
renderFeaturedSkills(topSkills);
}
}
function toggleSkillDetail(id) {
var el = document.getElementById(id);
if (!el) return;
el.style.display = el.style.display === 'none' ? 'block' : 'none';
}
function confirmSkillInstall(btn) {
var source = btn.dataset.source;
var name = btn.dataset.name;
var scope = document.getElementById('skillInstallScope').value;
var repoUrl = 'https://github.com/' + source.replace(/^github\\//, '');
var command = 'npx -y skills add ' + repoUrl + ' --skill ' + name + ' --agent claude-code -y';
if (scope === 'global') {
command += ' --global';
}
vscode.postMessage({
type: 'runTerminalCommand',
command: command
});
hideSkillsModal();
}
`;
export default getSkillsScript;

51
src/skills-ui.ts Normal file
View File

@@ -0,0 +1,51 @@
const getSkillsHtml = () => `
<!-- Skills modal -->
<div id="skillsModal" class="tools-modal" style="display: none;">
<div class="tools-modal-content">
<div class="tools-modal-header">
<span>Skills</span>
<button class="tools-close-btn" onclick="hideSkillsModal()">✕</button>
</div>
<div class="tools-list">
<div class="mcp-servers-list" id="skillsList">
<!-- Installed skills will be loaded here -->
</div>
<div class="mcp-popular-servers" id="skillsMarketplace">
<h4>Search Skills</h4>
<div class="marketplace-search">
<input type="text" id="skillsSearch" placeholder="Search skills..." oninput="searchSkills(this.value)" />
</div>
<div class="marketplace-grid" id="skillsGrid">
</div>
</div>
<div class="mcp-add-form" id="skillAddForm" style="display: none;">
<div class="form-group">
<label for="skillName">Skill Name:</label>
<input type="text" id="skillName" placeholder="my-skill" required>
</div>
<div class="form-group">
<label for="skillDescription">Description:</label>
<input type="text" id="skillDescription" placeholder="What this skill does">
</div>
<div class="form-group">
<label for="skillScope">Scope:</label>
<select id="skillScope">
<option value="personal">Personal (~/.claude/skills/)</option>
<option value="project">Project (.claude/skills/)</option>
</select>
</div>
<div class="form-group">
<label for="skillContent">Instructions (Markdown):</label>
<textarea id="skillContent" placeholder="Instructions for Claude to follow when this skill is invoked..." rows="8"></textarea>
</div>
<div class="form-buttons">
<button class="btn" onclick="saveSkill()">Create Skill</button>
<button class="btn outlined" onclick="hideSkillAddForm()">Cancel</button>
</div>
</div>
</div>
</div>
</div>
`;
export default getSkillsHtml;

479
src/top-mcp-servers.json Normal file
View File

@@ -0,0 +1,479 @@
[
{
"id": "sequential-thinking",
"name": "Sequential Thinking",
"description": "Step-by-step reasoning capabilities",
"icon": "",
"stars": 0,
"url": "",
"installType": "npm",
"installConfig": {
"type": "stdio",
"command": "npx",
"args": [
"-y",
"@modelcontextprotocol/server-sequential-thinking"
]
},
"featured": true
},
{
"id": "memory",
"name": "Memory",
"description": "Knowledge graph storage",
"icon": "",
"stars": 0,
"url": "",
"installType": "npm",
"installConfig": {
"type": "stdio",
"command": "npx",
"args": [
"-y",
"@modelcontextprotocol/server-memory"
]
},
"featured": true
},
{
"id": "puppeteer",
"name": "Puppeteer",
"description": "Browser automation",
"icon": "",
"stars": 0,
"url": "",
"installType": "npm",
"installConfig": {
"type": "stdio",
"command": "npx",
"args": [
"-y",
"@modelcontextprotocol/server-puppeteer"
]
},
"featured": true
},
{
"id": "fetch",
"name": "Fetch",
"description": "HTTP requests & web scraping",
"icon": "",
"stars": 0,
"url": "",
"installType": "npm",
"installConfig": {
"type": "stdio",
"command": "npx",
"args": [
"-y",
"@modelcontextprotocol/server-fetch"
]
},
"featured": true
},
{
"id": "filesystem",
"name": "Filesystem",
"description": "File operations & management",
"icon": "",
"stars": 0,
"url": "",
"installType": "npm",
"installConfig": {
"type": "stdio",
"command": "npx",
"args": [
"-y",
"@modelcontextprotocol/server-filesystem"
]
},
"featured": true
},
{
"id": "io.github.upstash/context7",
"name": "Context7",
"description": "Up-to-date code docs for any prompt",
"icon": "",
"stars": 0,
"url": "https://github.com/upstash/context7",
"installType": "npm",
"installConfig": {
"type": "stdio",
"command": "npx",
"args": [
"-y",
"@upstash/context7-mcp"
],
"env": {
"CONTEXT7_API_KEY": ""
}
},
"featured": true
},
{
"id": "com.airtable/mcp",
"name": "Airtable",
"description": "Official Airtable MCP server for managing bases, tables, and records.",
"icon": "",
"stars": 0,
"url": "",
"installType": "http",
"installConfig": {
"type": "http",
"url": "https://mcp.airtable.com/mcp",
"headers": {
"Authorization": ""
}
}
},
{
"id": "com.apify/mcp",
"name": "Apify",
"description": "Extract data from social media, search engines, maps, e-commerce sites, and any website using thousands of ready-made tools from Apify Store.",
"icon": "",
"stars": 0,
"url": "https://github.com/apify/apify-mcp-server",
"installType": "http",
"installConfig": {
"type": "http",
"url": "https://mcp.apify.com"
}
},
{
"id": "io.github.browserbase/mcp-server-browserbase",
"name": "Browserbase",
"description": "MCP server for AI web browser automation using Browserbase and Stagehand",
"icon": "",
"stars": 0,
"url": "https://github.com/browserbase/mcp-server-browserbase",
"installType": "npm",
"installConfig": {
"type": "stdio",
"command": "npx",
"args": [
"-y",
"@browserbasehq/mcp-server-browserbase"
],
"env": {
"BROWSERBASE_API_KEY": "",
"BROWSERBASE_PROJECT_ID": "",
"GEMINI_API_KEY": ""
}
}
},
{
"id": "io.github.clerk/mcp-server",
"name": "Clerk",
"description": "Access Clerk authentication docs, SDK snippets, and quickstart guides",
"icon": "",
"stars": 0,
"url": "https://clerk.com/docs/guides/ai/mcp/clerk-mcp-server",
"installType": "http",
"installConfig": {
"type": "http",
"url": "https://mcp.clerk.com/mcp"
}
},
{
"id": "com.cloudflare.mcp/mcp",
"name": "Cloudflare",
"description": "Cloudflare MCP servers",
"icon": "",
"stars": 0,
"url": "https://github.com/cloudflare/mcp-server-cloudflare",
"installType": "http",
"installConfig": {
"type": "http",
"url": "https://docs.mcp.cloudflare.com/mcp"
}
},
{
"id": "ai.exa/mcp",
"name": "Exa",
"description": "Web search and code search MCP server powered by Exa",
"icon": "",
"stars": 0,
"url": "https://github.com/exa-labs/exa-mcp-server",
"installType": "http",
"installConfig": {
"type": "http",
"url": "https://mcp.exa.ai/mcp"
}
},
{
"id": "com.figma/mcp",
"name": "Figma",
"description": "Official Figma MCP server for accessing design files, components, and design context",
"icon": "",
"stars": 0,
"url": "https://help.figma.com/hc/en-us/articles/35281350665623-Figma-MCP-collection-How-to-set-up-the-Figma-remote-MCP-server-preferred",
"installType": "http",
"installConfig": {
"type": "http",
"url": "https://mcp.figma.com/mcp"
}
},
{
"id": "dev.firecrawl/mcp",
"name": "Firecrawl",
"description": "Web scraping, crawling, search, and structured data extraction powered by Firecrawl.",
"icon": "",
"stars": 0,
"url": "https://github.com/firecrawl/firecrawl-mcp-server",
"installType": "http",
"installConfig": {
"type": "http",
"url": "https://mcp.firecrawl.dev/v2/mcp",
"headers": {
"Authorization": ""
}
}
},
{
"id": "io.github.github/github-mcp-server",
"name": "GitHub",
"description": "Official GitHub MCP server for repos, issues, PRs, and workflows",
"icon": "",
"stars": 0,
"url": "https://github.com/github/github-mcp-server",
"installType": "http",
"installConfig": {
"type": "http",
"url": "https://api.githubcopilot.com/mcp/"
}
},
{
"id": "app.linear/linear",
"name": "Linear",
"description": "MCP server for Linear project management and issue tracking",
"icon": "",
"stars": 0,
"url": "",
"installType": "sse",
"installConfig": {
"type": "sse",
"url": "https://mcp.linear.app/sse"
}
},
{
"id": "com.mux/mcp",
"name": "Mux",
"description": "The official MCP Server for the Mux API",
"icon": "",
"stars": 0,
"url": "https://github.com/muxinc/mux-node-sdk",
"installType": "http",
"installConfig": {
"type": "http",
"url": "https://mcp.mux.com",
"headers": {
"Authorization": ""
}
}
},
{
"id": "com.neon/mcp",
"name": "Neon",
"description": "Official Neon MCP server for managing Neon projects and Postgres databases.",
"icon": "",
"stars": 0,
"url": "https://github.com/neondatabase/mcp-server-neon",
"installType": "http",
"installConfig": {
"type": "http",
"url": "https://mcp.neon.tech/mcp",
"headers": {
"Authorization": "",
"x-read-only": ""
}
}
},
{
"id": "com.netlify/mcp",
"name": "Netlify",
"description": "Netlify's official MCP server for builds, deploys, and project management.",
"icon": "",
"stars": 0,
"url": "https://github.com/netlify/netlify-mcp",
"installType": "npm",
"installConfig": {
"type": "stdio",
"command": "npx",
"args": [
"-y",
"@netlify/mcp"
],
"env": {
"NETLIFY_PERSONAL_ACCESS_TOKEN": ""
}
}
},
{
"id": "io.github.vercel/next-devtools-mcp",
"name": "Next.js Devtools",
"description": "Next.js development tools MCP server with stdio transport",
"icon": "",
"stars": 0,
"url": "https://github.com/vercel/next-devtools-mcp",
"installType": "npm",
"installConfig": {
"type": "stdio",
"command": "npx",
"args": [
"-y",
"next-devtools-mcp"
]
}
},
{
"id": "com.notion/mcp",
"name": "Notion",
"description": "Official Notion MCP server",
"icon": "",
"stars": 0,
"url": "",
"installType": "http",
"installConfig": {
"type": "http",
"url": "https://mcp.notion.com/mcp"
}
},
{
"id": "io.github.railwayapp/mcp-server",
"name": "Railway",
"description": "Official Railway MCP server",
"icon": "",
"stars": 0,
"url": "https://github.com/railwayapp/railway-mcp-server",
"installType": "npm",
"installConfig": {
"type": "stdio",
"command": "npx",
"args": [
"-y",
"@railway/mcp-server"
]
}
},
{
"id": "com.render/mcp",
"name": "Render",
"description": "Official Render MCP server for managing Render resources.",
"icon": "",
"stars": 0,
"url": "https://github.com/render-oss/render-mcp-server",
"installType": "http",
"installConfig": {
"type": "http",
"url": "https://mcp.render.com/mcp",
"headers": {
"Authorization": ""
}
}
},
{
"id": "com.resend/mcp",
"name": "Resend",
"description": "Official Resend MCP server for email operations and audience management.",
"icon": "",
"stars": 0,
"url": "https://github.com/resend/mcp-send-email",
"installType": "npm",
"installConfig": {
"type": "stdio",
"command": "npx",
"args": [
"-y",
"resend-mcp"
],
"env": {
"RESEND_API_KEY": ""
}
}
},
{
"id": "io.sanity.www/mcp",
"name": "Sanity",
"description": "Direct access to your Sanity projects (content, datasets, releases, schemas) and agent rules",
"icon": "",
"stars": 0,
"url": "https://github.com/sanity-io/agent-toolkit",
"installType": "http",
"installConfig": {
"type": "http",
"url": "https://mcp.sanity.io"
}
},
{
"id": "io.github.getsentry/sentry-mcp",
"name": "Sentry",
"description": "MCP server for Sentry issue tracking and debugging",
"icon": "",
"stars": 0,
"url": "https://github.com/getsentry/sentry-mcp",
"installType": "npm",
"installConfig": {
"type": "stdio",
"command": "npx",
"args": [
"-y",
"@sentry/mcp-server"
],
"env": {
"SENTRY_ACCESS_TOKEN": ""
}
}
},
{
"id": "com.slack/mcp",
"name": "Slack",
"description": "Official Slack MCP server for search, messaging, canvases, and users.",
"icon": "",
"stars": 0,
"url": "https://github.com/slackapi/slack-mcp-plugin",
"installType": "http",
"installConfig": {
"type": "http",
"url": "https://mcp.slack.com/mcp"
}
},
{
"id": "com.stripe/mcp",
"name": "Stripe",
"description": "Official Stripe MCP server for Stripe API tools.",
"icon": "",
"stars": 0,
"url": "https://github.com/stripe/agent-toolkit",
"installType": "http",
"installConfig": {
"type": "http",
"url": "https://mcp.stripe.com"
}
},
{
"id": "com.supabase/mcp",
"name": "Supabase",
"description": "MCP server for interacting with the Supabase platform",
"icon": "",
"stars": 0,
"url": "https://github.com/supabase-community/supabase-mcp",
"installType": "http",
"installConfig": {
"type": "http",
"url": "https://mcp.supabase.com/mcp"
}
},
{
"id": "com.vercel/vercel-mcp",
"name": "Vercel",
"description": "An MCP server for Vercel",
"icon": "",
"stars": 0,
"url": "https://github.com/vercel/vercel-mcp-overview",
"installType": "http",
"installConfig": {
"type": "http",
"url": "https://mcp.vercel.com"
}
}
]

240
src/top-plugins.json Normal file
View File

@@ -0,0 +1,240 @@
[
{
"name": "agent-sdk-dev",
"description": "Claude Agent SDK Development Plugin",
"verified": true,
"type": "official",
"installId": "agent-sdk-dev@claude-plugins-official"
},
{
"name": "claude-code-setup",
"description": "Analyze codebases and recommend tailored Claude Code automations such as hooks, skills, MCP servers, and subagents.",
"verified": true,
"type": "official",
"installId": "claude-code-setup@claude-plugins-official"
},
{
"name": "claude-md-management",
"description": "Tools to maintain and improve CLAUDE.md files - audit quality, capture session learnings, and keep project memory current.",
"verified": true,
"type": "official",
"installId": "claude-md-management@claude-plugins-official"
},
{
"name": "code-review",
"description": "Automated code review for pull requests using multiple specialized agents with confidence-based scoring",
"verified": true,
"type": "official",
"installId": "code-review@claude-plugins-official"
},
{
"name": "code-simplifier",
"description": "Agent that simplifies and refines code for clarity, consistency, and maintainability while preserving functionality",
"verified": true,
"type": "official",
"installId": "code-simplifier@claude-plugins-official"
},
{
"name": "commit-commands",
"description": "Streamline your git workflow with simple commands for committing, pushing, and creating pull requests",
"verified": true,
"type": "official",
"installId": "commit-commands@claude-plugins-official"
},
{
"name": "explanatory-output-style",
"description": "Adds educational insights about implementation choices and codebase patterns (mimics the deprecated Explanatory output style)",
"verified": true,
"type": "official",
"installId": "explanatory-output-style@claude-plugins-official"
},
{
"name": "feature-dev",
"description": "Comprehensive feature development workflow with specialized agents for codebase exploration, architecture design, and quality review",
"verified": true,
"type": "official",
"installId": "feature-dev@claude-plugins-official"
},
{
"name": "frontend-design",
"description": "Frontend design skill for UI/UX implementation",
"verified": true,
"type": "official",
"installId": "frontend-design@claude-plugins-official"
},
{
"name": "hookify",
"description": "Easily create hooks to prevent unwanted behaviors by analyzing conversation patterns",
"verified": true,
"type": "official",
"installId": "hookify@claude-plugins-official"
},
{
"name": "learning-output-style",
"description": "Interactive learning mode that requests meaningful code contributions at decision points (mimics the unshipped Learning output style)",
"verified": true,
"type": "official",
"installId": "learning-output-style@claude-plugins-official"
},
{
"name": "math-olympiad",
"description": "Solve competition math (IMO, Putnam, USAMO) with adversarial verification that catches what self-verification misses. Fresh-context verifiers attack proofs with specific failure patterns. Calibrated abstention over bluffing.",
"verified": true,
"type": "official",
"installId": "math-olympiad@claude-plugins-official"
},
{
"name": "mcp-server-dev",
"description": "Skills for designing and building MCP servers that work seamlessly with Claude \u2014 guides you through deployment models (remote HTTP, MCPB, local), tool design patterns, auth, and interactive MCP apps.",
"verified": true,
"type": "official",
"installId": "mcp-server-dev@claude-plugins-official"
},
{
"name": "playground",
"description": "Creates interactive HTML playgrounds \u2014 self-contained single-file explorers with visual controls, live preview, and prompt output with copy button",
"verified": true,
"type": "official",
"installId": "playground@claude-plugins-official"
},
{
"name": "plugin-dev",
"description": "Plugin development toolkit with skills for creating agents, commands, hooks, MCP integrations, and comprehensive plugin structure guidance",
"verified": true,
"type": "official",
"installId": "plugin-dev@claude-plugins-official"
},
{
"name": "pr-review-toolkit",
"description": "Comprehensive PR review agents specializing in comments, tests, error handling, type design, code quality, and code simplification",
"verified": true,
"type": "official",
"installId": "pr-review-toolkit@claude-plugins-official"
},
{
"name": "ralph-loop",
"description": "Continuous self-referential AI loops for interactive iterative development, implementing the Ralph Wiggum technique. Run Claude in a while-true loop with the same prompt until task completion.",
"verified": true,
"type": "official",
"installId": "ralph-loop@claude-plugins-official"
},
{
"name": "security-guidance",
"description": "Security reminder hook that warns about potential security issues when editing files, including command injection, XSS, and unsafe code patterns",
"verified": true,
"type": "official",
"installId": "security-guidance@claude-plugins-official"
},
{
"name": "skill-creator",
"description": "Create new skills, improve existing skills, and measure skill performance. Use when users want to create a skill from scratch, update or optimize an existing skill, run evals to test a skill, or benchmark skill performance with variance analysis.",
"verified": true,
"type": "official",
"installId": "skill-creator@claude-plugins-official"
},
{
"name": "asana",
"description": "Asana project management integration. Create and manage tasks, search projects, update assignments, track progress, and integrate your development workflow with Asana's work management platform.",
"verified": false,
"type": "external",
"installId": "asana@claude-plugins-official"
},
{
"name": "context7",
"description": "Upstash Context7 MCP server for up-to-date documentation lookup. Pull version-specific documentation and code examples directly from source repositories into your LLM context.",
"verified": false,
"type": "external",
"installId": "context7@claude-plugins-official"
},
{
"name": "discord",
"description": "Discord channel for Claude Code \u2014 messaging bridge with built-in access control. Manage pairing, allowlists, and policy via /discord:access.",
"verified": false,
"type": "external",
"installId": "discord@claude-plugins-official"
},
{
"name": "fakechat",
"description": "Localhost iMessage-style web chat for Claude Code \u2014 test surface with file upload and edits. No tokens, no access control.",
"verified": false,
"type": "external",
"installId": "fakechat@claude-plugins-official"
},
{
"name": "firebase",
"description": "Google Firebase MCP integration. Manage Firestore databases, authentication, cloud functions, hosting, and storage. Build and manage your Firebase backend directly from your development workflow.",
"verified": false,
"type": "external",
"installId": "firebase@claude-plugins-official"
},
{
"name": "github",
"description": "Official GitHub MCP server for repository management. Create issues, manage pull requests, review code, search repositories, and interact with GitHub's full API directly from Claude Code.",
"verified": false,
"type": "external",
"installId": "github@claude-plugins-official"
},
{
"name": "gitlab",
"description": "GitLab DevOps platform integration. Manage repositories, merge requests, CI/CD pipelines, issues, and wikis. Full access to GitLab's comprehensive DevOps lifecycle tools.",
"verified": false,
"type": "external",
"installId": "gitlab@claude-plugins-official"
},
{
"name": "greptile",
"description": "AI code review agent for GitHub and GitLab. View and resolve Greptile's PR review comments directly from Claude Code.",
"verified": false,
"type": "external",
"installId": "greptile@claude-plugins-official"
},
{
"name": "laravel-boost",
"description": "Laravel development toolkit MCP server. Provides intelligent assistance for Laravel applications including Artisan commands, Eloquent queries, routing, migrations, and framework-specific code generation.",
"verified": false,
"type": "external",
"installId": "laravel-boost@claude-plugins-official"
},
{
"name": "linear",
"description": "Linear issue tracking integration. Create issues, manage projects, update statuses, search across workspaces, and streamline your software development workflow with Linear's modern issue tracker.",
"verified": false,
"type": "external",
"installId": "linear@claude-plugins-official"
},
{
"name": "playwright",
"description": "Browser automation and end-to-end testing MCP server by Microsoft. Enables Claude to interact with web pages, take screenshots, fill forms, click elements, and perform automated browser testing workflows.",
"verified": false,
"type": "external",
"installId": "playwright@claude-plugins-official"
},
{
"name": "serena",
"description": "Semantic code analysis MCP server providing intelligent code understanding, refactoring suggestions, and codebase navigation through language server protocol integration.",
"verified": false,
"type": "external",
"installId": "serena@claude-plugins-official"
},
{
"name": "slack",
"description": "Slack workspace integration. Search messages, access channels, read threads, and stay connected with your team's communications while coding. Find relevant discussions and context quickly.",
"verified": false,
"type": "external",
"installId": "slack@claude-plugins-official"
},
{
"name": "supabase",
"description": "Supabase MCP integration for database operations, authentication, storage, and real-time subscriptions. Manage your Supabase projects, run SQL queries, and interact with your backend directly.",
"verified": false,
"type": "external",
"installId": "supabase@claude-plugins-official"
},
{
"name": "telegram",
"description": "Telegram channel for Claude Code \u2014 messaging bridge with built-in access control. Manage pairing, allowlists, and policy via /telegram:access.",
"verified": false,
"type": "external",
"installId": "telegram@claude-plugins-official"
}
]

289
src/top-skills.json Normal file
View File

@@ -0,0 +1,289 @@
[
{
"id": "vercel-labs/skills/find-skills",
"name": "find-skills",
"installs": 654260,
"source": "vercel-labs/skills",
"rawUrl": "https://raw.githubusercontent.com/vercel-labs/skills/main/skills/find-skills/SKILL.md"
},
{
"id": "vercel-labs/agent-skills/vercel-react-best-practices",
"name": "vercel-react-best-practices",
"installs": 234225,
"source": "vercel-labs/agent-skills",
"rawUrl": "https://raw.githubusercontent.com/vercel-labs/agent-skills/main/skills/react-best-practices/SKILL.md"
},
{
"id": "vercel-labs/agent-skills/web-design-guidelines",
"name": "web-design-guidelines",
"installs": 187122,
"source": "vercel-labs/agent-skills",
"rawUrl": "https://raw.githubusercontent.com/vercel-labs/agent-skills/main/skills/web-design-guidelines/SKILL.md"
},
{
"id": "anthropics/skills/frontend-design",
"name": "frontend-design",
"installs": 184608,
"source": "anthropics/skills",
"rawUrl": "https://raw.githubusercontent.com/anthropics/skills/main/skills/frontend-design/SKILL.md"
},
{
"id": "vercel-labs/agent-browser/agent-browser",
"name": "agent-browser",
"installs": 119125,
"source": "vercel-labs/agent-browser",
"rawUrl": "https://raw.githubusercontent.com/vercel-labs/agent-browser/main/skills/agent-browser/SKILL.md"
},
{
"id": "anthropics/skills/skill-creator",
"name": "skill-creator",
"installs": 97605,
"source": "anthropics/skills",
"rawUrl": "https://raw.githubusercontent.com/anthropics/skills/main/skills/skill-creator/SKILL.md"
},
{
"id": "nextlevelbuilder/ui-ux-pro-max-skill/ui-ux-pro-max",
"name": "ui-ux-pro-max",
"installs": 74564,
"source": "nextlevelbuilder/ui-ux-pro-max-skill",
"rawUrl": "https://raw.githubusercontent.com/nextlevelbuilder/ui-ux-pro-max-skill/main/.claude/skills/ui-ux-pro-max/SKILL.md"
},
{
"id": "microsoft/azure-skills/microsoft-foundry",
"name": "microsoft-foundry",
"installs": 74376,
"source": "microsoft/azure-skills",
"rawUrl": "https://raw.githubusercontent.com/microsoft/azure-skills/main/skills/microsoft-foundry/SKILL.md"
},
{
"id": "obra/superpowers/brainstorming",
"name": "brainstorming",
"installs": 66697,
"source": "obra/superpowers",
"rawUrl": "https://raw.githubusercontent.com/obra/superpowers/main/skills/brainstorming/SKILL.md"
},
{
"id": "browser-use/browser-use/browser-use",
"name": "browser-use",
"installs": 52773,
"source": "browser-use/browser-use",
"rawUrl": "https://raw.githubusercontent.com/browser-use/browser-use/main/skills/browser-use/SKILL.md"
},
{
"id": "coreyhaines31/marketingskills/seo-audit",
"name": "seo-audit",
"installs": 50157,
"source": "coreyhaines31/marketingskills",
"rawUrl": "https://raw.githubusercontent.com/coreyhaines31/marketingskills/main/skills/seo-audit/SKILL.md"
},
{
"id": "anthropics/skills/pdf",
"name": "pdf",
"installs": 45709,
"source": "anthropics/skills",
"rawUrl": "https://raw.githubusercontent.com/anthropics/skills/main/skills/pdf/SKILL.md"
},
{
"id": "supabase/agent-skills/supabase-postgres-best-practices",
"name": "supabase-postgres-best-practices",
"installs": 43862,
"source": "supabase/agent-skills",
"rawUrl": "https://raw.githubusercontent.com/supabase/agent-skills/main/skills/supabase-postgres-best-practices/SKILL.md"
},
{
"id": "coreyhaines31/marketingskills/copywriting",
"name": "copywriting",
"installs": 42743,
"source": "coreyhaines31/marketingskills",
"rawUrl": "https://raw.githubusercontent.com/coreyhaines31/marketingskills/main/skills/copywriting/SKILL.md"
},
{
"id": "anthropics/skills/pptx",
"name": "pptx",
"installs": 41526,
"source": "anthropics/skills",
"rawUrl": "https://raw.githubusercontent.com/anthropics/skills/main/skills/pptx/SKILL.md"
},
{
"id": "vercel-labs/next-skills/next-best-practices",
"name": "next-best-practices",
"installs": 40732,
"source": "vercel-labs/next-skills",
"rawUrl": "https://raw.githubusercontent.com/vercel-labs/next-skills/main/skills/next-best-practices/SKILL.md"
},
{
"id": "squirrelscan/skills/audit-website",
"name": "audit-website",
"installs": 37654,
"source": "squirrelscan/skills",
"rawUrl": "https://raw.githubusercontent.com/squirrelscan/skills/main/audit-website/SKILL.md"
},
{
"id": "obra/superpowers/systematic-debugging",
"name": "systematic-debugging",
"installs": 36470,
"source": "obra/superpowers",
"rawUrl": "https://raw.githubusercontent.com/obra/superpowers/main/skills/systematic-debugging/SKILL.md"
},
{
"id": "anthropics/skills/docx",
"name": "docx",
"installs": 35928,
"source": "anthropics/skills",
"rawUrl": "https://raw.githubusercontent.com/anthropics/skills/main/skills/docx/SKILL.md"
},
{
"id": "obra/superpowers/writing-plans",
"name": "writing-plans",
"installs": 35010,
"source": "obra/superpowers",
"rawUrl": "https://raw.githubusercontent.com/obra/superpowers/main/skills/writing-plans/SKILL.md"
},
{
"id": "shadcn/ui/shadcn",
"name": "shadcn",
"installs": 33897,
"source": "shadcn/ui",
"rawUrl": "https://raw.githubusercontent.com/shadcn/ui/main/skills/shadcn/SKILL.md"
},
{
"id": "anthropics/skills/xlsx",
"name": "xlsx",
"installs": 32936,
"source": "anthropics/skills",
"rawUrl": "https://raw.githubusercontent.com/anthropics/skills/main/skills/xlsx/SKILL.md"
},
{
"id": "obra/superpowers/using-superpowers",
"name": "using-superpowers",
"installs": 30937,
"source": "obra/superpowers",
"rawUrl": "https://raw.githubusercontent.com/obra/superpowers/main/skills/using-superpowers/SKILL.md"
},
{
"id": "coreyhaines31/marketingskills/marketing-psychology",
"name": "marketing-psychology",
"installs": 30917,
"source": "coreyhaines31/marketingskills",
"rawUrl": "https://raw.githubusercontent.com/coreyhaines31/marketingskills/main/skills/marketing-psychology/SKILL.md"
},
{
"id": "obra/superpowers/test-driven-development",
"name": "test-driven-development",
"installs": 30410,
"source": "obra/superpowers",
"rawUrl": "https://raw.githubusercontent.com/obra/superpowers/main/skills/test-driven-development/SKILL.md"
},
{
"id": "anthropics/skills/webapp-testing",
"name": "webapp-testing",
"installs": 29748,
"source": "anthropics/skills",
"rawUrl": "https://raw.githubusercontent.com/anthropics/skills/main/skills/webapp-testing/SKILL.md"
},
{
"id": "obra/superpowers/executing-plans",
"name": "executing-plans",
"installs": 28743,
"source": "obra/superpowers",
"rawUrl": "https://raw.githubusercontent.com/obra/superpowers/main/skills/executing-plans/SKILL.md"
},
{
"id": "obra/superpowers/requesting-code-review",
"name": "requesting-code-review",
"installs": 28421,
"source": "obra/superpowers",
"rawUrl": "https://raw.githubusercontent.com/obra/superpowers/main/skills/requesting-code-review/SKILL.md"
},
{
"id": "coreyhaines31/marketingskills/content-strategy",
"name": "content-strategy",
"installs": 27875,
"source": "coreyhaines31/marketingskills",
"rawUrl": "https://raw.githubusercontent.com/coreyhaines31/marketingskills/main/skills/content-strategy/SKILL.md"
},
{
"id": "coreyhaines31/marketingskills/programmatic-seo",
"name": "programmatic-seo",
"installs": 27820,
"source": "coreyhaines31/marketingskills",
"rawUrl": "https://raw.githubusercontent.com/coreyhaines31/marketingskills/main/skills/programmatic-seo/SKILL.md"
},
{
"id": "coreyhaines31/marketingskills/social-content",
"name": "social-content",
"installs": 26700,
"source": "coreyhaines31/marketingskills",
"rawUrl": "https://raw.githubusercontent.com/coreyhaines31/marketingskills/main/skills/social-content/SKILL.md"
},
{
"id": "coreyhaines31/marketingskills/product-marketing-context",
"name": "product-marketing-context",
"installs": 25930,
"source": "coreyhaines31/marketingskills",
"rawUrl": "https://raw.githubusercontent.com/coreyhaines31/marketingskills/main/skills/product-marketing-context/SKILL.md"
},
{
"id": "coreyhaines31/marketingskills/marketing-ideas",
"name": "marketing-ideas",
"installs": 25516,
"source": "coreyhaines31/marketingskills",
"rawUrl": "https://raw.githubusercontent.com/coreyhaines31/marketingskills/main/skills/marketing-ideas/SKILL.md"
},
{
"id": "roin-orca/skills/simple",
"name": "simple",
"installs": 25467,
"source": "roin-orca/skills",
"rawUrl": "https://raw.githubusercontent.com/roin-orca/skills/main/skills/simple/SKILL.md"
},
{
"id": "coreyhaines31/marketingskills/pricing-strategy",
"name": "pricing-strategy",
"installs": 25142,
"source": "coreyhaines31/marketingskills",
"rawUrl": "https://raw.githubusercontent.com/coreyhaines31/marketingskills/main/skills/pricing-strategy/SKILL.md"
},
{
"id": "anthropics/skills/mcp-builder",
"name": "mcp-builder",
"installs": 24764,
"source": "anthropics/skills",
"rawUrl": "https://raw.githubusercontent.com/anthropics/skills/main/skills/mcp-builder/SKILL.md"
},
{
"id": "obra/superpowers/subagent-driven-development",
"name": "subagent-driven-development",
"installs": 24432,
"source": "obra/superpowers",
"rawUrl": "https://raw.githubusercontent.com/obra/superpowers/main/skills/subagent-driven-development/SKILL.md"
},
{
"id": "coreyhaines31/marketingskills/copy-editing",
"name": "copy-editing",
"installs": 24073,
"source": "coreyhaines31/marketingskills",
"rawUrl": "https://raw.githubusercontent.com/coreyhaines31/marketingskills/main/skills/copy-editing/SKILL.md"
},
{
"id": "pbakaus/impeccable/frontend-design",
"name": "frontend-design",
"installs": 23984,
"source": "pbakaus/impeccable",
"rawUrl": "https://raw.githubusercontent.com/pbakaus/impeccable/main/.claude/skills/frontend-design/SKILL.md"
},
{
"id": "pbakaus/impeccable/polish",
"name": "polish",
"installs": 23360,
"source": "pbakaus/impeccable",
"rawUrl": "https://raw.githubusercontent.com/pbakaus/impeccable/main/.claude/skills/polish/SKILL.md"
},
{
"id": "google-labs-code/stitch-skills/design-md",
"name": "design-md",
"installs": 19272,
"source": "google-labs-code/stitch-skills",
"rawUrl": "https://raw.githubusercontent.com/google-labs-code/stitch-skills/main/skills/design-md/SKILL.md"
}
]

File diff suppressed because it is too large Load Diff

544
src/ui.ts
View File

@@ -1,12 +1,19 @@
import getScript from './script';
import styles from './ui-styles'
import recommendedModels from './recommended-models.json'
import topMcpServers from './top-mcp-servers.json'
import topSkills from './top-skills.json'
import topPlugins from './top-plugins.json'
import getSkillsHtml from './skills-ui'
import getPluginsHtml from './plugins-ui'
const getHtml = (isTelemetryEnabled: boolean) => `<!DOCTYPE html>
const getHtml = (isTelemetryEnabled: boolean, opencreditsApiUrl: string = 'https://ccc.api.opencredits.ai', opencreditsWebUrl: string = 'https://ccc.opencredits.ai', opencreditsPublishableKey: string = 'oc_pk_c43da4f9a9484ae484ad29bc97cc354f') => `<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="Content-Security-Policy" content="default-src * 'unsafe-inline' 'unsafe-eval' data: blob:; frame-src *;">
<title>Claude Code Chat</title>
${styles}
</head>
@@ -57,33 +64,48 @@ const getHtml = (isTelemetryEnabled: boolean) => `<!DOCTYPE html>
</div>
<div class="input-container" id="inputContainer">
<div class="input-modes">
<div class="mode-toggle">
<span onclick="togglePlanMode()">Plan First</span>
<div class="mode-switch" id="planModeSwitch" onclick="togglePlanMode()"></div>
</div>
<div class="mode-toggle">
<span id="thinkingModeLabel" onclick="toggleThinkingMode()">Thinking Mode</span>
<div class="mode-switch" id="thinkingModeSwitch" onclick="toggleThinkingMode()"></div>
<div class="model-selector-row">
<button class="model-selector-main" id="modelDropdownBtn" onclick="showModelSelector()" title="Select model">
<span id="modelDropdownText">Opus</span>
<svg width="8" height="8" viewBox="0 0 8 8" fill="currentColor"><path d="M1 2.5l3 3 3-3"></path></svg>
</button>
<button class="model-selector-main" id="modelSelector" onclick="showModelSelector()" title="Select model" style="display: none;">
<span class="model-selector-new" id="modelSelectorBadge">NEW</span>
<span id="modelSelectorText">Try other models</span>
</button>
<div class="model-quick-select" id="modelQuickSelect">
</div>
<button class="model-more-btn" id="modelMoreBtn" onclick="showModelSelector()" style="display: none;">+</button>
</div>
<div class="textarea-container">
<div class="textarea-wrapper">
<div class="image-preview-container" id="imagePreviewContainer" style="display: none;"></div>
<textarea class="input-field" id="messageInput" placeholder="Type your message to Claude Code..." rows="1"></textarea>
<div class="input-controls">
<div class="left-controls">
<button class="model-selector" id="modelSelector" onclick="showModelSelector()" title="Select model">
<span id="selectedModel">Opus</span>
<svg width="8" height="8" viewBox="0 0 8 8" fill="currentColor">
<path d="M1 2.5l3 3 3-3"></path>
</svg>
</button>
<button class="tools-btn" onclick="showMCPModal()" title="Configure MCP servers">
MCP
<svg width="8" height="8" viewBox="0 0 8 8" fill="currentColor">
<path d="M1 2.5l3 3 3-3"></path>
</svg>
</button>
<div class="connect-dropdown-wrapper">
<button class="input-dropdown-btn" id="connectBtn" onclick="toggleConnectMenu()">
<span>Add</span>
<svg width="8" height="8" viewBox="0 0 8 8" fill="currentColor"><path d="M1 2.5l3 3 3-3"></path></svg>
</button>
<div class="connect-menu" id="connectMenu" style="display: none;">
<div class="connect-menu-header">Add</div>
<button class="connect-menu-item" onclick="hideConnectMenu(); showPluginsModal();">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M21 16V8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73l7 4a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16z"/><polyline points="3.27 6.96 12 12.01 20.73 6.96"/><line x1="12" y1="22.08" x2="12" y2="12"/></svg>
<span>Plugins</span>
</button>
<button class="connect-menu-item" onclick="hideConnectMenu(); showSkillsModal();">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M13 2L3 14h9l-1 8 10-12h-9l1-8z"/></svg>
<span>Skills</span>
</button>
<button class="connect-menu-item" onclick="hideConnectMenu(); showMCPModal();">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="2" y="2" width="20" height="8" rx="2"/><rect x="2" y="14" width="20" height="8" rx="2"/><circle cx="6" cy="6" r="1" fill="currentColor"/><circle cx="6" cy="18" r="1" fill="currentColor"/></svg>
<span>MCP Servers</span>
</button>
</div>
</div>
<button class="input-toggle-btn" id="planToggleBtn" onclick="cyclePlanMode()">Plan</button>
<button class="input-toggle-btn" id="thinkToggleBtn" onclick="toggleThinkingMode()">Ultrathink</button>
</div>
<div class="right-controls">
<button class="slash-btn" onclick="showSlashCommandsModal()" title="Slash commands">/</button>
@@ -102,21 +124,19 @@ const getHtml = (isTelemetryEnabled: boolean) => `<!DOCTYPE html>
</svg>
</button>
<button class="send-btn" id="sendBtn" onclick="sendMessage()">
<div>
<span>Send </span>
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
width="11"
height="11"
>
<path
fill="currentColor"
d="M20 4v9a4 4 0 0 1-4 4H6.914l2.5 2.5L8 20.914L3.086 16L8 11.086L9.414 12.5l-2.5 2.5H16a2 2 0 0 0 2-2V4z"
></path>
<div>
<span>Send </span>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="11" height="11">
<path fill="currentColor" d="M20 4v9a4 4 0 0 1-4 4H6.914l2.5 2.5L8 20.914L3.086 16L8 11.086L9.414 12.5l-2.5 2.5H16a2 2 0 0 0 2-2V4z"></path>
</svg>
</div>
</button>
<button class="stop-inline-btn" id="stopInlineBtn" onclick="stopRequest()" style="display: none;">
<svg width="10" height="10" viewBox="0 0 24 24" fill="currentColor">
<path d="M6 6h12v12H6z"/>
</svg>
Stop
</button>
</div>
</div>
</div>
@@ -127,11 +147,9 @@ const getHtml = (isTelemetryEnabled: boolean) => `<!DOCTYPE html>
<div class="status ready" id="status">
<div class="status-indicator"></div>
<div class="status-text" id="statusText">Initializing...</div>
<button class="btn stop" id="stopBtn" onclick="stopRequest()" style="display: none;">
<svg width="10" height="10" viewBox="0 0 24 24" fill="currentColor">
<path d="M6 6h12v12H6z"/>
</svg>
Stop
<button class="support-btn" onclick="showSupportModal()" title="Send feedback">
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"/></svg>
Support
</button>
</div>
@@ -163,54 +181,15 @@ const getHtml = (isTelemetryEnabled: boolean) => `<!DOCTYPE html>
<div class="mcp-servers-list" id="mcpServersList">
<!-- MCP servers will be loaded here -->
</div>
<div class="mcp-add-server">
<button class="btn outlined" onclick="showAddServerForm()" id="addServerBtn">+ Add MCP Server</button>
</div>
<div class="mcp-popular-servers" id="popularServers">
<h4>Popular MCP Servers</h4>
<div class="popular-servers-grid">
<div class="popular-server-item" onclick="addPopularServer('context7', { type: 'http', url: 'https://context7.liam.sh/mcp' })">
<div class="popular-server-icon">📚</div>
<div class="popular-server-info">
<div class="popular-server-name">Context7</div>
<div class="popular-server-desc">Up-to-date Code Docs For Any Prompt</div>
</div>
</div>
<div class="popular-server-item" onclick="addPopularServer('sequential-thinking', { type: 'stdio', command: 'npx', args: ['-y', '@modelcontextprotocol/server-sequential-thinking'] })">
<div class="popular-server-icon">🔗</div>
<div class="popular-server-info">
<div class="popular-server-name">Sequential Thinking</div>
<div class="popular-server-desc">Step-by-step reasoning capabilities</div>
</div>
</div>
<div class="popular-server-item" onclick="addPopularServer('memory', { type: 'stdio', command: 'npx', args: ['-y', '@modelcontextprotocol/server-memory'] })">
<div class="popular-server-icon">🧠</div>
<div class="popular-server-info">
<div class="popular-server-name">Memory</div>
<div class="popular-server-desc">Knowledge graph storage</div>
</div>
</div>
<div class="popular-server-item" onclick="addPopularServer('puppeteer', { type: 'stdio', command: 'npx', args: ['-y', '@modelcontextprotocol/server-puppeteer'] })">
<div class="popular-server-icon">🎭</div>
<div class="popular-server-info">
<div class="popular-server-name">Puppeteer</div>
<div class="popular-server-desc">Browser automation</div>
</div>
</div>
<div class="popular-server-item" onclick="addPopularServer('fetch', { type: 'stdio', command: 'npx', args: ['-y', '@modelcontextprotocol/server-fetch'] })">
<div class="popular-server-icon">🌐</div>
<div class="popular-server-info">
<div class="popular-server-name">Fetch</div>
<div class="popular-server-desc">HTTP requests & web scraping</div>
</div>
</div>
<div class="popular-server-item" onclick="addPopularServer('filesystem', { type: 'stdio', command: 'npx', args: ['-y', '@modelcontextprotocol/server-filesystem'] })">
<div class="popular-server-icon">📁</div>
<div class="popular-server-info">
<div class="popular-server-name">Filesystem</div>
<div class="popular-server-desc">File operations & management</div>
</div>
</div>
<h4>Search MCP Servers</h4>
<div class="marketplace-search">
<input type="text" id="marketplaceSearch" placeholder="Search MCP servers..." oninput="filterMarketplace(this.value)" />
</div>
<div class="marketplace-grid" id="marketplaceGrid">
</div>
<div class="marketplace-load-more" id="marketplaceLoadMore" style="display: none;">
<button class="btn outlined" onclick="loadMoreMarketplace()">Load more</button>
</div>
</div>
<div class="mcp-add-form" id="addServerForm" style="display: none;">
@@ -218,6 +197,13 @@ const getHtml = (isTelemetryEnabled: boolean) => `<!DOCTYPE html>
<label for="serverName">Server Name:</label>
<input type="text" id="serverName" placeholder="my-server" required>
</div>
<div class="form-group">
<label for="serverScope">Install to:</label>
<select id="serverScope">
<option value="project">Project (.mcp.json)</option>
<option value="global">Global (~/.claude.json)</option>
</select>
</div>
<div class="form-group">
<label for="serverType">Server Type:</label>
<select id="serverType" onchange="updateServerForm()">
@@ -252,17 +238,56 @@ const getHtml = (isTelemetryEnabled: boolean) => `<!DOCTYPE html>
</div>
</div>
</div>
<div class="tools-list" id="mcpMarketplaceView" style="display: none;">
<div class="marketplace-search">
<input type="text" id="marketplaceSearch" placeholder="Search MCP servers..." oninput="filterMarketplace(this.value)" />
</div>
<div class="marketplace-grid" id="marketplaceGrid">
<div class="marketplace-loading">Loading servers...</div>
</div>
<div class="marketplace-load-more" id="marketplaceLoadMore" style="display: none;">
<button class="btn outlined" onclick="loadMoreMarketplace()">Load more</button>
</div>
</div>
</div>
</div>
<!-- Support modal -->
<div id="supportModal" class="tools-modal" style="display: none;">
<div class="tools-modal-content" style="max-width: 420px;">
<div class="tools-modal-header">
<h3>Send Feedback</h3>
<button class="tools-close-btn" onclick="hideSupportModal()">✕</button>
</div>
<div style="padding: 16px; display: flex; flex-direction: column; gap: 12px;">
<div>
<label style="font-size: 12px; color: var(--vscode-descriptionForeground); display: block; margin-bottom: 4px;">Type</label>
<select id="supportType" style="width: 100%; padding: 6px 8px; background: var(--vscode-input-background); color: var(--vscode-input-foreground); border: 1px solid var(--vscode-input-border); border-radius: 4px; font-size: 13px;">
<option value="bug">Bug Report</option>
<option value="feature">Feature Request</option>
</select>
</div>
<div>
<label style="font-size: 12px; color: var(--vscode-descriptionForeground); display: block; margin-bottom: 4px;">Email (optional)</label>
<input type="email" id="supportEmail" placeholder="your@email.com" style="width: 100%; padding: 6px 8px; background: var(--vscode-input-background); color: var(--vscode-input-foreground); border: 1px solid var(--vscode-input-border); border-radius: 4px; font-size: 13px; box-sizing: border-box;" />
</div>
<div>
<label style="font-size: 12px; color: var(--vscode-descriptionForeground); display: block; margin-bottom: 4px;">Message</label>
<textarea id="supportMessage" rows="5" placeholder="Describe the issue or suggestion..." style="width: 100%; padding: 6px 8px; background: var(--vscode-input-background); color: var(--vscode-input-foreground); border: 1px solid var(--vscode-input-border); border-radius: 4px; font-size: 13px; resize: vertical; box-sizing: border-box;"></textarea>
</div>
<button id="supportSubmitBtn" onclick="submitSupport()" style="padding: 8px 16px; background: var(--vscode-button-background); color: var(--vscode-button-foreground); border: none; border-radius: 4px; cursor: pointer; font-size: 13px;">Send</button>
</div>
</div>
</div>
<!-- Settings modal -->
<div id="settingsModal" class="tools-modal" style="display: none;">
<div class="tools-modal-content">
<div class="tools-modal-content" style="max-height: 600px;">
<div class="tools-modal-header">
<span>Claude Code Chat Settings</span>
<button class="tools-close-btn" onclick="hideSettingsModal()">✕</button>
</div>
<div class="tools-list">
<div class="tools-list" style="max-height: none;">
<h3 style="margin-top: 0; margin-bottom: 16px; font-size: 14px; font-weight: 600;">WSL Configuration</h3>
<div>
<p style="font-size: 11px; color: var(--vscode-descriptionForeground); margin: 0;">
@@ -347,54 +372,186 @@ const getHtml = (isTelemetryEnabled: boolean) => `<!DOCTYPE html>
</div>
</div>
<h3 style="margin-top: 24px; margin-bottom: 16px; font-size: 14px; font-weight: 600;">Customize Claude Command</h3>
<div>
<p style="font-size: 11px; color: var(--vscode-descriptionForeground); margin: 0 0 12px 0;">
Customize the Claude Code executable and environment.
</p>
<div id="opencreditsPromo" style="margin-bottom: 16px; padding: 14px 16px; border-radius: 8px; border: 1px solid var(--vscode-panel-border); background: rgba(139, 92, 246, 0.05);"></div>
</div>
<div class="settings-group">
<div style="margin-bottom: 16px;">
<label style="display: block; margin-bottom: 4px; font-size: 12px; color: var(--vscode-descriptionForeground);">Executable Path</label>
<input type="text" id="executable-path" class="file-search-input" style="width: 100%;" placeholder="claude (default)" onchange="updateSettings()">
<p style="font-size: 11px; color: var(--vscode-descriptionForeground); margin: 4px 0 0 0;">
Custom path to the Claude Code executable. Leave empty to use the default <code style="background: var(--vscode-textCodeBlock-background); padding: 2px 4px; border-radius: 3px;">claude</code> command.
</p>
</div>
<div>
<div style="display: flex; align-items: center; justify-content: space-between; margin-bottom: 8px;">
<label id="envsLabel" style="font-size: 12px; color: var(--vscode-descriptionForeground);">Environment Variables</label>
<button id="envsToggleBtn" style="display: none; font-size: 10px; padding: 2px 10px; border-radius: 4px; border: 1px solid var(--vscode-panel-border); background: none; color: var(--vscode-descriptionForeground); cursor: pointer;"></button>
</div>
<div id="env-variables-list" class="env-variables-list"></div>
<button class="permissions-show-add-btn" style="margin-top: 8px;" onclick="addEnvVariable()">+ Add Variable</button>
</div>
<div class="tool-item" style="margin-top: 16px; display: none;">
<input type="checkbox" id="use-router" onchange="updateSettings()">
<label for="use-router">Enable OpenAI → Anthropic Router</label>
</div>
<p style="display: none; font-size: 11px; color: var(--vscode-descriptionForeground); margin: 4px 0 0 24px;">
Enable this if your API provider uses OpenAI-compatible format. The router will convert requests/responses locally.
</p>
<div id="providerExclusionSection" class="tool-item" style="margin-top: 16px; display: none;">
<input type="checkbox" id="us-eu-providers-only" onchange="applyProviderExclusion()">
<label for="us-eu-providers-only">Only use US & EU providers</label>
</div>
<p id="providerExclusionHint" style="display: none; font-size: 11px; color: var(--vscode-descriptionForeground); margin: 4px 0 0 24px;">
When enabled, requests are routed only through US and EU-based infrastructure providers.
</p>
</div>
</div>
</div>
</div>
<!-- Model selector modal -->
<div id="modelModal" class="tools-modal" style="display: none;">
<div class="tools-modal-content" style="width: 400px;">
<div class="tools-modal-content model-modal-content">
<div class="tools-modal-header">
<span>Enforce Model</span>
<span>Select Model</span>
<button class="tools-close-btn" onclick="hideModelModal()">✕</button>
</div>
<div class="model-explanatory-text">
This overrides your default model setting for this conversation only.
<!-- Claude Code section -->
<div id="claudeCodeSection" class="model-section">
<div class="model-section-header">
<span class="model-section-title">CLAUDE CODE STANDARD MODELS</span>
</div>
<div class="claude-cards-container" id="claudeModelCards">
<div class="claude-card" data-model="opus" onclick="selectModel('opus')">
<div class="claude-card-name">Opus</div>
<div class="claude-card-desc">Most powerful, best for complex tasks</div>
</div>
<div class="claude-card" data-model="sonnet" onclick="selectModel('sonnet')">
<div class="claude-card-name">Sonnet</div>
<div class="claude-card-desc">Balanced performance and speed</div>
</div>
<div class="claude-card" data-model="default" onclick="selectModel('default')">
<div class="claude-card-name">Default</div>
<div class="claude-card-desc">Let Claude Code choose the best model</div>
</div>
</div>
</div>
<div class="tools-list">
<div class="tool-item" onclick="selectModel('opus')">
<input type="radio" name="model" id="model-opus" value="opus" checked>
<label for="model-opus">
<div class="model-title">Opus - Most capable model</div>
<div class="model-description">
Best for complex tasks and highest quality output
</div>
</label>
<!-- Divider (only shown when both sections visible) -->
<div id="modelSectionDivider" class="model-section-divider" style="display: none;"></div>
<!-- Other models section -->
<div id="opencreditsModelsSection" class="model-section opencredits-section" style="display: none;">
<div class="model-section-header">
<span class="model-section-title">TRY OTHER MODELS <span class="new-badge">NEW</span><span class="beta-badge" data-tooltip="This feature is in beta. Experience may vary across models.">BETA</span></span>
</div>
<div class="tool-item" onclick="selectModel('sonnet')">
<input type="radio" name="model" id="model-sonnet" value="sonnet">
<label for="model-sonnet">
<div class="model-title">Sonnet - Balanced model</div>
<div class="model-description">
Good balance of speed and capability
</div>
</label>
<div id="opencreditsComparisonHeader"></div>
<div class="model-cards-container" id="opencreditsModelCards">
<!-- Cards populated by JavaScript -->
</div>
<div class="tool-item" onclick="selectModel('default')">
<input type="radio" name="model" id="model-default" value="default">
<label for="model-default" class="default-model-layout">
<div class="model-option-content">
<div class="model-title">Default - User configured</div>
<div class="model-description">
Uses the model configured in your settings
</div>
</div>
<button class="secondary-button configure-button" onclick="event.stopPropagation(); openModelTerminal();">
Configure
</button>
</label>
<div style="margin: 14px 0 0 0; padding: 8px 10px; border-radius: 6px; background: var(--vscode-textBlockQuote-background, rgba(127,127,127,0.1)); display: flex; gap: 6px; align-items: flex-start;">
<span style="font-size: 10px; line-height: 1.4; flex-shrink: 0; opacity: 0.4;">&#9432;</span>
<span style="font-size: 10px; color: var(--vscode-descriptionForeground); line-height: 1.4;">Savings are compared to using Claude Opus directly with Anthropic API. Models can be configured to use only US &amp; EU providers.</span>
</div>
</div>
</div>
</div>
<!-- All Models Browser modal -->
<div id="allModelsModal" class="tools-modal" style="display: none;">
<div class="tools-modal-content" style="width: 500px; max-width: 95vw; max-height: 80vh;">
<div class="tools-modal-header">
<span>Browse All Models</span>
<button class="tools-close-btn" onclick="hideAllModelsModal()">✕</button>
</div>
<div class="all-models-search">
<input type="text" id="allModelsSearch" placeholder="Search models..." oninput="filterAllModels()">
</div>
<div class="all-models-list" id="allModelsList">
<!-- Models populated by JavaScript -->
</div>
</div>
</div>
<!-- Advanced Settings modal (OpenCredits) -->
<div id="advancedModal" class="tools-modal" style="display: none;">
<div class="tools-modal-content" style="width: 440px; max-width: 90vw; max-height: 80vh; overflow-y: auto; overflow-x: hidden;">
<div class="tools-modal-header">
<span>Advanced Settings</span>
<button class="tools-close-btn" onclick="hideAdvancedModal()">✕</button>
</div>
<div style="padding: 16px;">
<p style="font-size: 12px; color: var(--vscode-descriptionForeground); margin-bottom: 16px;">
Override the default models used when you select Opus, Sonnet, or Haiku.
</p>
<div class="custom-provider-field">
<label>Sonnet Model</label>
<div class="model-combo" id="comboSonnet">
<input type="text" class="model-combo-input" placeholder="Default — click to search models" autocomplete="off">
<div class="model-combo-dropdown"></div>
</div>
</div>
<div class="custom-provider-field">
<label>Opus Model</label>
<div class="model-combo" id="comboOpus">
<input type="text" class="model-combo-input" placeholder="Default — click to search models" autocomplete="off">
<div class="model-combo-dropdown"></div>
</div>
</div>
<div class="custom-provider-field">
<label>Haiku Model</label>
<div class="model-combo" id="comboHaiku">
<input type="text" class="model-combo-input" placeholder="Default — click to search models" autocomplete="off">
<div class="model-combo-dropdown"></div>
</div>
</div>
<button class="install-btn" style="width: 100%; margin-top: 16px;" onclick="saveAdvancedSettings()">Save</button>
</div>
</div>
</div>
<!-- Custom Provider modal -->
<div id="customProviderModal" class="tools-modal" style="display: none;">
<div class="tools-modal-content" style="width: 440px;">
<div class="tools-modal-header">
<span>Custom Provider</span>
<button class="tools-close-btn" onclick="hideCustomProviderModal()">✕</button>
</div>
<div style="padding: 16px;">
<p style="font-size: 12px; color: var(--vscode-descriptionForeground); margin-bottom: 16px;">
Connect to any OpenAI-compatible or Anthropic-compatible API endpoint.
</p>
<div class="custom-provider-field">
<label>Base URL</label>
<input type="text" id="customProviderBaseUrl" placeholder="https://api.example.com">
</div>
<div class="custom-provider-field">
<label>Auth Token</label>
<input type="password" id="customProviderAuthToken" placeholder="sk-...">
</div>
<div class="custom-provider-field">
<label>Sonnet Model <span style="opacity:0.5">(optional)</span></label>
<input type="text" id="customProviderSonnet" placeholder="claude-sonnet-4-20250514">
</div>
<div class="custom-provider-field">
<label>Opus Model <span style="opacity:0.5">(optional)</span></label>
<input type="text" id="customProviderOpus" placeholder="claude-opus-4-20250514">
</div>
<div class="custom-provider-field">
<label>Haiku Model <span style="opacity:0.5">(optional)</span></label>
<input type="text" id="customProviderHaiku" placeholder="claude-haiku-4-20250514">
</div>
<button class="install-btn" style="width: 100%; margin-top: 16px;" onclick="saveCustomProvider()">Save & Connect</button>
</div>
</div>
</div>
@@ -443,9 +600,134 @@ const getHtml = (isTelemetryEnabled: boolean) => `<!DOCTYPE html>
<polyline points="20 6 9 17 4 12"></polyline>
</svg>
</div>
<p class="install-success-text">Installation Complete</p>
<p class="install-success-hint">Send a message to get started</p>
<p class="install-success-text">Installed!</p>
<p class="install-success-hint">How would you like to use Claude Code?</p>
<div class="install-options">
<button class="install-option" onclick="loginWithPlan()">
<span class="install-option-title">I have a plan</span>
<span class="install-option-desc">Login with Anthropic · Pro, Max, or API key</span>
</button>
<button class="install-option install-option-secondary" onclick="showFundsSelection()">
<span class="install-option-title">Just try it</span>
<span class="install-option-desc">No account needed · Pay as you go with OpenCredits</span>
</button>
</div>
</div>
<div class="install-funds" id="installFunds" style="display: none;">
<p class="install-funds-title">Add funds to get started</p>
<p class="install-funds-hint">Pay as you go - no subscription required</p>
<div class="install-amounts">
<button class="install-amount" onclick="selectFundsAmount(5)">$5</button>
<button class="install-amount" onclick="selectFundsAmount(10)">$10</button>
<button class="install-amount" onclick="selectFundsAmount(25)">$25</button>
<button class="install-amount" onclick="selectFundsAmount(50)">$50</button>
<button class="install-amount" onclick="selectFundsAmount(100)">$100</button>
</div>
<div class="install-custom-amount">
<span class="install-custom-currency">$</span>
<input type="number" id="customAmountInput" class="install-custom-input" placeholder="Other" min="1" max="500" />
<button class="install-custom-btn" onclick="selectCustomAmount()">Add</button>
</div>
<div class="install-powered-by">
Powered by <a href="${opencreditsWebUrl}" target="_blank">OpenCredits</a>
</div>
<p style="font-size: 10px; color: var(--vscode-descriptionForeground); margin: 8px 0 0; opacity: 0.7;">By continuing, you agree to OpenCredits' <a href="#" onclick="event.preventDefault(); vscode.postMessage({ type: 'openExternalUrl', url: '${opencreditsWebUrl}/legal/terms-of-service' });" style="color: var(--vscode-textLink-foreground);">Terms of Service</a> and <a href="#" onclick="event.preventDefault(); vscode.postMessage({ type: 'openExternalUrl', url: '${opencreditsWebUrl}/legal/privacy-policy' });" style="color: var(--vscode-textLink-foreground);">Privacy Policy</a>.</p>
<button class="install-back-btn" onclick="showInstallOptions()">
← Back
</button>
</div>
<div class="install-checkout" id="installCheckout" style="display: none;">
<div id="checkoutPreparing" style="text-align: center;">
<div class="install-spinner" style="margin: 0 auto 16px;"></div>
<p class="install-funds-title">Preparing checkout...</p>
<p class="install-funds-hint">Please wait while we set up your payment</p>
</div>
<div id="checkoutReady" style="display: none; text-align: center;">
<p class="install-funds-title">Checkout opened in your browser</p>
<p class="install-funds-hint">Complete your payment, then come back here.</p>
<div id="checkoutUrlBox" style="display: flex; align-items: center; gap: 6px; margin: 12px 0; padding: 8px 12px; background: var(--vscode-textBlockQuote-background, rgba(255,255,255,0.05)); border-radius: 6px; border: 1px solid var(--vscode-panel-border); overflow: hidden; min-width: 0; max-width: 100%;">
<span id="checkoutUrlDisplay" style="flex: 1; min-width: 0; font-size: 11px; color: var(--vscode-descriptionForeground); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; display: block;"></span>
<button id="checkoutUrlCopyBtn" title="Copy URL" style="flex-shrink: 0; background: none; border: none; color: var(--vscode-foreground); cursor: pointer; padding: 2px; opacity: 0.7;">
<svg width="14" height="14" viewBox="0 0 24 24" fill="currentColor"><path d="M16 1H4c-1.1 0-2 .9-2 2v14h2V3h12V1zm3 4H8c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h11c1.1 0 2-.9 2-2V7c0-1.1-.9-2-2-2zm0 16H8V7h11v14z"/></svg>
</button>
</div>
<button id="checkoutOpenBtn" class="install-btn" style="margin-top: 8px;">Open Checkout Again</button>
</div>
<div id="checkoutComplete" style="display: none; text-align: center;">
<div class="install-success-icon" style="margin: 0 auto 16px;">
<svg class="install-check" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5">
<polyline points="20 6 9 17 4 12"></polyline>
</svg>
</div>
<p class="install-funds-title">Payment successful!</p>
<p class="install-funds-hint">Your account has been funded.</p>
<button class="install-btn" style="margin-top: 12px;" onclick="hideInstallModal()">Close</button>
</div>
<div id="checkoutError" style="display: none; text-align: center;">
<div style="width: 40px; height: 40px; margin: 0 auto 12px; border-radius: 50%; background: rgba(239, 68, 68, 0.15); display: flex; align-items: center; justify-content: center;">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="#ef4444" stroke-width="2.5">
<line x1="18" y1="6" x2="6" y2="18"></line>
<line x1="6" y1="6" x2="18" y2="18"></line>
</svg>
</div>
<p class="install-funds-title">Something went wrong</p>
<p class="install-funds-hint" id="checkoutErrorMsg">Could not complete checkout. Please try again.</p>
<button id="checkoutRetryBtn" class="install-btn" style="margin-top: 12px;">Try Again</button>
</div>
<p style="font-size: 10px; color: var(--vscode-descriptionForeground); margin: 16px 0 0; opacity: 0.7;">By continuing, you agree to OpenCredits' <a href="#" onclick="event.preventDefault(); vscode.postMessage({ type: 'openExternalUrl', url: 'https://opencredits.ai/terms' });" style="color: var(--vscode-textLink-foreground);">Terms of Service</a> and <a href="#" onclick="event.preventDefault(); vscode.postMessage({ type: 'openExternalUrl', url: 'https://opencredits.ai/privacy' });" style="color: var(--vscode-textLink-foreground);">Privacy Policy</a>.</p>
</div>
</div>
</div>
</div>
<!-- Provider choice modal -->
<div id="providerChoiceModal" class="tools-modal" style="display: none;">
<div class="tools-modal-content" style="width: 300px;">
<div class="tools-modal-header">
<span id="providerChoiceTitle">Use model via</span>
<button class="tools-close-btn" onclick="document.getElementById('providerChoiceModal').style.display='none'">✕</button>
</div>
<div style="padding: 16px; display: flex; flex-direction: column; gap: 10px;">
<button id="providerChoiceOpenCredits" style="padding: 12px 16px; border-radius: 8px; border: 1px solid var(--vscode-panel-border); background: var(--vscode-editor-background); color: var(--vscode-foreground); cursor: pointer; text-align: left;">
<div style="font-weight: 600; font-size: 13px;">OpenCredits</div>
<div style="font-size: 11px; color: var(--vscode-descriptionForeground); margin-top: 2px;">Pay as you go with your OpenCredits balance</div>
</button>
<button id="providerChoiceAnthropic" style="padding: 12px 16px; border-radius: 8px; border: 1px solid var(--vscode-panel-border); background: var(--vscode-editor-background); color: var(--vscode-foreground); cursor: pointer; text-align: left;">
<div style="font-weight: 600; font-size: 13px;">Anthropic</div>
<div style="font-size: 11px; color: var(--vscode-descriptionForeground); margin-top: 2px;">Use your Anthropic API key or subscription</div>
</button>
</div>
</div>
</div>
<!-- External URL opened modal -->
<div id="externalUrlModal" class="tools-modal" style="display: none;">
<div class="tools-modal-content" style="width: 380px;">
<div class="tools-modal-header">
<span>Opening Browser</span>
<button class="tools-close-btn" onclick="document.getElementById('externalUrlModal').style.display='none'">✕</button>
</div>
<div style="padding: 24px; text-align: center;">
<p style="margin: 0 0 16px; font-size: 13px; color: var(--vscode-foreground);">A page should have opened in your browser.</p>
<div style="display: flex; align-items: center; gap: 6px; margin: 0 0 20px; padding: 8px 12px; background: var(--vscode-textBlockQuote-background, rgba(255,255,255,0.05)); border-radius: 6px; border: 1px solid var(--vscode-panel-border);">
<span id="externalUrlDisplay" style="flex: 1; font-size: 11px; color: var(--vscode-descriptionForeground); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; text-align: left;"></span>
<button id="externalUrlCopyBtn" title="Copy URL" style="flex-shrink: 0; background: none; border: none; color: var(--vscode-foreground); cursor: pointer; padding: 2px; opacity: 0.7;">
<svg width="14" height="14" viewBox="0 0 24 24" fill="currentColor"><path d="M16 1H4c-1.1 0-2 .9-2 2v14h2V3h12V1zm3 4H8c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h11c1.1 0 2-.9 2-2V7c0-1.1-.9-2-2-2zm0 16H8V7h11v14z"/></svg>
</button>
</div>
<p style="margin: 0 0 16px; font-size: 12px; color: var(--vscode-descriptionForeground);">If it didn't open, click the button below.</p>
<button id="externalUrlFallbackBtn" style="padding: 8px 20px; background: var(--vscode-button-background); color: var(--vscode-button-foreground); border: none; border-radius: 6px; cursor: pointer; font-size: 12px;">Open in Browser</button>
</div>
</div>
</div>
@@ -477,6 +759,9 @@ const getHtml = (isTelemetryEnabled: boolean) => `<!DOCTYPE html>
</div>
</div>
${getSkillsHtml()}
${getPluginsHtml()}
<!-- Slash commands modal -->
<div id="slashCommandsModal" class="tools-modal" style="display: none;">
<div class="tools-modal-content">
@@ -779,18 +1064,19 @@ const getHtml = (isTelemetryEnabled: boolean) => `<!DOCTYPE html>
</div>
</div>
${getScript(isTelemetryEnabled)}
<script>window.__recommendedModels = ${JSON.stringify(recommendedModels)};window.__topMcpServers = ${JSON.stringify(topMcpServers)};window.__topSkills = ${JSON.stringify(topSkills)};window.__topPlugins = ${JSON.stringify(topPlugins)};</script>
${getScript(isTelemetryEnabled, opencreditsApiUrl, opencreditsWebUrl, opencreditsPublishableKey)}
<!--
<!--
Analytics FAQ:
1. Is Umami GDPR compliant?
Yes, Umami does not collect any personally identifiable information and anonymizes all data collected. Users cannot be identified and are never tracked across websites.
2. Do I need to display a cookie notice to users?
No, Umami does not use any cookies in the tracking code.
-->
${isTelemetryEnabled ? '<script defer src="https://cloud.umami.is/script.js" data-website-id="d050ac9b-2b6d-4c67-b4c6-766432f95644"></script>' : '<!-- Umami analytics disabled due to VS Code telemetry settings -->'}
${isTelemetryEnabled ? '<script defer src="https://umami.claudecodechat.com/script.js" data-website-id="6310c878-cfe4-4044-b4ef-a60cd0e0dfe4"></script>' : '<!-- Umami analytics disabled due to VS Code telemetry settings -->'}
</body>
</html>`;

View File

@@ -6,8 +6,10 @@
"lib": [
"ES2022"
],
"types": ["node", "mocha"],
"sourceMap": true,
"rootDir": "src",
"resolveJsonModule": true,
"strict": true, /* enable all strict type-checking options */
/* Additional Checks */
// "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */
@@ -16,6 +18,7 @@
},
"exclude": [
"mcp-permissions.js",
"claude-code-chat-permissions-mcp"
"claude-code-chat-permissions-mcp",
"backup-files"
]
}