mirror of
https://github.com/andrepimenta/claude-code-chat.git
synced 2026-05-30 00:05:44 +08:00
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:
5
.claude/settings.json
Normal file
5
.claude/settings.json
Normal file
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"enabledPlugins": {
|
||||
"frontend-design@claude-plugins-official": true
|
||||
}
|
||||
}
|
||||
@@ -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
3
.gitignore
vendored
@@ -3,4 +3,5 @@ dist
|
||||
node_modules
|
||||
.vscode-test/
|
||||
*.vsix
|
||||
backup
|
||||
backup
|
||||
backup-files
|
||||
|
||||
@@ -14,4 +14,5 @@ backup
|
||||
claude-code-chat-permissions-mcp/**
|
||||
node_modules
|
||||
mcp-permissions.js
|
||||
build
|
||||
backup-files
|
||||
build
|
||||
|
||||
21
backup.sh
Executable file
21
backup.sh
Executable 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"
|
||||
22
package.json
22
package.json
@@ -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."
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
1547
src/extension.ts
1547
src/extension.ts
File diff suppressed because it is too large
Load Diff
148
src/model-updater.ts
Normal file
148
src/model-updater.ts
Normal 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
150
src/plugins-script.ts
Normal 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">✓</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, ''');
|
||||
|
||||
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">✓ Anthropic verified</span>' : '';
|
||||
|
||||
var grid = document.getElementById('pluginsGrid');
|
||||
pluginsDisplayedList = grid.innerHTML;
|
||||
|
||||
grid.innerHTML = '<div class="marketplace-detail">' +
|
||||
'<button class="marketplace-back-btn" onclick="backToPluginsList()">← 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
26
src/plugins-ui.ts
Normal 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;
|
||||
66
src/recommended-models.json
Normal file
66
src/recommended-models.json
Normal 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
265
src/router/formatRequest.ts
Normal 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;
|
||||
}
|
||||
37
src/router/formatResponse.ts
Normal file
37
src/router/formatResponse.ts
Normal 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
2
src/router/index.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
export { startRouter, stopRouter, isRouterRunning, getRouterPort, setBaseUrl } from './server';
|
||||
export { setModelConfig } from './formatRequest';
|
||||
220
src/router/server.ts
Normal file
220
src/router/server.ts
Normal 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;
|
||||
}
|
||||
219
src/router/streamResponse.ts
Normal file
219
src/router/streamResponse.ts
Normal 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();
|
||||
},
|
||||
});
|
||||
}
|
||||
2176
src/script.ts
2176
src/script.ts
File diff suppressed because it is too large
Load Diff
286
src/skills-script.ts
Normal file
286
src/skills-script.ts
Normal 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, ''');
|
||||
|
||||
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, ''');
|
||||
|
||||
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()">← 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
51
src/skills-ui.ts
Normal 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
479
src/top-mcp-servers.json
Normal 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
240
src/top-plugins.json
Normal 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
289
src/top-skills.json
Normal 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"
|
||||
}
|
||||
]
|
||||
1823
src/ui-styles.ts
1823
src/ui-styles.ts
File diff suppressed because it is too large
Load Diff
544
src/ui.ts
544
src/ui.ts
@@ -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;">ⓘ</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 & 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>`;
|
||||
|
||||
|
||||
@@ -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"
|
||||
]
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user