ocb-cli 1.0.4 → 1.0.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/dist/proxy.js +209 -263
  2. package/package.json +1 -1
  3. package/src/proxy.ts +207 -263
package/dist/proxy.js CHANGED
@@ -156,6 +156,29 @@ app.post("/api/refresh-models", async (req, res) => {
156
156
  await fetchModelsFromOpenCode();
157
157
  res.json({ success: true, count: availableModels.length });
158
158
  });
159
+ app.post("/api/apply-claude-config", async (req, res) => {
160
+ try {
161
+ const { readFileSync, writeFileSync, existsSync, mkdirSync } = await import("fs");
162
+ const { join, dirname } = await import("path");
163
+ const homedir = process.env.USERPROFILE || process.env.HOME || process.env.HOMEPATH || "";
164
+ const settingsPath = join(homedir, ".claude", "settings.json");
165
+ const settingsDir = dirname(settingsPath);
166
+ if (!existsSync(settingsDir))
167
+ mkdirSync(settingsDir, { recursive: true });
168
+ let settings = {};
169
+ if (existsSync(settingsPath))
170
+ settings = JSON.parse(readFileSync(settingsPath, "utf-8"));
171
+ settings.env = settings.env || {};
172
+ settings.env.ANTHROPIC_BASE_URL = "http://localhost:8300";
173
+ settings.env.ANTHROPIC_API_KEY = "test";
174
+ settings.env.ANTHROPIC_MODEL = currentModel;
175
+ writeFileSync(settingsPath, JSON.stringify(settings, null, 2));
176
+ res.json({ success: true });
177
+ }
178
+ catch (e) {
179
+ res.status(500).json({ success: false, error: e instanceof Error ? e.message : String(e) });
180
+ }
181
+ });
159
182
  app.post("/api/reset-stats", (req, res) => {
160
183
  totalTokensUsed = 0;
161
184
  totalRequests = 0;
@@ -259,309 +282,232 @@ app.post("/v1/messages/count_tokens", (req, res) => {
259
282
  const PORT = PROXY_PORT;
260
283
  function generateHTML() {
261
284
  return `<!DOCTYPE html>
262
- <html lang="en">
285
+ <html lang="en" data-theme="dark" class="dark">
263
286
  <head>
264
287
  <meta charset="UTF-8">
265
288
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
266
289
  <title>OCB - OpenCode Bridge</title>
267
290
  <script src="https://cdn.tailwindcss.com"></script>
291
+ <script defer src="https://cdn.jsdelivr.net/npm/alpinejs@3.x.x/dist/cdn.min.js"></script>
268
292
  <link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500;600&family=Inter:wght@300;400;500;600;700&display=swap" rel="stylesheet">
293
+ <script>
294
+ tailwind.config = {
295
+ theme: {
296
+ extend: {
297
+ colors: {
298
+ space: { 950: '#0a0a0f', 900: '#121218', 800: '#1a1a24', 700: '#24242e', border: '#2a2a38' },
299
+ neon: { purple: '#a855f7', cyan: '#22d3ee', green: '#22c55e', pink: '#ec4899' }
300
+ },
301
+ fontFamily: { sans: ['Inter', 'sans-serif'], mono: ['JetBrains Mono', 'monospace'] }
302
+ }
303
+ }
304
+ }
305
+ </script>
269
306
  <style>
270
307
  * { box-sizing: border-box; margin: 0; padding: 0; }
271
- body { font-family: 'Inter', -apple-system, sans-serif; background: #0f0f11; color: #fafafa; min-height: 100vh; }
308
+ body { font-family: 'Inter', -apple-system, sans-serif; background: #0a0a0f; color: #e4e4e7; min-height: 100vh; }
309
+ [x-cloak] { display: none !important; }
272
310
  ::-webkit-scrollbar { width: 6px; }
273
311
  ::-webkit-scrollbar-track { background: transparent; }
274
312
  ::-webkit-scrollbar-thumb { background: #3f3f46; border-radius: 3px; }
275
- .tab-active { background: linear-gradient(135deg, #6366f1 0%, #8b5cf6 100%); color: white; }
276
- .model-card:hover { transform: translateY(-2px); box-shadow: 0 8px 25px -5px rgba(99, 102, 241, 0.3); }
313
+ .nav-item { transition: all 0.2s; }
314
+ .nav-item.active { background: linear-gradient(90deg, rgba(168,85,247,0.2) 0%, transparent 100%); border-left: 2px solid #a855f7; color: white; }
277
315
  </style>
278
316
  </head>
279
- <body>
280
- <div class="min-h-screen flex flex-col">
281
- <header class="bg-[#18181b] border-b border-zinc-800 px-6 py-4">
282
- <div class="max-w-7xl mx-auto flex items-center justify-between">
283
- <div class="flex items-center gap-4">
284
- <div class="w-12 h-12 bg-gradient-to-br from-indigo-500 to-purple-600 rounded-2xl flex items-center justify-center text-2xl shadow-lg shadow-indigo-500/20">⚡</div>
285
- <div>
286
- <h1 class="text-2xl font-bold bg-gradient-to-r from-white to-zinc-400 bg-clip-text text-transparent">OCB</h1>
287
- <p class="text-xs text-zinc-500 font-medium">OpenCode Bridge</p>
288
- </div>
289
- <div class="ml-8 flex items-center gap-2 px-4 py-2 bg-zinc-900 rounded-xl border border-zinc-800">
290
- <div id="healthDot" class="w-2.5 h-2.5 bg-green-500 rounded-full animate-pulse"></div>
291
- <span id="connectionStatus" class="text-sm text-zinc-300">Connected</span>
292
- </div>
293
- </div>
294
- <div class="flex items-center gap-3">
295
- <div class="text-right mr-4">
296
- <p class="text-xs text-zinc-500">Current Model</p>
297
- <p id="currentModelDisplay" class="text-sm font-medium text-white">Loading...</p>
298
- </div>
299
- <button onclick="refreshModels()" class="p-2.5 bg-zinc-800 hover:bg-zinc-700 rounded-xl transition text-zinc-300 hover:text-white" title="Refresh Models">
300
- <svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15"></path></svg>
301
- </button>
302
- </div>
317
+ <body class="overflow-hidden" x-data="app()" x-init="init()">
318
+ <div class="h-14 border-b border-space-border flex items-center px-4 justify-between bg-space-900/80 backdrop-blur-md">
319
+ <div class="flex items-center gap-3">
320
+ <div class="w-8 h-8 rounded-lg bg-gradient-to-br from-neon-purple to-blue-600 flex items-center justify-center text-white font-bold shadow-lg shadow-neon-purple/20">OC</div>
321
+ <div class="flex flex-col">
322
+ <span class="text-sm font-bold tracking-wide text-white">OCB</span>
323
+ <span class="text-[10px] text-gray-500 font-mono">OPENCODE BRIDGE</span>
303
324
  </div>
304
- </header>
305
-
306
- <nav class="bg-[#18181b] border-b border-zinc-800 px-6">
307
- <div class="max-w-7xl mx-auto flex gap-1">
308
- <button onclick="switchTab('models')" id="tab-models" class="px-5 py-3 text-sm font-medium rounded-t-xl transition-all tab-active">Models</button>
309
- <button onclick="switchTab('status')" id="tab-status" class="px-5 py-3 text-sm font-medium rounded-t-xl transition-all text-zinc-400 hover:bg-zinc-800">Status</button>
310
- <button onclick="switchTab('settings')" id="tab-settings" class="px-5 py-3 text-sm font-medium rounded-t-xl transition-all text-zinc-400 hover:bg-zinc-800">Settings</button>
325
+ </div>
326
+ <div class="flex items-center gap-4">
327
+ <div class="flex items-center gap-2 px-3 py-1 rounded-full text-xs font-mono border transition-all" :class="connected ? 'bg-neon-green/10 border-neon-green/20 text-neon-green' : 'bg-red-500/10 border-red-500/20 text-red-400'">
328
+ <div class="w-1.5 h-1.5 rounded-full" :class="connected ? 'bg-neon-green shadow-lg shadow-neon-green/50' : 'bg-red-400'"></div>
329
+ <span x-text="connected ? 'Connected' : 'Disconnected'"></span>
311
330
  </div>
312
- </nav>
313
-
314
- <main class="flex-1 max-w-7xl mx-auto w-full p-6">
315
- <div id="models-view" class="flex gap-6 h-[calc(100vh-220px)]">
316
- <aside class="w-72 bg-[#18181b] rounded-2xl border border-zinc-800 flex flex-col overflow-hidden">
317
- <div class="p-4 border-b border-zinc-800">
318
- <div class="relative">
319
- <input type="text" id="searchInput" placeholder="Search models..." oninput="filterData()" class="w-full px-4 py-2.5 bg-zinc-900 border border-zinc-700 rounded-xl text-sm text-white placeholder-zinc-500 focus:outline-none focus:border-indigo-500 focus:ring-2 focus:ring-indigo-500/20">
320
- </div>
321
- </div>
322
- <div id="providerList" class="flex-1 overflow-y-auto p-2"></div>
323
- </aside>
324
- <section class="flex-1 overflow-y-auto">
325
- <div class="flex items-center justify-between mb-4">
326
- <h2 id="sectionTitle" class="text-lg font-semibold text-white">All Models</h2>
327
- <span id="modelCount" class="text-sm text-zinc-500"></span>
328
- </div>
329
- <div id="modelGrid" class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4"></div>
330
- </section>
331
+ <button @click="refresh()" class="p-2 hover:bg-white/5 rounded-lg transition text-gray-400">
332
+ <svg class="w-4 h-4" :class="{'animate-spin': loading}" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15"></path></svg>
333
+ </button>
334
+ </div>
335
+ </div>
336
+ <div class="flex h-[calc(100vh-56px)]">
337
+ <nav class="w-56 bg-space-900 border-r border-space-border flex flex-col">
338
+ <div class="p-4 space-y-1">
339
+ <button @click="tab='dashboard'" class="nav-item w-full flex items-center gap-3 px-4 py-2.5 rounded-lg text-sm font-medium" :class="tab==='dashboard'?'active':''">
340
+ <svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2H6a2 2 0 01-2-2V6zM14 6a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2h-2a2 2 0 01-2-2V6zM4 16a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2H6a2 2 0 01-2-2v-2zM14 16a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2h-2a2 2 0 01-2-2v-2z"></path></svg>
341
+ Dashboard
342
+ </button>
343
+ <button @click="tab='models'" class="nav-item w-full flex items-center gap-3 px-4 py-2.5 rounded-lg text-sm font-medium" :class="tab==='models'?'active':''">
344
+ <svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M20 7l-8-4-8 4m16 0l-8 4m8-4v10l-8 4m0-10L4 7m8 4v10M4 7v10l8 4"></path></svg>
345
+ Models
346
+ </button>
347
+ <button @click="tab='settings'" class="nav-item w-full flex items-center gap-3 px-4 py-2.5 rounded-lg text-sm font-medium" :class="tab==='settings'?'active':''">
348
+ <svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z"></path><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"></path></svg>
349
+ Settings
350
+ </button>
331
351
  </div>
332
-
333
- <div id="status-view" class="hidden space-y-6">
334
- <div class="grid grid-cols-1 md:grid-cols-3 gap-4">
335
- <div class="bg-[#18181b] rounded-2xl border border-zinc-800 p-6">
336
- <div class="flex items-center gap-3 mb-4">
337
- <div class="w-10 h-10 bg-indigo-500/20 rounded-xl flex items-center justify-center text-indigo-400">📊</div>
338
- <div>
339
- <p class="text-xs text-zinc-500">Total Requests</p>
340
- <p id="totalRequests" class="text-2xl font-bold text-white">0</p>
341
- </div>
352
+ <div class="mt-auto p-4 border-t border-space-border">
353
+ <div class="text-xs text-gray-500 font-mono">
354
+ <div class="flex justify-between mb-1"><span>Port:</span><span class="text-gray-400">8300</span></div>
355
+ <div class="flex justify-between"><span>OpenCode:</span><span class="text-neon-green">localhost:4096</span></div>
356
+ </div>
357
+ </div>
358
+ </nav>
359
+ <main class="flex-1 overflow-auto bg-space-950 p-6">
360
+ <div x-show="tab==='dashboard'" x-transition>
361
+ <div class="grid grid-cols-1 md:grid-cols-3 gap-4 mb-6">
362
+ <div class="bg-space-900 rounded-xl border border-space-border p-5">
363
+ <div class="flex items-center gap-3 mb-3">
364
+ <div class="w-10 h-10 bg-neon-purple/20 rounded-xl flex items-center justify-center text-neon-purple">📊</div>
365
+ <div><p class="text-xs text-gray-500">Total Requests</p><p class="text-2xl font-bold text-white" x-text="stats.requests.toLocaleString()">0</p></div>
342
366
  </div>
343
367
  </div>
344
- <div class="bg-[#18181b] rounded-2xl border border-zinc-800 p-6">
345
- <div class="flex items-center gap-3 mb-4">
346
- <div class="w-10 h-10 bg-purple-500/20 rounded-xl flex items-center justify-center text-purple-400">🎯</div>
347
- <div>
348
- <p class="text-xs text-zinc-500">Total Tokens</p>
349
- <p id="totalTokens" class="text-2xl font-bold text-white">0</p>
350
- </div>
368
+ <div class="bg-space-900 rounded-xl border border-space-border p-5">
369
+ <div class="flex items-center gap-3 mb-3">
370
+ <div class="w-10 h-10 bg-neon-cyan/20 rounded-xl flex items-center justify-center text-neon-cyan">🎯</div>
371
+ <div><p class="text-xs text-gray-500">Total Tokens</p><p class="text-2xl font-bold text-white" x-text="stats.tokens.toLocaleString()">0</p></div>
351
372
  </div>
352
373
  </div>
353
- <div class="bg-[#18181b] rounded-2xl border border-zinc-800 p-6">
354
- <div class="flex items-center gap-3 mb-4">
355
- <div class="w-10 h-10 bg-green-500/20 rounded-xl flex items-center justify-center text-green-400">🔗</div>
356
- <div>
357
- <p class="text-xs text-zinc-500">Session</p>
358
- <p id="sessionStatus" class="text-lg font-semibold text-green-400">Active</p>
359
- </div>
374
+ <div class="bg-space-900 rounded-xl border border-space-border p-5">
375
+ <div class="flex items-center gap-3 mb-3">
376
+ <div class="w-10 h-10 bg-neon-green/20 rounded-xl flex items-center justify-center text-neon-green">🤖</div>
377
+ <div><p class="text-xs text-gray-500">Active Model</p><p class="text-lg font-semibold text-white truncate" x-text="currentModelName">-</p></div>
360
378
  </div>
361
379
  </div>
362
380
  </div>
363
- <div class="bg-[#18181b] rounded-2xl border border-zinc-800 p-6">
364
- <h3 class="text-lg font-semibold text-white mb-4">Model Usage</h3>
365
- <div class="space-y-3">
366
- <div class="flex justify-between items-center py-2 border-b border-zinc-800">
367
- <span class="text-zinc-400">Active Model</span>
368
- <span id="activeModelName" class="text-white font-medium">-</span>
369
- </div>
370
- <div class="flex justify-between items-center py-2 border-b border-zinc-800">
371
- <span class="text-zinc-400">Available Models</span>
372
- <span id="totalModelsDisplay" class="text-white font-medium">-</span>
373
- </div>
374
- <div class="flex justify-between items-center py-2 border-b border-zinc-800">
375
- <span class="text-zinc-400">Providers</span>
376
- <span id="totalProviders" class="text-white font-medium">-</span>
377
- </div>
378
- <div class="flex justify-between items-center py-2">
379
- <span class="text-zinc-400">OpenCode Server</span>
380
- <span class="text-green-400 font-medium">localhost:4096</span>
381
- </div>
381
+ <div class="bg-space-900 rounded-xl border border-space-border p-5">
382
+ <h3 class="text-lg font-semibold text-white mb-4">Current Session</h3>
383
+ <div class="grid grid-cols-2 gap-4 text-sm">
384
+ <div class="flex justify-between py-2 border-b border-space-border"><span class="text-gray-500">Session ID</span><span class="text-gray-300 font-mono text-xs" x-text="stats.sessionId || 'inactive'">-</span></div>
385
+ <div class="flex justify-between py-2 border-b border-space-border"><span class="text-gray-500">Status</span><span class="text-neon-green" x-text="stats.sessionId ? 'Active' : 'Inactive'">-</span></div>
386
+ <div class="flex justify-between py-2 border-b border-space-border"><span class="text-gray-500">Models Available</span><span class="text-gray-300" x-text="stats.models">0</span></div>
387
+ <div class="flex justify-between py-2 border-b border-space-border"><span class="text-gray-500">Providers</span><span class="text-gray-300" x-text="stats.providers">0</span></div>
388
+ </div>
389
+ <div class="mt-4 flex gap-3">
390
+ <button @click="resetSession()" class="px-4 py-2 bg-space-700 hover:bg-space-600 rounded-lg text-sm transition">Reset Session</button>
391
+ <button @click="refreshModels()" class="px-4 py-2 bg-space-700 hover:bg-space-600 rounded-lg text-sm transition">Refresh Models</button>
382
392
  </div>
383
393
  </div>
384
- <div class="flex gap-4">
385
- <button onclick="resetSession()" class="px-6 py-3 bg-zinc-800 hover:bg-zinc-700 rounded-xl text-white font-medium transition">Reset Session</button>
386
- <button onclick="resetStats()" class="px-6 py-3 bg-zinc-800 hover:bg-zinc-700 rounded-xl text-white font-medium transition">Reset Stats</button>
394
+ </div>
395
+ <div x-show="tab==='models'" x-transition style="display:none;">
396
+ <div class="mb-4 flex gap-4">
397
+ <input type="text" x-model="search" @input="filterModels()" placeholder="Search models..." class="flex-1 px-4 py-2.5 bg-space-900 border border-space-border rounded-xl text-white placeholder-gray-500 focus:outline-none focus:border-neon-purple">
398
+ <select x-model="selectedProvider" @change="filterModels()" class="px-4 py-2.5 bg-space-900 border border-space-border rounded-xl text-white focus:outline-none">
399
+ <option value="all">All Providers</option>
400
+ <template x-for="p in providers" :key="p"><option :value="p" x-text="p"></option></template>
401
+ </select>
402
+ </div>
403
+ <div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-3 max-h-[calc(100vh-220px)] overflow-y-auto">
404
+ <template x-for="m in filteredModels" :key="m.id">
405
+ <div @click="selectModel(m.id)" class="p-4 rounded-xl border cursor-pointer transition-all hover:scale-[1.02]" :class="m.id===currentModel?'bg-neon-purple/20 border-neon-purple':'bg-space-900 border-space-border hover:border-space-700'">
406
+ <div class="flex items-start justify-between mb-2">
407
+ <h3 class="font-semibold text-white truncate" x-text="m.name"></h3>
408
+ <span x-show="m.id===currentModel" class="text-xs bg-neon-purple text-white px-2 py-0.5 rounded">Active</span>
409
+ </div>
410
+ <p class="text-xs text-gray-500 font-mono truncate mb-2" x-text="m.id"></p>
411
+ <p class="text-xs text-gray-600" x-text="m.provider"></p>
412
+ </div>
413
+ </template>
387
414
  </div>
388
415
  </div>
389
-
390
- <div id="settings-view" class="hidden space-y-6">
391
- <div class="bg-[#18181b] rounded-2xl border border-zinc-800 p-6">
416
+ <div x-show="tab==='settings'" x-transition style="display:none;">
417
+ <div class="bg-space-900 rounded-xl border border-space-border p-6 mb-4">
392
418
  <h3 class="text-lg font-semibold text-white mb-4">Claude Code Configuration</h3>
393
- <p class="text-zinc-400 text-sm mb-4">Add this to your Claude Code settings to use OCB:</p>
394
- <div class="bg-zinc-950 rounded-xl p-4 font-mono text-sm text-zinc-300 overflow-x-auto">
395
- <pre id="configJson">{
396
- "env": {
397
- "ANTHROPIC_BASE_URL": "http://localhost:8300",
398
- "ANTHROPIC_API_KEY": "test",
399
- "ANTHROPIC_MODEL": "claude-sonnet-4-5"
400
- }
401
- }</pre>
419
+ <p class="text-gray-400 text-sm mb-4">Configure Claude Code to use OCB as the API endpoint:</p>
420
+ <div class="bg-space-950 rounded-xl p-4 font-mono text-sm text-gray-300 overflow-x-auto mb-4">
421
+ <pre x-text="configJson"></pre>
402
422
  </div>
403
- <button onclick="copyConfig()" class="mt-4 px-6 py-2.5 bg-indigo-600 hover:bg-indigo-500 rounded-xl text-white font-medium transition flex items-center gap-2">
404
- <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 16H6a2 2 0 01-2-2V6a2 2 0 012-2h8a2 2 0 012 2v2m-6 12h8a2 2 0 002-2v-8a2 2 0 00-2-2h-8a2 2 0 00-2 2v8a2 2 0 002 2z"></path></svg>
405
- Copy Config
423
+ <button @click="applyToClaude()" class="px-6 py-2.5 bg-neon-purple hover:bg-neon-purple/80 rounded-xl text-white font-medium transition flex items-center gap-2">
424
+ <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"></path></svg>
425
+ Apply to Claude Code
406
426
  </button>
407
427
  </div>
408
- <div class="bg-[#18181b] rounded-2xl border border-zinc-800 p-6">
428
+ <div class="bg-space-900 rounded-xl border border-space-border p-6">
409
429
  <h3 class="text-lg font-semibold text-white mb-4">API Endpoints</h3>
410
430
  <div class="space-y-2 font-mono text-sm">
411
- <div class="flex justify-between py-2 border-b border-zinc-800"><span class="text-zinc-400">Health</span><span class="text-indigo-400">http://localhost:8300/health</span></div>
412
- <div class="flex justify-between py-2 border-b border-zinc-800"><span class="text-zinc-400">Status</span><span class="text-indigo-400">http://localhost:8300/api/status</span></div>
413
- <div class="flex justify-between py-2 border-b border-zinc-800"><span class="text-zinc-400">Models</span><span class="text-indigo-400">http://localhost:8300/api/models</span></div>
414
- <div class="flex justify-between py-2 border-b border-zinc-800"><span class="text-zinc-400">Set Model</span><span class="text-indigo-400">POST /api/model</span></div>
415
- <div class="flex justify-between py-2"><span class="text-zinc-400">Messages</span><span class="text-indigo-400">http://localhost:8300/v1/messages</span></div>
431
+ <div class="flex justify-between py-2 border-b border-space-border"><span class="text-gray-500">Health</span><span class="text-neon-cyan">/health</span></div>
432
+ <div class="flex justify-between py-2 border-b border-space-border"><span class="text-gray-500">Status</span><span class="text-neon-cyan">/api/status</span></div>
433
+ <div class="flex justify-between py-2 border-b border-space-border"><span class="text-gray-500">Models</span><span class="text-neon-cyan">/v1/models</span></div>
434
+ <div class="flex justify-between py-2 border-b border-space-border"><span class="text-gray-500">Messages</span><span class="text-neon-cyan">/v1/messages</span></div>
416
435
  </div>
417
436
  </div>
418
437
  </div>
419
438
  </main>
420
-
421
- <footer class="bg-[#18181b] border-t border-zinc-800 px-6 py-3">
422
- <div class="max-w-7xl mx-auto flex items-center justify-between text-sm text-zinc-500">
423
- <div class="flex items-center gap-6">
424
- <span>OCB v1.0.1</span>
425
- <span>Proxy Port: <span class="text-white">8300</span></span>
426
- </div>
427
- <span>OpenCode Bridge for Claude Code</span>
428
- </div>
429
- </footer>
430
439
  </div>
431
440
  <script>
432
- let allModels = [];
433
- let groupedModels = {};
434
- let currentProvider = 'all';
435
- let currentModelId = null;
436
-
437
- function switchTab(tab) {
438
- document.querySelectorAll('[id^="tab-"]').forEach(el => {
439
- el.classList.remove('tab-active');
440
- el.classList.add('text-zinc-400', 'hover:bg-zinc-800');
441
- });
442
- document.getElementById('tab-' + tab).classList.add('tab-active');
443
- document.getElementById('tab-' + tab).classList.remove('text-zinc-400', 'hover:bg-zinc-800');
444
-
445
- document.getElementById('models-view').classList.add('hidden');
446
- document.getElementById('status-view').classList.add('hidden');
447
- document.getElementById('settings-view').classList.add('hidden');
448
- document.getElementById(tab + '-view').classList.remove('hidden');
449
- }
450
-
451
- function copyConfig() {
452
- const config = document.getElementById('configJson').textContent;
453
- navigator.clipboard.writeText(config);
454
- alert('Config copied to clipboard!');
455
- }
456
-
457
- async function resetStats() {
458
- await fetch('/api/reset-stats', { method: 'POST' });
459
- loadStatus();
460
- }
461
-
462
- async function loadModels() {
463
- const res = await fetch('/api/models');
464
- const data = await res.json();
465
- allModels = data.models || [];
466
- groupedModels = data.grouped || {};
467
- renderProviders();
468
- renderModels(allModels);
469
- updateStats();
470
- }
471
-
472
- function renderProviders() {
473
- const container = document.getElementById('providerList');
474
- const searchTerm = document.getElementById('searchInput').value.toLowerCase();
475
- let providersToShow = Object.entries(groupedModels);
476
- if (searchTerm) {
477
- providersToShow = providersToShow.filter(([name, models]) => name.toLowerCase().includes(searchTerm) || models.some(m => m.name.toLowerCase().includes(searchTerm)));
478
- }
479
- let html = '<div onclick="selectProvider('all')" class="px-3 py-2.5 rounded-xl cursor-pointer flex items-center justify-between text-sm mb-1 ' + (currentProvider === 'all' ? 'bg-indigo-600 text-white' : 'text-zinc-400 hover:bg-zinc-800') + '"><span>🏠 All Models</span><span class="text-xs opacity-60 bg-zinc-800 px-2 py-0.5 rounded-full">' + allModels.length + '</span></div>';
480
- for (const [provider, models] of providersToShow) {
481
- html += '<div onclick="selectProvider(\\'' + provider.replace(/'/g, "\\\\'") + '\\')" class="px-3 py-2.5 rounded-xl cursor-pointer flex items-center justify-between text-sm mb-1 ' + (currentProvider === provider ? 'bg-indigo-600 text-white' : 'text-zinc-400 hover:bg-zinc-800') + '"><span class="truncate">' + provider + '</span><span class="text-xs opacity-60 bg-zinc-800 px-2 py-0.5 rounded-full">' + models.length + '</span></div>';
482
- }
483
- container.innerHTML = html;
484
- }
485
-
486
- function selectProvider(provider) {
487
- currentProvider = provider;
488
- document.getElementById('sectionTitle').textContent = provider === 'all' ? 'All Models' : provider;
489
- renderProviders();
490
- renderModels(provider === 'all' ? allModels : (groupedModels[provider] || []));
491
- }
492
-
493
- function renderModels(models) {
494
- const container = document.getElementById('modelGrid');
495
- const searchTerm = document.getElementById('searchInput').value.toLowerCase();
496
- let filtered = models;
497
- if (searchTerm) {
498
- filtered = models.filter(m => m.name.toLowerCase().includes(searchTerm) || m.id.toLowerCase().includes(searchTerm));
499
- }
500
- document.getElementById('modelCount').textContent = filtered.length + ' models';
501
- if (filtered.length === 0) {
502
- container.innerHTML = '<div class="col-span-full text-center text-zinc-500 py-12">No models found</div>';
503
- return;
504
- }
505
- container.innerHTML = filtered.slice(0, 100).map(m => '<div onclick="selectModel(\\'' + m.id.replace(/'/g, "\\\\'") + '\\')" class="model-card p-5 rounded-2xl border cursor-pointer transition-all ' + (m.id === currentModelId ? 'bg-indigo-600/20 border-indigo-500 shadow-lg shadow-indigo-500/20' : 'bg-[#18181b] border-zinc-800 hover:border-zinc-700') + '"><div class="flex items-start justify-between mb-3"><h3 class="font-semibold text-white truncate text-base">' + m.name + '</h3>' + (m.id === currentModelId ? '<span class="text-xs bg-indigo-500 text-white px-3 py-1 rounded-full font-medium">Active</span>' : '') + '</div><p class="text-xs text-zinc-500 font-mono truncate mb-3">' + m.id + '</p>' + (m.cost ? '<div class="flex items-center gap-2 text-xs text-zinc-400"><span class="bg-zinc-800 px-2 py-1 rounded">$' + m.cost.input + '/M</span><span>→</span><span class="bg-zinc-800 px-2 py-1 rounded">$' + m.cost.output + '/M</span></div>' : '<div class="text-xs text-zinc-600">Free</div>') + '</div>').join('');
506
- }
507
-
508
- function filterData() {
509
- const searchTerm = document.getElementById('searchInput').value.toLowerCase();
510
- if (searchTerm) {
511
- const filtered = allModels.filter(m => m.name.toLowerCase().includes(searchTerm) || m.id.toLowerCase().includes(searchTerm));
512
- document.getElementById('sectionTitle').textContent = 'Search Results (' + filtered.length + ')';
513
- renderModels(filtered);
514
- renderProviders();
515
- } else {
516
- selectProvider(currentProvider);
517
- }
518
- }
519
-
520
- async function selectModel(modelId) {
521
- const res = await fetch('/api/model', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ modelId }) });
522
- const data = await res.json();
523
- if (data.success) {
524
- currentModelId = modelId;
525
- loadStatus();
526
- renderModels(currentProvider === 'all' ? allModels : (groupedModels[currentProvider] || []));
527
- document.getElementById('configJson').textContent = JSON.stringify({ env: { ANTHROPIC_BASE_URL: "http://localhost:8300", ANTHROPIC_API_KEY: "test", ANTHROPIC_MODEL: modelId } }, null, 2);
528
- }
529
- }
530
-
531
- async function loadStatus() {
532
- const res = await fetch('/api/status');
533
- const data = await res.json();
534
- const model = allModels.find(m => m.id === data.currentModel);
535
- document.getElementById('currentModelDisplay').textContent = model?.name || data.currentModel;
536
- document.getElementById('healthDot').className = 'w-2.5 h-2.5 rounded-full animate-pulse ' + (data.sessionId === 'active' ? 'bg-green-500' : 'bg-red-500');
537
- document.getElementById('connectionStatus').textContent = data.sessionId === 'active' ? 'Connected' : 'Disconnected';
538
- document.getElementById('totalRequests').textContent = data.totalRequests.toLocaleString();
539
- document.getElementById('totalTokens').textContent = data.totalTokensUsed.toLocaleString();
540
- document.getElementById('sessionStatus').textContent = data.sessionId === 'active' ? 'Active' : 'Inactive';
541
- document.getElementById('sessionStatus').className = 'text-lg font-semibold ' + (data.sessionId === 'active' ? 'text-green-400' : 'text-red-400');
542
- document.getElementById('activeModelName').textContent = model?.name || data.currentModel;
543
- document.getElementById('totalModelsDisplay').textContent = allModels.length + ' models';
544
- document.getElementById('totalProviders').textContent = Object.keys(groupedModels).length + ' providers';
545
- currentModelId = data.currentModel;
546
- }
547
-
548
- async function resetSession() {
549
- await fetch('/api/reset-session', { method: 'POST' });
550
- loadStatus();
551
- }
552
-
553
- async function refreshModels() {
554
- await fetch('/api/refresh-models', { method: 'POST' });
555
- loadModels();
556
- }
557
-
558
- function updateStats() {
559
- document.getElementById('totalModelsDisplay').textContent = allModels.length + ' models from ' + Object.keys(groupedModels).length + ' providers';
441
+ function app() {
442
+ return {
443
+ tab: 'dashboard',
444
+ loading: false,
445
+ connected: false,
446
+ search: '',
447
+ selectedProvider: 'all',
448
+ models: [],
449
+ providers: [],
450
+ filteredModels: [],
451
+ currentModel: '',
452
+ currentModelName: '',
453
+ stats: { requests: 0, tokens: 0, sessionId: '', models: 0, providers: 0 },
454
+ configJson: '',
455
+ async init() {
456
+ await this.refresh();
457
+ setInterval(() => this.refresh(), 3000);
458
+ },
459
+ async refresh() {
460
+ this.loading = true;
461
+ try {
462
+ const [statusRes, modelsRes] = await Promise.all([fetch('/api/status'), fetch('/api/models')]);
463
+ const status = await statusRes.json();
464
+ const modelsData = await modelsRes.json();
465
+ this.connected = status.sessionId === 'active';
466
+ this.stats = { requests: status.totalRequests, tokens: status.totalTokensUsed, sessionId: status.sessionId, models: modelsData.models?.length || 0, providers: modelsData.providers?.length || 0 };
467
+ this.currentModel = status.currentModel;
468
+ this.models = modelsData.models || [];
469
+ this.providers = [...new Set(this.models.map(m => m.provider))].sort();
470
+ const current = this.models.find(m => m.id === this.currentModel);
471
+ this.currentModelName = current?.name || this.currentModel;
472
+ this.filterModels();
473
+ this.updateConfig();
474
+ } catch (e) { console.error(e); }
475
+ this.loading = false;
476
+ },
477
+ filterModels() {
478
+ let filtered = this.models;
479
+ if (this.selectedProvider !== 'all') filtered = filtered.filter(m => m.provider === this.selectedProvider);
480
+ if (this.search) {
481
+ const s = this.search.toLowerCase();
482
+ filtered = filtered.filter(m => m.name.toLowerCase().includes(s) || m.id.toLowerCase().includes(s));
483
+ }
484
+ this.filteredModels = filtered.slice(0, 100);
485
+ },
486
+ async selectModel(modelId) {
487
+ await fetch('/api/model', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ modelId }) });
488
+ this.currentModel = modelId;
489
+ this.refresh();
490
+ },
491
+ async resetSession() {
492
+ await fetch('/api/reset-session', { method: 'POST' });
493
+ this.refresh();
494
+ },
495
+ async refreshModels() {
496
+ await fetch('/api/refresh-models', { method: 'POST' });
497
+ this.refresh();
498
+ },
499
+ updateConfig() {
500
+ this.configJson = JSON.stringify({ env: { ANTHROPIC_BASE_URL: 'http://localhost:8300', ANTHROPIC_API_KEY: 'test', ANTHROPIC_MODEL: this.currentModel } }, null, 2);
501
+ },
502
+ async applyToClaude() {
503
+ try {
504
+ const response = await fetch('/api/apply-claude-config', { method: 'POST' });
505
+ const data = await response.json();
506
+ alert(data.success ? 'Applied to Claude Code! Restart Claude Code to use.' : 'Failed: ' + data.error);
507
+ } catch (e) { alert('Error: ' + e.message); }
508
+ }
509
+ };
560
510
  }
561
-
562
- loadModels();
563
- loadStatus();
564
- setInterval(loadStatus, 3000);
565
511
  </script>
566
512
  </body>
567
513
  </html>`;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ocb-cli",
3
- "version": "1.0.4",
3
+ "version": "1.0.5",
4
4
  "description": "OpenCode Bridge - Use OpenCode AI models in Claude Code",
5
5
  "type": "module",
6
6
  "main": "dist/proxy.js",
package/src/proxy.ts CHANGED
@@ -188,6 +188,27 @@ app.post("/api/refresh-models", async (req, res) => {
188
188
  res.json({ success: true, count: availableModels.length });
189
189
  });
190
190
 
191
+ app.post("/api/apply-claude-config", async (req, res) => {
192
+ try {
193
+ const { readFileSync, writeFileSync, existsSync, mkdirSync } = await import("fs");
194
+ const { join, dirname } = await import("path");
195
+ const homedir = process.env.USERPROFILE || process.env.HOME || process.env.HOMEPATH || "";
196
+ const settingsPath = join(homedir, ".claude", "settings.json");
197
+ const settingsDir = dirname(settingsPath);
198
+ if (!existsSync(settingsDir)) mkdirSync(settingsDir, { recursive: true });
199
+ let settings: any = {};
200
+ if (existsSync(settingsPath)) settings = JSON.parse(readFileSync(settingsPath, "utf-8"));
201
+ settings.env = settings.env || {};
202
+ settings.env.ANTHROPIC_BASE_URL = "http://localhost:8300";
203
+ settings.env.ANTHROPIC_API_KEY = "test";
204
+ settings.env.ANTHROPIC_MODEL = currentModel;
205
+ writeFileSync(settingsPath, JSON.stringify(settings, null, 2));
206
+ res.json({ success: true });
207
+ } catch (e) {
208
+ res.status(500).json({ success: false, error: e instanceof Error ? e.message : String(e) });
209
+ }
210
+ });
211
+
191
212
  app.post("/api/reset-stats", (req, res) => {
192
213
  totalTokensUsed = 0;
193
214
  totalRequests = 0;
@@ -298,309 +319,232 @@ const PORT = PROXY_PORT;
298
319
 
299
320
  function generateHTML() {
300
321
  return `<!DOCTYPE html>
301
- <html lang="en">
322
+ <html lang="en" data-theme="dark" class="dark">
302
323
  <head>
303
324
  <meta charset="UTF-8">
304
325
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
305
326
  <title>OCB - OpenCode Bridge</title>
306
327
  <script src="https://cdn.tailwindcss.com"></script>
328
+ <script defer src="https://cdn.jsdelivr.net/npm/alpinejs@3.x.x/dist/cdn.min.js"></script>
307
329
  <link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500;600&family=Inter:wght@300;400;500;600;700&display=swap" rel="stylesheet">
330
+ <script>
331
+ tailwind.config = {
332
+ theme: {
333
+ extend: {
334
+ colors: {
335
+ space: { 950: '#0a0a0f', 900: '#121218', 800: '#1a1a24', 700: '#24242e', border: '#2a2a38' },
336
+ neon: { purple: '#a855f7', cyan: '#22d3ee', green: '#22c55e', pink: '#ec4899' }
337
+ },
338
+ fontFamily: { sans: ['Inter', 'sans-serif'], mono: ['JetBrains Mono', 'monospace'] }
339
+ }
340
+ }
341
+ }
342
+ </script>
308
343
  <style>
309
344
  * { box-sizing: border-box; margin: 0; padding: 0; }
310
- body { font-family: 'Inter', -apple-system, sans-serif; background: #0f0f11; color: #fafafa; min-height: 100vh; }
345
+ body { font-family: 'Inter', -apple-system, sans-serif; background: #0a0a0f; color: #e4e4e7; min-height: 100vh; }
346
+ [x-cloak] { display: none !important; }
311
347
  ::-webkit-scrollbar { width: 6px; }
312
348
  ::-webkit-scrollbar-track { background: transparent; }
313
349
  ::-webkit-scrollbar-thumb { background: #3f3f46; border-radius: 3px; }
314
- .tab-active { background: linear-gradient(135deg, #6366f1 0%, #8b5cf6 100%); color: white; }
315
- .model-card:hover { transform: translateY(-2px); box-shadow: 0 8px 25px -5px rgba(99, 102, 241, 0.3); }
350
+ .nav-item { transition: all 0.2s; }
351
+ .nav-item.active { background: linear-gradient(90deg, rgba(168,85,247,0.2) 0%, transparent 100%); border-left: 2px solid #a855f7; color: white; }
316
352
  </style>
317
353
  </head>
318
- <body>
319
- <div class="min-h-screen flex flex-col">
320
- <header class="bg-[#18181b] border-b border-zinc-800 px-6 py-4">
321
- <div class="max-w-7xl mx-auto flex items-center justify-between">
322
- <div class="flex items-center gap-4">
323
- <div class="w-12 h-12 bg-gradient-to-br from-indigo-500 to-purple-600 rounded-2xl flex items-center justify-center text-2xl shadow-lg shadow-indigo-500/20">⚡</div>
324
- <div>
325
- <h1 class="text-2xl font-bold bg-gradient-to-r from-white to-zinc-400 bg-clip-text text-transparent">OCB</h1>
326
- <p class="text-xs text-zinc-500 font-medium">OpenCode Bridge</p>
327
- </div>
328
- <div class="ml-8 flex items-center gap-2 px-4 py-2 bg-zinc-900 rounded-xl border border-zinc-800">
329
- <div id="healthDot" class="w-2.5 h-2.5 bg-green-500 rounded-full animate-pulse"></div>
330
- <span id="connectionStatus" class="text-sm text-zinc-300">Connected</span>
331
- </div>
332
- </div>
333
- <div class="flex items-center gap-3">
334
- <div class="text-right mr-4">
335
- <p class="text-xs text-zinc-500">Current Model</p>
336
- <p id="currentModelDisplay" class="text-sm font-medium text-white">Loading...</p>
337
- </div>
338
- <button onclick="refreshModels()" class="p-2.5 bg-zinc-800 hover:bg-zinc-700 rounded-xl transition text-zinc-300 hover:text-white" title="Refresh Models">
339
- <svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15"></path></svg>
340
- </button>
341
- </div>
354
+ <body class="overflow-hidden" x-data="app()" x-init="init()">
355
+ <div class="h-14 border-b border-space-border flex items-center px-4 justify-between bg-space-900/80 backdrop-blur-md">
356
+ <div class="flex items-center gap-3">
357
+ <div class="w-8 h-8 rounded-lg bg-gradient-to-br from-neon-purple to-blue-600 flex items-center justify-center text-white font-bold shadow-lg shadow-neon-purple/20">OC</div>
358
+ <div class="flex flex-col">
359
+ <span class="text-sm font-bold tracking-wide text-white">OCB</span>
360
+ <span class="text-[10px] text-gray-500 font-mono">OPENCODE BRIDGE</span>
342
361
  </div>
343
- </header>
344
-
345
- <nav class="bg-[#18181b] border-b border-zinc-800 px-6">
346
- <div class="max-w-7xl mx-auto flex gap-1">
347
- <button onclick="switchTab('models')" id="tab-models" class="px-5 py-3 text-sm font-medium rounded-t-xl transition-all tab-active">Models</button>
348
- <button onclick="switchTab('status')" id="tab-status" class="px-5 py-3 text-sm font-medium rounded-t-xl transition-all text-zinc-400 hover:bg-zinc-800">Status</button>
349
- <button onclick="switchTab('settings')" id="tab-settings" class="px-5 py-3 text-sm font-medium rounded-t-xl transition-all text-zinc-400 hover:bg-zinc-800">Settings</button>
362
+ </div>
363
+ <div class="flex items-center gap-4">
364
+ <div class="flex items-center gap-2 px-3 py-1 rounded-full text-xs font-mono border transition-all" :class="connected ? 'bg-neon-green/10 border-neon-green/20 text-neon-green' : 'bg-red-500/10 border-red-500/20 text-red-400'">
365
+ <div class="w-1.5 h-1.5 rounded-full" :class="connected ? 'bg-neon-green shadow-lg shadow-neon-green/50' : 'bg-red-400'"></div>
366
+ <span x-text="connected ? 'Connected' : 'Disconnected'"></span>
350
367
  </div>
351
- </nav>
352
-
353
- <main class="flex-1 max-w-7xl mx-auto w-full p-6">
354
- <div id="models-view" class="flex gap-6 h-[calc(100vh-220px)]">
355
- <aside class="w-72 bg-[#18181b] rounded-2xl border border-zinc-800 flex flex-col overflow-hidden">
356
- <div class="p-4 border-b border-zinc-800">
357
- <div class="relative">
358
- <input type="text" id="searchInput" placeholder="Search models..." oninput="filterData()" class="w-full px-4 py-2.5 bg-zinc-900 border border-zinc-700 rounded-xl text-sm text-white placeholder-zinc-500 focus:outline-none focus:border-indigo-500 focus:ring-2 focus:ring-indigo-500/20">
359
- </div>
360
- </div>
361
- <div id="providerList" class="flex-1 overflow-y-auto p-2"></div>
362
- </aside>
363
- <section class="flex-1 overflow-y-auto">
364
- <div class="flex items-center justify-between mb-4">
365
- <h2 id="sectionTitle" class="text-lg font-semibold text-white">All Models</h2>
366
- <span id="modelCount" class="text-sm text-zinc-500"></span>
367
- </div>
368
- <div id="modelGrid" class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4"></div>
369
- </section>
368
+ <button @click="refresh()" class="p-2 hover:bg-white/5 rounded-lg transition text-gray-400">
369
+ <svg class="w-4 h-4" :class="{'animate-spin': loading}" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15"></path></svg>
370
+ </button>
371
+ </div>
372
+ </div>
373
+ <div class="flex h-[calc(100vh-56px)]">
374
+ <nav class="w-56 bg-space-900 border-r border-space-border flex flex-col">
375
+ <div class="p-4 space-y-1">
376
+ <button @click="tab='dashboard'" class="nav-item w-full flex items-center gap-3 px-4 py-2.5 rounded-lg text-sm font-medium" :class="tab==='dashboard'?'active':''">
377
+ <svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2H6a2 2 0 01-2-2V6zM14 6a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2h-2a2 2 0 01-2-2V6zM4 16a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2H6a2 2 0 01-2-2v-2zM14 16a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2h-2a2 2 0 01-2-2v-2z"></path></svg>
378
+ Dashboard
379
+ </button>
380
+ <button @click="tab='models'" class="nav-item w-full flex items-center gap-3 px-4 py-2.5 rounded-lg text-sm font-medium" :class="tab==='models'?'active':''">
381
+ <svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M20 7l-8-4-8 4m16 0l-8 4m8-4v10l-8 4m0-10L4 7m8 4v10M4 7v10l8 4"></path></svg>
382
+ Models
383
+ </button>
384
+ <button @click="tab='settings'" class="nav-item w-full flex items-center gap-3 px-4 py-2.5 rounded-lg text-sm font-medium" :class="tab==='settings'?'active':''">
385
+ <svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z"></path><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"></path></svg>
386
+ Settings
387
+ </button>
370
388
  </div>
371
-
372
- <div id="status-view" class="hidden space-y-6">
373
- <div class="grid grid-cols-1 md:grid-cols-3 gap-4">
374
- <div class="bg-[#18181b] rounded-2xl border border-zinc-800 p-6">
375
- <div class="flex items-center gap-3 mb-4">
376
- <div class="w-10 h-10 bg-indigo-500/20 rounded-xl flex items-center justify-center text-indigo-400">📊</div>
377
- <div>
378
- <p class="text-xs text-zinc-500">Total Requests</p>
379
- <p id="totalRequests" class="text-2xl font-bold text-white">0</p>
380
- </div>
389
+ <div class="mt-auto p-4 border-t border-space-border">
390
+ <div class="text-xs text-gray-500 font-mono">
391
+ <div class="flex justify-between mb-1"><span>Port:</span><span class="text-gray-400">8300</span></div>
392
+ <div class="flex justify-between"><span>OpenCode:</span><span class="text-neon-green">localhost:4096</span></div>
393
+ </div>
394
+ </div>
395
+ </nav>
396
+ <main class="flex-1 overflow-auto bg-space-950 p-6">
397
+ <div x-show="tab==='dashboard'" x-transition>
398
+ <div class="grid grid-cols-1 md:grid-cols-3 gap-4 mb-6">
399
+ <div class="bg-space-900 rounded-xl border border-space-border p-5">
400
+ <div class="flex items-center gap-3 mb-3">
401
+ <div class="w-10 h-10 bg-neon-purple/20 rounded-xl flex items-center justify-center text-neon-purple">📊</div>
402
+ <div><p class="text-xs text-gray-500">Total Requests</p><p class="text-2xl font-bold text-white" x-text="stats.requests.toLocaleString()">0</p></div>
381
403
  </div>
382
404
  </div>
383
- <div class="bg-[#18181b] rounded-2xl border border-zinc-800 p-6">
384
- <div class="flex items-center gap-3 mb-4">
385
- <div class="w-10 h-10 bg-purple-500/20 rounded-xl flex items-center justify-center text-purple-400">🎯</div>
386
- <div>
387
- <p class="text-xs text-zinc-500">Total Tokens</p>
388
- <p id="totalTokens" class="text-2xl font-bold text-white">0</p>
389
- </div>
405
+ <div class="bg-space-900 rounded-xl border border-space-border p-5">
406
+ <div class="flex items-center gap-3 mb-3">
407
+ <div class="w-10 h-10 bg-neon-cyan/20 rounded-xl flex items-center justify-center text-neon-cyan">🎯</div>
408
+ <div><p class="text-xs text-gray-500">Total Tokens</p><p class="text-2xl font-bold text-white" x-text="stats.tokens.toLocaleString()">0</p></div>
390
409
  </div>
391
410
  </div>
392
- <div class="bg-[#18181b] rounded-2xl border border-zinc-800 p-6">
393
- <div class="flex items-center gap-3 mb-4">
394
- <div class="w-10 h-10 bg-green-500/20 rounded-xl flex items-center justify-center text-green-400">🔗</div>
395
- <div>
396
- <p class="text-xs text-zinc-500">Session</p>
397
- <p id="sessionStatus" class="text-lg font-semibold text-green-400">Active</p>
398
- </div>
411
+ <div class="bg-space-900 rounded-xl border border-space-border p-5">
412
+ <div class="flex items-center gap-3 mb-3">
413
+ <div class="w-10 h-10 bg-neon-green/20 rounded-xl flex items-center justify-center text-neon-green">🤖</div>
414
+ <div><p class="text-xs text-gray-500">Active Model</p><p class="text-lg font-semibold text-white truncate" x-text="currentModelName">-</p></div>
399
415
  </div>
400
416
  </div>
401
417
  </div>
402
- <div class="bg-[#18181b] rounded-2xl border border-zinc-800 p-6">
403
- <h3 class="text-lg font-semibold text-white mb-4">Model Usage</h3>
404
- <div class="space-y-3">
405
- <div class="flex justify-between items-center py-2 border-b border-zinc-800">
406
- <span class="text-zinc-400">Active Model</span>
407
- <span id="activeModelName" class="text-white font-medium">-</span>
408
- </div>
409
- <div class="flex justify-between items-center py-2 border-b border-zinc-800">
410
- <span class="text-zinc-400">Available Models</span>
411
- <span id="totalModelsDisplay" class="text-white font-medium">-</span>
412
- </div>
413
- <div class="flex justify-between items-center py-2 border-b border-zinc-800">
414
- <span class="text-zinc-400">Providers</span>
415
- <span id="totalProviders" class="text-white font-medium">-</span>
416
- </div>
417
- <div class="flex justify-between items-center py-2">
418
- <span class="text-zinc-400">OpenCode Server</span>
419
- <span class="text-green-400 font-medium">localhost:4096</span>
420
- </div>
418
+ <div class="bg-space-900 rounded-xl border border-space-border p-5">
419
+ <h3 class="text-lg font-semibold text-white mb-4">Current Session</h3>
420
+ <div class="grid grid-cols-2 gap-4 text-sm">
421
+ <div class="flex justify-between py-2 border-b border-space-border"><span class="text-gray-500">Session ID</span><span class="text-gray-300 font-mono text-xs" x-text="stats.sessionId || 'inactive'">-</span></div>
422
+ <div class="flex justify-between py-2 border-b border-space-border"><span class="text-gray-500">Status</span><span class="text-neon-green" x-text="stats.sessionId ? 'Active' : 'Inactive'">-</span></div>
423
+ <div class="flex justify-between py-2 border-b border-space-border"><span class="text-gray-500">Models Available</span><span class="text-gray-300" x-text="stats.models">0</span></div>
424
+ <div class="flex justify-between py-2 border-b border-space-border"><span class="text-gray-500">Providers</span><span class="text-gray-300" x-text="stats.providers">0</span></div>
425
+ </div>
426
+ <div class="mt-4 flex gap-3">
427
+ <button @click="resetSession()" class="px-4 py-2 bg-space-700 hover:bg-space-600 rounded-lg text-sm transition">Reset Session</button>
428
+ <button @click="refreshModels()" class="px-4 py-2 bg-space-700 hover:bg-space-600 rounded-lg text-sm transition">Refresh Models</button>
421
429
  </div>
422
430
  </div>
423
- <div class="flex gap-4">
424
- <button onclick="resetSession()" class="px-6 py-3 bg-zinc-800 hover:bg-zinc-700 rounded-xl text-white font-medium transition">Reset Session</button>
425
- <button onclick="resetStats()" class="px-6 py-3 bg-zinc-800 hover:bg-zinc-700 rounded-xl text-white font-medium transition">Reset Stats</button>
431
+ </div>
432
+ <div x-show="tab==='models'" x-transition style="display:none;">
433
+ <div class="mb-4 flex gap-4">
434
+ <input type="text" x-model="search" @input="filterModels()" placeholder="Search models..." class="flex-1 px-4 py-2.5 bg-space-900 border border-space-border rounded-xl text-white placeholder-gray-500 focus:outline-none focus:border-neon-purple">
435
+ <select x-model="selectedProvider" @change="filterModels()" class="px-4 py-2.5 bg-space-900 border border-space-border rounded-xl text-white focus:outline-none">
436
+ <option value="all">All Providers</option>
437
+ <template x-for="p in providers" :key="p"><option :value="p" x-text="p"></option></template>
438
+ </select>
439
+ </div>
440
+ <div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-3 max-h-[calc(100vh-220px)] overflow-y-auto">
441
+ <template x-for="m in filteredModels" :key="m.id">
442
+ <div @click="selectModel(m.id)" class="p-4 rounded-xl border cursor-pointer transition-all hover:scale-[1.02]" :class="m.id===currentModel?'bg-neon-purple/20 border-neon-purple':'bg-space-900 border-space-border hover:border-space-700'">
443
+ <div class="flex items-start justify-between mb-2">
444
+ <h3 class="font-semibold text-white truncate" x-text="m.name"></h3>
445
+ <span x-show="m.id===currentModel" class="text-xs bg-neon-purple text-white px-2 py-0.5 rounded">Active</span>
446
+ </div>
447
+ <p class="text-xs text-gray-500 font-mono truncate mb-2" x-text="m.id"></p>
448
+ <p class="text-xs text-gray-600" x-text="m.provider"></p>
449
+ </div>
450
+ </template>
426
451
  </div>
427
452
  </div>
428
-
429
- <div id="settings-view" class="hidden space-y-6">
430
- <div class="bg-[#18181b] rounded-2xl border border-zinc-800 p-6">
453
+ <div x-show="tab==='settings'" x-transition style="display:none;">
454
+ <div class="bg-space-900 rounded-xl border border-space-border p-6 mb-4">
431
455
  <h3 class="text-lg font-semibold text-white mb-4">Claude Code Configuration</h3>
432
- <p class="text-zinc-400 text-sm mb-4">Add this to your Claude Code settings to use OCB:</p>
433
- <div class="bg-zinc-950 rounded-xl p-4 font-mono text-sm text-zinc-300 overflow-x-auto">
434
- <pre id="configJson">{
435
- "env": {
436
- "ANTHROPIC_BASE_URL": "http://localhost:8300",
437
- "ANTHROPIC_API_KEY": "test",
438
- "ANTHROPIC_MODEL": "claude-sonnet-4-5"
439
- }
440
- }</pre>
456
+ <p class="text-gray-400 text-sm mb-4">Configure Claude Code to use OCB as the API endpoint:</p>
457
+ <div class="bg-space-950 rounded-xl p-4 font-mono text-sm text-gray-300 overflow-x-auto mb-4">
458
+ <pre x-text="configJson"></pre>
441
459
  </div>
442
- <button onclick="copyConfig()" class="mt-4 px-6 py-2.5 bg-indigo-600 hover:bg-indigo-500 rounded-xl text-white font-medium transition flex items-center gap-2">
443
- <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 16H6a2 2 0 01-2-2V6a2 2 0 012-2h8a2 2 0 012 2v2m-6 12h8a2 2 0 002-2v-8a2 2 0 00-2-2h-8a2 2 0 00-2 2v8a2 2 0 002 2z"></path></svg>
444
- Copy Config
460
+ <button @click="applyToClaude()" class="px-6 py-2.5 bg-neon-purple hover:bg-neon-purple/80 rounded-xl text-white font-medium transition flex items-center gap-2">
461
+ <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"></path></svg>
462
+ Apply to Claude Code
445
463
  </button>
446
464
  </div>
447
- <div class="bg-[#18181b] rounded-2xl border border-zinc-800 p-6">
465
+ <div class="bg-space-900 rounded-xl border border-space-border p-6">
448
466
  <h3 class="text-lg font-semibold text-white mb-4">API Endpoints</h3>
449
467
  <div class="space-y-2 font-mono text-sm">
450
- <div class="flex justify-between py-2 border-b border-zinc-800"><span class="text-zinc-400">Health</span><span class="text-indigo-400">http://localhost:8300/health</span></div>
451
- <div class="flex justify-between py-2 border-b border-zinc-800"><span class="text-zinc-400">Status</span><span class="text-indigo-400">http://localhost:8300/api/status</span></div>
452
- <div class="flex justify-between py-2 border-b border-zinc-800"><span class="text-zinc-400">Models</span><span class="text-indigo-400">http://localhost:8300/api/models</span></div>
453
- <div class="flex justify-between py-2 border-b border-zinc-800"><span class="text-zinc-400">Set Model</span><span class="text-indigo-400">POST /api/model</span></div>
454
- <div class="flex justify-between py-2"><span class="text-zinc-400">Messages</span><span class="text-indigo-400">http://localhost:8300/v1/messages</span></div>
468
+ <div class="flex justify-between py-2 border-b border-space-border"><span class="text-gray-500">Health</span><span class="text-neon-cyan">/health</span></div>
469
+ <div class="flex justify-between py-2 border-b border-space-border"><span class="text-gray-500">Status</span><span class="text-neon-cyan">/api/status</span></div>
470
+ <div class="flex justify-between py-2 border-b border-space-border"><span class="text-gray-500">Models</span><span class="text-neon-cyan">/v1/models</span></div>
471
+ <div class="flex justify-between py-2 border-b border-space-border"><span class="text-gray-500">Messages</span><span class="text-neon-cyan">/v1/messages</span></div>
455
472
  </div>
456
473
  </div>
457
474
  </div>
458
475
  </main>
459
-
460
- <footer class="bg-[#18181b] border-t border-zinc-800 px-6 py-3">
461
- <div class="max-w-7xl mx-auto flex items-center justify-between text-sm text-zinc-500">
462
- <div class="flex items-center gap-6">
463
- <span>OCB v1.0.1</span>
464
- <span>Proxy Port: <span class="text-white">8300</span></span>
465
- </div>
466
- <span>OpenCode Bridge for Claude Code</span>
467
- </div>
468
- </footer>
469
476
  </div>
470
477
  <script>
471
- let allModels = [];
472
- let groupedModels = {};
473
- let currentProvider = 'all';
474
- let currentModelId = null;
475
-
476
- function switchTab(tab) {
477
- document.querySelectorAll('[id^="tab-"]').forEach(el => {
478
- el.classList.remove('tab-active');
479
- el.classList.add('text-zinc-400', 'hover:bg-zinc-800');
480
- });
481
- document.getElementById('tab-' + tab).classList.add('tab-active');
482
- document.getElementById('tab-' + tab).classList.remove('text-zinc-400', 'hover:bg-zinc-800');
483
-
484
- document.getElementById('models-view').classList.add('hidden');
485
- document.getElementById('status-view').classList.add('hidden');
486
- document.getElementById('settings-view').classList.add('hidden');
487
- document.getElementById(tab + '-view').classList.remove('hidden');
488
- }
489
-
490
- function copyConfig() {
491
- const config = document.getElementById('configJson').textContent;
492
- navigator.clipboard.writeText(config);
493
- alert('Config copied to clipboard!');
494
- }
495
-
496
- async function resetStats() {
497
- await fetch('/api/reset-stats', { method: 'POST' });
498
- loadStatus();
499
- }
500
-
501
- async function loadModels() {
502
- const res = await fetch('/api/models');
503
- const data = await res.json();
504
- allModels = data.models || [];
505
- groupedModels = data.grouped || {};
506
- renderProviders();
507
- renderModels(allModels);
508
- updateStats();
509
- }
510
-
511
- function renderProviders() {
512
- const container = document.getElementById('providerList');
513
- const searchTerm = document.getElementById('searchInput').value.toLowerCase();
514
- let providersToShow = Object.entries(groupedModels);
515
- if (searchTerm) {
516
- providersToShow = providersToShow.filter(([name, models]) => name.toLowerCase().includes(searchTerm) || models.some(m => m.name.toLowerCase().includes(searchTerm)));
517
- }
518
- let html = '<div onclick="selectProvider('all')" class="px-3 py-2.5 rounded-xl cursor-pointer flex items-center justify-between text-sm mb-1 ' + (currentProvider === 'all' ? 'bg-indigo-600 text-white' : 'text-zinc-400 hover:bg-zinc-800') + '"><span>🏠 All Models</span><span class="text-xs opacity-60 bg-zinc-800 px-2 py-0.5 rounded-full">' + allModels.length + '</span></div>';
519
- for (const [provider, models] of providersToShow) {
520
- html += '<div onclick="selectProvider(\\'' + provider.replace(/'/g, "\\\\'") + '\\')" class="px-3 py-2.5 rounded-xl cursor-pointer flex items-center justify-between text-sm mb-1 ' + (currentProvider === provider ? 'bg-indigo-600 text-white' : 'text-zinc-400 hover:bg-zinc-800') + '"><span class="truncate">' + provider + '</span><span class="text-xs opacity-60 bg-zinc-800 px-2 py-0.5 rounded-full">' + models.length + '</span></div>';
521
- }
522
- container.innerHTML = html;
523
- }
524
-
525
- function selectProvider(provider) {
526
- currentProvider = provider;
527
- document.getElementById('sectionTitle').textContent = provider === 'all' ? 'All Models' : provider;
528
- renderProviders();
529
- renderModels(provider === 'all' ? allModels : (groupedModels[provider] || []));
530
- }
531
-
532
- function renderModels(models) {
533
- const container = document.getElementById('modelGrid');
534
- const searchTerm = document.getElementById('searchInput').value.toLowerCase();
535
- let filtered = models;
536
- if (searchTerm) {
537
- filtered = models.filter(m => m.name.toLowerCase().includes(searchTerm) || m.id.toLowerCase().includes(searchTerm));
538
- }
539
- document.getElementById('modelCount').textContent = filtered.length + ' models';
540
- if (filtered.length === 0) {
541
- container.innerHTML = '<div class="col-span-full text-center text-zinc-500 py-12">No models found</div>';
542
- return;
543
- }
544
- container.innerHTML = filtered.slice(0, 100).map(m => '<div onclick="selectModel(\\'' + m.id.replace(/'/g, "\\\\'") + '\\')" class="model-card p-5 rounded-2xl border cursor-pointer transition-all ' + (m.id === currentModelId ? 'bg-indigo-600/20 border-indigo-500 shadow-lg shadow-indigo-500/20' : 'bg-[#18181b] border-zinc-800 hover:border-zinc-700') + '"><div class="flex items-start justify-between mb-3"><h3 class="font-semibold text-white truncate text-base">' + m.name + '</h3>' + (m.id === currentModelId ? '<span class="text-xs bg-indigo-500 text-white px-3 py-1 rounded-full font-medium">Active</span>' : '') + '</div><p class="text-xs text-zinc-500 font-mono truncate mb-3">' + m.id + '</p>' + (m.cost ? '<div class="flex items-center gap-2 text-xs text-zinc-400"><span class="bg-zinc-800 px-2 py-1 rounded">$' + m.cost.input + '/M</span><span>→</span><span class="bg-zinc-800 px-2 py-1 rounded">$' + m.cost.output + '/M</span></div>' : '<div class="text-xs text-zinc-600">Free</div>') + '</div>').join('');
545
- }
546
-
547
- function filterData() {
548
- const searchTerm = document.getElementById('searchInput').value.toLowerCase();
549
- if (searchTerm) {
550
- const filtered = allModels.filter(m => m.name.toLowerCase().includes(searchTerm) || m.id.toLowerCase().includes(searchTerm));
551
- document.getElementById('sectionTitle').textContent = 'Search Results (' + filtered.length + ')';
552
- renderModels(filtered);
553
- renderProviders();
554
- } else {
555
- selectProvider(currentProvider);
556
- }
557
- }
558
-
559
- async function selectModel(modelId) {
560
- const res = await fetch('/api/model', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ modelId }) });
561
- const data = await res.json();
562
- if (data.success) {
563
- currentModelId = modelId;
564
- loadStatus();
565
- renderModels(currentProvider === 'all' ? allModels : (groupedModels[currentProvider] || []));
566
- document.getElementById('configJson').textContent = JSON.stringify({ env: { ANTHROPIC_BASE_URL: "http://localhost:8300", ANTHROPIC_API_KEY: "test", ANTHROPIC_MODEL: modelId } }, null, 2);
567
- }
568
- }
569
-
570
- async function loadStatus() {
571
- const res = await fetch('/api/status');
572
- const data = await res.json();
573
- const model = allModels.find(m => m.id === data.currentModel);
574
- document.getElementById('currentModelDisplay').textContent = model?.name || data.currentModel;
575
- document.getElementById('healthDot').className = 'w-2.5 h-2.5 rounded-full animate-pulse ' + (data.sessionId === 'active' ? 'bg-green-500' : 'bg-red-500');
576
- document.getElementById('connectionStatus').textContent = data.sessionId === 'active' ? 'Connected' : 'Disconnected';
577
- document.getElementById('totalRequests').textContent = data.totalRequests.toLocaleString();
578
- document.getElementById('totalTokens').textContent = data.totalTokensUsed.toLocaleString();
579
- document.getElementById('sessionStatus').textContent = data.sessionId === 'active' ? 'Active' : 'Inactive';
580
- document.getElementById('sessionStatus').className = 'text-lg font-semibold ' + (data.sessionId === 'active' ? 'text-green-400' : 'text-red-400');
581
- document.getElementById('activeModelName').textContent = model?.name || data.currentModel;
582
- document.getElementById('totalModelsDisplay').textContent = allModels.length + ' models';
583
- document.getElementById('totalProviders').textContent = Object.keys(groupedModels).length + ' providers';
584
- currentModelId = data.currentModel;
585
- }
586
-
587
- async function resetSession() {
588
- await fetch('/api/reset-session', { method: 'POST' });
589
- loadStatus();
590
- }
591
-
592
- async function refreshModels() {
593
- await fetch('/api/refresh-models', { method: 'POST' });
594
- loadModels();
595
- }
596
-
597
- function updateStats() {
598
- document.getElementById('totalModelsDisplay').textContent = allModels.length + ' models from ' + Object.keys(groupedModels).length + ' providers';
478
+ function app() {
479
+ return {
480
+ tab: 'dashboard',
481
+ loading: false,
482
+ connected: false,
483
+ search: '',
484
+ selectedProvider: 'all',
485
+ models: [],
486
+ providers: [],
487
+ filteredModels: [],
488
+ currentModel: '',
489
+ currentModelName: '',
490
+ stats: { requests: 0, tokens: 0, sessionId: '', models: 0, providers: 0 },
491
+ configJson: '',
492
+ async init() {
493
+ await this.refresh();
494
+ setInterval(() => this.refresh(), 3000);
495
+ },
496
+ async refresh() {
497
+ this.loading = true;
498
+ try {
499
+ const [statusRes, modelsRes] = await Promise.all([fetch('/api/status'), fetch('/api/models')]);
500
+ const status = await statusRes.json();
501
+ const modelsData = await modelsRes.json();
502
+ this.connected = status.sessionId === 'active';
503
+ this.stats = { requests: status.totalRequests, tokens: status.totalTokensUsed, sessionId: status.sessionId, models: modelsData.models?.length || 0, providers: modelsData.providers?.length || 0 };
504
+ this.currentModel = status.currentModel;
505
+ this.models = modelsData.models || [];
506
+ this.providers = [...new Set(this.models.map(m => m.provider))].sort();
507
+ const current = this.models.find(m => m.id === this.currentModel);
508
+ this.currentModelName = current?.name || this.currentModel;
509
+ this.filterModels();
510
+ this.updateConfig();
511
+ } catch (e) { console.error(e); }
512
+ this.loading = false;
513
+ },
514
+ filterModels() {
515
+ let filtered = this.models;
516
+ if (this.selectedProvider !== 'all') filtered = filtered.filter(m => m.provider === this.selectedProvider);
517
+ if (this.search) {
518
+ const s = this.search.toLowerCase();
519
+ filtered = filtered.filter(m => m.name.toLowerCase().includes(s) || m.id.toLowerCase().includes(s));
520
+ }
521
+ this.filteredModels = filtered.slice(0, 100);
522
+ },
523
+ async selectModel(modelId) {
524
+ await fetch('/api/model', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ modelId }) });
525
+ this.currentModel = modelId;
526
+ this.refresh();
527
+ },
528
+ async resetSession() {
529
+ await fetch('/api/reset-session', { method: 'POST' });
530
+ this.refresh();
531
+ },
532
+ async refreshModels() {
533
+ await fetch('/api/refresh-models', { method: 'POST' });
534
+ this.refresh();
535
+ },
536
+ updateConfig() {
537
+ this.configJson = JSON.stringify({ env: { ANTHROPIC_BASE_URL: 'http://localhost:8300', ANTHROPIC_API_KEY: 'test', ANTHROPIC_MODEL: this.currentModel } }, null, 2);
538
+ },
539
+ async applyToClaude() {
540
+ try {
541
+ const response = await fetch('/api/apply-claude-config', { method: 'POST' });
542
+ const data = await response.json();
543
+ alert(data.success ? 'Applied to Claude Code! Restart Claude Code to use.' : 'Failed: ' + data.error);
544
+ } catch (e) { alert('Error: ' + e.message); }
545
+ }
546
+ };
599
547
  }
600
-
601
- loadModels();
602
- loadStatus();
603
- setInterval(loadStatus, 3000);
604
548
  </script>
605
549
  </body>
606
550
  </html>`;