14 Commits

Author SHA1 Message Date
andrepimenta
474910a330 Bump version to 2.0.4
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-17 23:11:33 +01:00
andrepimenta
58de99030c Improve plan mode display and permission prompt
- Render ExitPlanMode as formatted markdown plan with proper headings,
  lists, and code blocks instead of raw key-value dump
- Show allowed prompts as clickable action buttons below the plan
- Use 📋 Plan header instead of 🔧 ExitPlanMode
- Permission prompt says "Approve the plan above?" with Approve button
- Hide "Always allow" for plan approvals
- Full height plan content without scroll constraint

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-14 21:18:52 +01:00
andrepimenta
6d112012b2 Add analytics tracking to MCP, skills, plugins, and checkout
Track modal opens, installs, removals with properties for MCP
servers, skills, and plugins. Add checkout completed event and
support attempted event. Include error details in install failures.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-12 22:37:41 +01:00
andrepimenta
e31c5357d2 Bump version to 2.0.2
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-12 15:11:14 +01:00
andrepimenta
b3ff6e9c03 Update README for v2.0 with new features
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-12 15:09:58 +01:00
andrepimenta
05a15d19a6 Add editor name tag to Umami analytics
Pass vscode.env.appName (VS Code, Cursor, etc.) as data-tag
on the Umami script to segment analytics by editor.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-12 14:55:34 +01:00
andrepimenta
f5d4c7851b Gitignore .claude/settings.local.json
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-12 14:41:30 +01:00
andrepimenta
ef0b3c1b4e Update Umami to product.opencredits.ai and track checkout start
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-12 14:35:17 +01:00
andrepimenta
deca7de8d5 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>
2026-04-11 23:54:13 +01:00
andrepimenta
b527b6f4c9 Add Open VSIX build script
Build script that creates Open VSIX variant with different branding:
- Changes displayName to "Claude Code Chat"
- Uses icon.png instead of icon-bubble.png
- Automatically backs up and restores files after build

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-06 21:44:03 +00:00
andrepimenta
2f792e7158 Bump version to 1.1.0
- Update changelog with all new features since 1.0.7
- Add Inline Diff Viewer section to README
- Update package.json version

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-12-06 12:56:15 +00:00
andrepimenta
97920395d1 Add WSL support for usage terminal commands
Run ccusage commands through bash -ic in WSL to properly load shell environment.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-12-06 12:55:44 +00:00
andrepimenta
2e640fa20a Use --permission-mode plan flag for plan mode
Replace message prepending with native CLI flag for cleaner implementation.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-12-06 12:32:25 +00:00
andrepimenta
5136985474 Add Umami analytics events to install flow
Track user journey through installation:
- Install modal shown
- Install started
- Install success/failed

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-12-06 11:19:23 +00:00
29 changed files with 8279 additions and 706 deletions

5
.claude/settings.json Normal file
View File

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

View File

@@ -1,13 +0,0 @@
{
"permissions": {
"allow": [
"Bash(npm run compile:*)",
"Bash(grep:*)",
"Bash(sed:*)",
"Bash(rg:*)",
"Bash(npx tsc:*)"
],
"deny": []
},
"enableAllProjectMcpServers": false
}

4
.gitignore vendored
View File

@@ -3,4 +3,6 @@ dist
node_modules
.vscode-test/
*.vsix
backup
backup
backup-files
.claude/settings.local.json

View File

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

View File

@@ -4,6 +4,38 @@ All notable changes to the "claude-code-chat" extension will be documented in th
Check [Keep a Changelog](http://keepachangelog.com/) for recommendations on how to structure this file.
## [1.1.0] - 2025-12-06
### 🚀 Features Added
- **Install Modal**: Added installation flow for users without Claude Code CLI
- Auto-detects when Claude Code is not installed
- One-click installation with progress indicator
- Platform-specific installation commands
- **Diff Viewer Improvements**:
- Show full diff in Edit, MultiEdit, and Write tool use messages
- Add "Open Diff" button to open VS Code's native side-by-side diff editor
- Add truncation with expand button for long diffs
- Optimize diff storage and improve Open Diff button behavior
- **Processing Indicator**: New morphing orange dot animation while Claude is working
- **Subscription Detection**: Added usage badge to status bar showing plan type (Pro, Max) or API cost
- **Conversation Compacting**: Handle `/compact` command in chat with status messages and token reset
- **Permission System**: Migrated from MCP file-based to stdio-based permission prompts
- **Plan Mode**: Now uses native `--permission-mode plan` CLI flag for cleaner implementation
### 🐛 Bug Fixes
- Fixed diff line alignment by removing ::before pseudo-elements
- Fixed auto-scroll for diff tool results
- Strip tool_use_error tags from error messages
- Improved process termination handling
### 🔧 Technical Improvements
- Run /compact command in chat instead of spawning terminal
- Improved terminal and UI experience
- Updated diff icon colors
### 📊 Analytics
- Added Umami analytics events to track install flow (modal shown, started, success/failed)
## [1.0.7] - 2025-10-01
### 🚀 Features Added

View File

@@ -15,13 +15,12 @@ Ditch the command line and experience Claude Code like never before. This extens
## ✨ **Why Choose Claude Code Chat?**
🖥️ **No Terminal Required** - Beautiful chat interface replaces command-line interactions
**Restore Checkpoints** - Undo changes and restore code to any previous state
🔌 **MCP Server Support** - Complete Model Context Protocol server management
**Restore Checkpoints** - Undo changes and restore code to any previous state
🔌 **MCP, Skills & Plugins** - Browse, search, and install from curated marketplaces
💾 **Conversation History** - Automatic conversation history and session management
🎨 **VS Code Native** - Claude Code integrated directly into VS Code with native theming and sidebar support
🧠 **Plan and Thinking modes** - Plan First and configurable Thinking modes for better results
**Smart File/Image Context and Custom Commands** - Reference any file, paste images or screenshots and create custom commands
🤖 **Model Selection** - Choose between Opus, Sonnet, or Default based on your needs
🎨 **VS Code & Cursor** - Works in VS Code, Cursor, and other compatible editors
🧠 **Plan and Ultrathink modes** - Plan First and Ultrathink modes
**Smart Context** - Reference files, paste images, and create custom commands
🐧 **Windows/WSL Support** - Full native Windows and WSL support
![Claude Code Chat 1 0 0](https://github.com/user-attachments/assets/5954a74c-eff7-4205-8482-6a1c9de6e102)
@@ -47,28 +46,36 @@ Ditch the command line and experience Claude Code like never before. This extens
- Real-time cost and token tracking
- Session statistics and performance metrics
### 🔌 **MCP Server Management** ⭐ **NEW IN V1.0**
- **Popular Servers Gallery** - One-click installation of common MCP servers
- **Custom Server Creation** - Build and configure your own MCP servers
- **Server Management** - Edit, delete, enable/disable servers through UI
- **Automatic Integration** - Seamless permissions and tool integration
- **Cross-platform Support** - Full WSL compatibility with path conversion
### 📝 **Inline Diff Viewer**
- **Full Diff Display** - See complete file changes directly in Edit, MultiEdit, and Write messages
- **Open in VS Code Diff** - One-click button to open VS Code's native side-by-side diff editor
- **Smart Truncation** - Long diffs are truncated with an expand button for better readability
- **Syntax Highlighting** - Proper code highlighting in diff views
- **Visual Change Indicators** - Clear green/red highlighting for additions and deletions
### 🔒 **Advanced Permissions System** ⭐ **NEW IN V1.0**
### 🔌 **MCP, Skills & Plugins Marketplace** ⭐ **NEW IN V2.0**
- **MCP Servers** - Browse 30+ featured servers (GitHub, Slack, Stripe, Notion, etc.) with dual registry search
- **Skills** - Browse and install curated skills from skills.sh with project or global scope
- **Plugins** - Browse and install plugins to extend Claude Code
- **Smart Search** - Search across add-mcp curated and official Anthropic registries with relevance ranking
- **Project or Global Install** - Install MCP servers to `.mcp.json` or `~/.claude.json`, skills to `.claude/skills/`
- **One-Click Install** - Pre-filled configuration with env vars, headers, and OAuth authentication
### 🖼️ **Image Preview & Attachments** ⭐ **NEW IN V2.0**
- **Paste Images** - Paste images with thumbnail preview before sending
- **File Picker** - Select images through VS Code's native file picker
- **Preview Strip** - See attached images above the text box with remove buttons
- **Inline Detection** - Image paths in messages are automatically detected and sent as base64
- **Multiple Images** - Attach multiple images to a single message
### 🔒 **Advanced Permissions System**
- **Interactive Permission Dialogs** - Detailed tool information with command previews
- **Always Allow Functionality** - Smart command pattern matching for common tools (npm, git, docker)
- **YOLO Mode** - Skip all permission checks for power users
- **Workspace Permissions** - Granular control over what tools can execute
- **Real-time Permission Management** - Add/remove permissions through intuitive UI
### 🖼️ **Image & Clipboard Support** ⭐ **NEW IN V1.0**
- **Drag & Drop Images** - Simply drag images directly into the chat
- **Clipboard Paste** - Press Ctrl+V to paste screenshots and copied images
- **Multiple Image Selection** - Choose multiple images through VS Code's file picker
- **Organized Storage** - Automatic organization in `.claude/claude-code-chat-images/`
- **Format Support** - PNG, JPG, JPEG, GIF, SVG, WebP, BMP formats
### 📱 **Sidebar Integration** ⭐ **NEW IN V1.0**
### 📱 **Sidebar Integration**
- **Native VS Code Sidebar** - Full chat functionality in the sidebar panel
- **Smart Panel Management** - Automatic switching between main and sidebar views
- **Persistent Sessions** - State maintained across panel switches
@@ -76,30 +83,30 @@ Ditch the command line and experience Claude Code like never before. This extens
### 📁 **Smart File Integration**
- Type `@` to instantly search and reference workspace files
- Image attachments via file browser and copy-paste screeshots
- Image attachments via file browser and copy-paste screenshots
- Lightning-fast file search across your entire project
- Seamless context preservation for multi-file discussions
### 🛠️ **Tool Management**
- Visual dashboard showing all available Claude Code tools
- Real-time tool execution with formatted results
- Process control - start, stop, and monitor operations
- Inline stop button replaces send during processing
- Smart permission system for secure tool execution
### 🎨 **VS Code Integration**
- Native theming that matches your editor
- Status bar integration with connection status
- Status bar with support button
- Activity bar panel for quick access
- Responsive design for any screen size
### 🤖 **Model Selection**
- **Quick Buttons** - GPT, Gemini, MiniMax, Kimi, GLM, DeepSeek above the text box
- **Opus** - Most capable model for complex tasks requiring deep reasoning
- **Sonnet** - Balanced model offering great performance for most use cases
- **Default** - Uses your configured model setting
- Model preference persists across sessions and is saved automatically
- Easy switching via dropdown selector in the chat interface
- Visual confirmation when switching between models
- One-click model configuration through integrated terminal
- **150+ OpenCredits Models** - Browse and switch to any available model
- Model preference persists across sessions
- Provider choice (OpenCredits vs Anthropic) for Claude models
### ⚡ **Slash Commands Integration**
- **Slash Commands Modal** - Type "/" to access all Claude Code commands instantly
@@ -115,14 +122,20 @@ Ditch the command line and experience Claude Code like never before. This extens
- **Intelligent Prompting** - Different prompts based on selected thinking intensity
- **Token Awareness** - Higher thinking levels consume more tokens but provide deeper reasoning
### 💬 **Support & Feedback** ⭐ **NEW IN V2.0**
- **In-App Support** - Click "Support" in the status bar to send feedback
- **Bug Reports & Feature Requests** - Submit directly from the extension
- **Optional Email** - Include your email for follow-up
---
## 🚀 **Getting Started**
### Prerequisites
- **VS Code 1.80+** - Latest version recommended
- **VS Code 1.80+** or **Cursor** - Latest version recommended
- **Claude Code CLI** - [Install from Anthropic](https://claude.ai/code)
- **Active Claude API or subscription** - API or Pro/Max plan
- **Node.js 18+** - Required for installation
### Installation
@@ -244,25 +257,26 @@ Example configuration in `settings.json`:
- Type `@` followed by your search term to quickly reference files
- Use `@src/` to narrow down to specific directories
- Reference multiple files in one message for cross-file analysis
- **NEW**: Copy-paste images directly into chat for visual context
- **NEW**: Paste screenshots with Ctrl+V for instant visual communication
- Paste images directly with preview thumbnails before sending
- Paste screenshots with Ctrl+V for instant visual communication
### ⚡ **Productivity Boosters**
- **Creates checkpoints automatically** before changes for safe experimentation
- **Restore instantly** if changes don't work out as expected
- **NEW**: Permission system prevents accidental tool execution
- **NEW**: YOLO mode for power users who want speed over safety
- Use the stop button to cancel long-running operations
- Permission system prevents accidental tool execution
- YOLO mode for power users who want speed over safety
- Inline stop button to cancel long-running operations
- Copy message contents to reuse Claude's responses
- Open history panel to reference previous conversations
- **NEW**: Sidebar integration for multi-panel workflow
- Sidebar integration for multi-panel workflow
- **Plan mode** and **Ultrathink** toggles above the text box
### 🎨 **Interface Customization**
- The UI automatically adapts to your VS Code theme
- Messages are color-coded: Green for you, Blue for Claude
- Hover over messages to reveal the copy button
- **NEW**: Enhanced code block rendering with syntax highlighting
- **NEW**: Copy-to-clipboard functionality for code blocks
- Enhanced code block rendering with syntax highlighting
- Copy-to-clipboard functionality for code blocks
---
@@ -334,6 +348,7 @@ See the [LICENSE](LICENSE) file for details.
Need help? We've got you covered:
- 💬 **In-App** - Click "Support" in the status bar to send feedback directly
- 🐛 **Issues**: [GitHub Issues](https://github.com/andrepimenta/claude-code-chat/issues)
---

21
backup.sh Executable file
View File

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

71
build/open-vsix/build.sh Executable file
View File

@@ -0,0 +1,71 @@
#!/bin/bash
# Build script for Open VSIX version
# This applies Open VSIX-specific changes, builds the package, then reverts
set -e
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
VERSION="2.0.4"
OUTPUT_NAME="vsix-claude-code-chat-${VERSION}.vsix"
echo "Building Open VSIX version ${VERSION}..."
# Backup original files to build folder
cp package.json "${SCRIPT_DIR}/package.json.backup"
cp src/extension.ts "${SCRIPT_DIR}/extension.ts.backup"
# Backup original icon.png if it exists
if [ -f "icon.png" ]; then
mv icon.png "${SCRIPT_DIR}/icon.png.backup"
fi
# Copy Open VSIX icon
cp "${SCRIPT_DIR}/icon.png" icon.png
echo "Copied Open VSIX icon"
# Temporarily remove icon-bubble.png (not needed for Open VSIX)
if [ -f "icon-bubble.png" ]; then
mv icon-bubble.png "${SCRIPT_DIR}/icon-bubble.png.backup"
fi
# Apply Open VSIX changes to package.json
sed -i.bak 's/"displayName": "Chat for Claude Code"/"displayName": "Claude Code Chat"/' package.json
sed -i.bak 's/"icon": "icon-bubble.png"/"icon": "icon.png"/g' package.json
rm -f package.json.bak
# Apply Open VSIX changes to extension.ts
sed -i.bak "s/icon-bubble.png/icon.png/g" src/extension.ts
rm -f src/extension.ts.bak
echo "Applied Open VSIX changes to package.json and extension.ts"
# Compile TypeScript
echo "Compiling TypeScript..."
npm run compile
# Build the VSIX
echo "Building VSIX package..."
vsce package --out "${OUTPUT_NAME}"
# Restore original files from build folder
mv "${SCRIPT_DIR}/package.json.backup" package.json
mv "${SCRIPT_DIR}/extension.ts.backup" src/extension.ts
# Restore original icon
rm -f icon.png
if [ -f "${SCRIPT_DIR}/icon.png.backup" ]; then
mv "${SCRIPT_DIR}/icon.png.backup" icon.png
fi
# Restore icon-bubble.png
if [ -f "${SCRIPT_DIR}/icon-bubble.png.backup" ]; then
mv "${SCRIPT_DIR}/icon-bubble.png.backup" icon-bubble.png
fi
# Recompile with original extension.ts
echo "Recompiling with original files..."
npm run compile
echo "Restored original files"
echo "Built: ${OUTPUT_NAME}"

BIN
build/open-vsix/icon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 689 KiB

View File

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

File diff suppressed because it is too large Load Diff

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

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

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

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

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

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

View File

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

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

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

View File

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

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

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

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

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

View File

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

File diff suppressed because it is too large Load Diff

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

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

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

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

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

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

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

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

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

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

File diff suppressed because it is too large Load Diff

544
src/ui.ts
View File

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

View File

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