ocb-cli 1.0.9 → 1.2.0

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.
package/dist/cli.js CHANGED
@@ -193,8 +193,81 @@ async function remove() {
193
193
  await unconfigureClaudeCode();
194
194
  log('āœ… Bridge removed from Claude Code!', colors.green);
195
195
  }
196
+ async function listModels() {
197
+ log('\nšŸ“‹ Loading models...', colors.cyan);
198
+ try {
199
+ const res = await fetch('http://localhost:' + PROXY_PORT + '/api/models');
200
+ const data = await res.json();
201
+ const models = data.models || [];
202
+ log(`\nšŸ“Š Found ${models.length} models from ${data.providers?.length || 0} providers\n`, colors.green);
203
+ // Group by provider
204
+ const byProvider = {};
205
+ for (const m of models) {
206
+ if (!byProvider[m.provider])
207
+ byProvider[m.provider] = [];
208
+ byProvider[m.provider].push(m.name);
209
+ }
210
+ // Show first 5 providers with up to 3 models each
211
+ let count = 0;
212
+ for (const [provider, modelNames] of Object.entries(byProvider)) {
213
+ if (count >= 10)
214
+ break;
215
+ log(`\n${colors.cyan}${provider}${colors.reset}:`, colors.blue);
216
+ for (const name of modelNames.slice(0, 5)) {
217
+ log(` - ${name}`, colors.reset);
218
+ }
219
+ count++;
220
+ }
221
+ log('\n\nUse: ocb model <model-id>', colors.yellow);
222
+ log('Example: ocb model 302ai/claude-sonnet-4-5-20250929\n', colors.yellow);
223
+ }
224
+ catch (e) {
225
+ log('\nāŒ Failed to fetch models. Is OCB running?', colors.yellow);
226
+ log(' Run: ocb start', colors.yellow);
227
+ }
228
+ }
229
+ async function setModel(modelId) {
230
+ log(`\nšŸ”„ Switching to model: ${modelId}...`, colors.cyan);
231
+ try {
232
+ const res = await fetch('http://localhost:' + PROXY_PORT + '/api/model', {
233
+ method: 'POST',
234
+ headers: { 'Content-Type': 'application/json' },
235
+ body: JSON.stringify({ modelId })
236
+ });
237
+ const data = await res.json();
238
+ if (data.success) {
239
+ log('āœ… Model switched! Restart Claude Code to use new model.', colors.green);
240
+ }
241
+ else {
242
+ log('āŒ ' + (data.error || 'Failed to switch model'), colors.yellow);
243
+ }
244
+ }
245
+ catch (e) {
246
+ log('\nāŒ Failed to switch model. Is OCB running?', colors.yellow);
247
+ log(' Run: ocb start', colors.yellow);
248
+ }
249
+ }
250
+ async function showStatus() {
251
+ try {
252
+ const res = await fetch('http://localhost:' + PROXY_PORT + '/api/status');
253
+ const data = await res.json();
254
+ log('\n' + '='.repeat(40), colors.blue);
255
+ log('šŸ“Š OCB Status', colors.blue);
256
+ log('='.repeat(40), colors.blue);
257
+ log(`Current Model: ${data.currentModel}`, colors.green);
258
+ log(`Total Requests: ${data.totalRequests}`, colors.reset);
259
+ log(`Total Tokens: ${data.totalTokensUsed}`, colors.reset);
260
+ log(`Session: ${data.sessionId || 'inactive'}`, colors.reset);
261
+ log('='.repeat(40) + '\n', colors.blue);
262
+ }
263
+ catch (e) {
264
+ log('\nāŒ OCB is not running', colors.yellow);
265
+ log(' Run: ocb start\n', colors.yellow);
266
+ }
267
+ }
196
268
  // CLI Commands
197
269
  const command = process.argv[2];
270
+ const args = process.argv.slice(3);
198
271
  switch (command) {
199
272
  case 'setup':
200
273
  setup();
@@ -211,6 +284,22 @@ switch (command) {
211
284
  case 'install':
212
285
  setup().then(() => start());
213
286
  break;
287
+ case 'models':
288
+ listModels();
289
+ break;
290
+ case 'model':
291
+ if (args[0]) {
292
+ setModel(args[0]);
293
+ }
294
+ else {
295
+ log('\nāŒ Please specify a model ID', colors.yellow);
296
+ log(' Usage: ocb model <model-id>', colors.yellow);
297
+ log(' List models: ocb models\n', colors.yellow);
298
+ }
299
+ break;
300
+ case 'status':
301
+ showStatus();
302
+ break;
214
303
  default:
215
304
  log('\nšŸ“– OCB - OpenCode Bridge CLI', colors.blue);
216
305
  log('\nUsage:', colors.cyan);
@@ -218,6 +307,9 @@ switch (command) {
218
307
  log(' ocb start - Start all services', colors.reset);
219
308
  log(' ocb install - Setup + Start (all in one)', colors.reset);
220
309
  log(' ocb stop - Stop all services', colors.reset);
310
+ log(' ocb status - Show current status', colors.reset);
311
+ log(' ocb models - List available models', colors.reset);
312
+ log(' ocb model <id> - Switch to a model', colors.reset);
221
313
  log(' ocb remove - Remove configuration', colors.reset);
222
314
  log('\nOr run with npx:', colors.yellow);
223
315
  log(' npx ocb install\n', colors.yellow);
package/dist/proxy.js CHANGED
@@ -125,12 +125,27 @@ app.use((req, res, next) => {
125
125
  res.header("Access-Control-Allow-Origin", "*");
126
126
  res.header("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS");
127
127
  res.header("Access-Control-Allow-Headers", "Content-Type, Authorization, x-api-key");
128
- res.header("Content-Type", "application/json");
129
128
  if (req.method === "OPTIONS")
130
129
  return res.sendStatus(200);
131
130
  next();
132
131
  });
133
- app.get("/", (req, res) => res.send(generateHTML()));
132
+ app.get("/", (req, res) => {
133
+ res.send(`OCB - OpenCode Bridge
134
+ ========================
135
+
136
+ Status: Running
137
+ Model: ${currentModel}
138
+ Requests: ${totalRequests}
139
+ Tokens: ${totalTokensUsed}
140
+
141
+ CLI Commands:
142
+ ocb status - Show status
143
+ ocb models - List models
144
+ ocb model <id> - Switch model
145
+
146
+ Docs: https://github.com/veokhan/ocb
147
+ `);
148
+ });
134
149
  app.get("/health", (req, res) => {
135
150
  res.json({ status: "ok", timestamp: Date.now(), models: availableModels.length, providers: Object.keys(providers).length });
136
151
  });
@@ -293,239 +308,6 @@ app.post("/v1/messages/count_tokens", (req, res) => {
293
308
  res.json({ tokens: totalTokens });
294
309
  });
295
310
  const PORT = PROXY_PORT;
296
- function generateHTML() {
297
- return `<!DOCTYPE html>
298
- <html lang="en" data-theme="dark" class="dark">
299
- <head>
300
- <meta charset="UTF-8">
301
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
302
- <title>OCB - OpenCode Bridge</title>
303
- <script src="https://cdn.tailwindcss.com"></script>
304
- <script defer src="https://cdn.jsdelivr.net/npm/alpinejs@3.x.x/dist/cdn.min.js"></script>
305
- <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">
306
- <script>
307
- tailwind.config = {
308
- theme: {
309
- extend: {
310
- colors: {
311
- space: { 950: '#0a0a0f', 900: '#121218', 800: '#1a1a24', 700: '#24242e', border: '#2a2a38' },
312
- neon: { purple: '#a855f7', cyan: '#22d3ee', green: '#22c55e', pink: '#ec4899' }
313
- },
314
- fontFamily: { sans: ['Inter', 'sans-serif'], mono: ['JetBrains Mono', 'monospace'] }
315
- }
316
- }
317
- }
318
- </script>
319
- <style>
320
- * { box-sizing: border-box; margin: 0; padding: 0; }
321
- body { font-family: 'Inter', -apple-system, sans-serif; background: #0a0a0f; color: #e4e4e7; min-height: 100vh; }
322
- [x-cloak] { display: none !important; }
323
- ::-webkit-scrollbar { width: 6px; }
324
- ::-webkit-scrollbar-track { background: transparent; }
325
- ::-webkit-scrollbar-thumb { background: #3f3f46; border-radius: 3px; }
326
- .nav-item { transition: all 0.2s; }
327
- .nav-item.active { background: linear-gradient(90deg, rgba(168,85,247,0.2) 0%, transparent 100%); border-left: 2px solid #a855f7; color: white; }
328
- </style>
329
- </head>
330
- <body class="overflow-hidden" x-data="app()" x-init="init()">
331
- <div class="h-14 border-b border-space-border flex items-center px-4 justify-between bg-space-900/80 backdrop-blur-md">
332
- <div class="flex items-center gap-3">
333
- <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>
334
- <div class="flex flex-col">
335
- <span class="text-sm font-bold tracking-wide text-white">OCB</span>
336
- <span class="text-[10px] text-gray-500 font-mono">OPENCODE BRIDGE</span>
337
- </div>
338
- </div>
339
- <div class="flex items-center gap-4">
340
- <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'">
341
- <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>
342
- <span x-text="connected ? 'Connected' : 'Disconnected'"></span>
343
- </div>
344
- <button @click="refresh()" class="p-2 hover:bg-white/5 rounded-lg transition text-gray-400">
345
- <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>
346
- </button>
347
- </div>
348
- </div>
349
- <div class="flex h-[calc(100vh-56px)]">
350
- <nav class="w-56 bg-space-900 border-r border-space-border flex flex-col">
351
- <div class="p-4 space-y-1">
352
- <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':''">
353
- <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>
354
- Dashboard
355
- </button>
356
- <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':''">
357
- <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>
358
- Models
359
- </button>
360
- <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':''">
361
- <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>
362
- Settings
363
- </button>
364
- </div>
365
- <div class="mt-auto p-4 border-t border-space-border">
366
- <div class="text-xs text-gray-500 font-mono">
367
- <div class="flex justify-between mb-1"><span>Port:</span><span class="text-gray-400">8300</span></div>
368
- <div class="flex justify-between"><span>OpenCode:</span><span class="text-neon-green">localhost:4096</span></div>
369
- </div>
370
- </div>
371
- </nav>
372
- <main class="flex-1 overflow-auto bg-space-950 p-6">
373
- <div x-show="tab==='dashboard'" x-transition>
374
- <div class="grid grid-cols-1 md:grid-cols-3 gap-4 mb-6">
375
- <div class="bg-space-900 rounded-xl border border-space-border p-5">
376
- <div class="flex items-center gap-3 mb-3">
377
- <div class="w-10 h-10 bg-neon-purple/20 rounded-xl flex items-center justify-center text-neon-purple">šŸ“Š</div>
378
- <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>
379
- </div>
380
- </div>
381
- <div class="bg-space-900 rounded-xl border border-space-border p-5">
382
- <div class="flex items-center gap-3 mb-3">
383
- <div class="w-10 h-10 bg-neon-cyan/20 rounded-xl flex items-center justify-center text-neon-cyan">šŸŽÆ</div>
384
- <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>
385
- </div>
386
- </div>
387
- <div class="bg-space-900 rounded-xl border border-space-border p-5">
388
- <div class="flex items-center gap-3 mb-3">
389
- <div class="w-10 h-10 bg-neon-green/20 rounded-xl flex items-center justify-center text-neon-green">šŸ¤–</div>
390
- <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>
391
- </div>
392
- </div>
393
- </div>
394
- <div class="bg-space-900 rounded-xl border border-space-border p-5">
395
- <h3 class="text-lg font-semibold text-white mb-4">Current Session</h3>
396
- <div class="grid grid-cols-2 gap-4 text-sm">
397
- <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>
398
- <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>
399
- <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>
400
- <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>
401
- </div>
402
- <div class="mt-4 flex gap-3">
403
- <button @click="resetSession()" class="px-4 py-2 bg-space-700 hover:bg-space-600 rounded-lg text-sm transition">Reset Session</button>
404
- <button @click="refreshModels()" class="px-4 py-2 bg-space-700 hover:bg-space-600 rounded-lg text-sm transition">Refresh Models</button>
405
- </div>
406
- </div>
407
- </div>
408
- <div x-show="tab==='models'" x-transition style="display:none;">
409
- <div class="mb-4 flex gap-4">
410
- <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">
411
- <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">
412
- <option value="all">All Providers</option>
413
- <template x-for="p in providers" :key="p"><option :value="p" x-text="p"></option></template>
414
- </select>
415
- </div>
416
- <div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-3 max-h-[calc(100vh-220px)] overflow-y-auto">
417
- <template x-for="m in filteredModels" :key="m.id">
418
- <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'">
419
- <div class="flex items-start justify-between mb-2">
420
- <h3 class="font-semibold text-white truncate" x-text="m.name"></h3>
421
- <span x-show="m.id===currentModel" class="text-xs bg-neon-purple text-white px-2 py-0.5 rounded">Active</span>
422
- </div>
423
- <p class="text-xs text-gray-500 font-mono truncate mb-2" x-text="m.id"></p>
424
- <p class="text-xs text-gray-600" x-text="m.provider"></p>
425
- </div>
426
- </template>
427
- </div>
428
- </div>
429
- <div x-show="tab==='settings'" x-transition style="display:none;">
430
- <div class="bg-space-900 rounded-xl border border-space-border p-6 mb-4">
431
- <h3 class="text-lg font-semibold text-white mb-4">Claude Code Configuration</h3>
432
- <p class="text-gray-400 text-sm mb-4">Configure Claude Code to use OCB as the API endpoint:</p>
433
- <div class="bg-space-950 rounded-xl p-4 font-mono text-sm text-gray-300 overflow-x-auto mb-4">
434
- <pre x-text="configJson"></pre>
435
- </div>
436
- <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">
437
- <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>
438
- Apply to Claude Code
439
- </button>
440
- </div>
441
- <div class="bg-space-900 rounded-xl border border-space-border p-6">
442
- <h3 class="text-lg font-semibold text-white mb-4">API Endpoints</h3>
443
- <div class="space-y-2 font-mono text-sm">
444
- <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>
445
- <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>
446
- <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>
447
- <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>
448
- </div>
449
- </div>
450
- </div>
451
- </main>
452
- </div>
453
- <script>
454
- function app() {
455
- return {
456
- tab: 'dashboard',
457
- loading: false,
458
- connected: false,
459
- search: '',
460
- selectedProvider: 'all',
461
- models: [],
462
- providers: [],
463
- filteredModels: [],
464
- currentModel: '',
465
- currentModelName: '',
466
- stats: { requests: 0, tokens: 0, sessionId: '', models: 0, providers: 0 },
467
- configJson: '',
468
- async init() {
469
- await this.refresh();
470
- setInterval(() => this.refresh(), 3000);
471
- },
472
- async refresh() {
473
- this.loading = true;
474
- try {
475
- const [statusRes, modelsRes] = await Promise.all([fetch('/api/status'), fetch('/api/models')]);
476
- const status = await statusRes.json();
477
- const modelsData = await modelsRes.json();
478
- this.connected = status.sessionId === 'active';
479
- this.stats = { requests: status.totalRequests, tokens: status.totalTokensUsed, sessionId: status.sessionId, models: modelsData.models?.length || 0, providers: modelsData.providers?.length || 0 };
480
- this.currentModel = status.currentModel;
481
- this.models = modelsData.models || [];
482
- this.providers = [...new Set(this.models.map(m => m.provider))].sort();
483
- const current = this.models.find(m => m.id === this.currentModel);
484
- this.currentModelName = current?.name || this.currentModel;
485
- this.filterModels();
486
- this.updateConfig();
487
- } catch (e) { console.error(e); }
488
- this.loading = false;
489
- },
490
- filterModels() {
491
- let filtered = this.models;
492
- if (this.selectedProvider !== 'all') filtered = filtered.filter(m => m.provider === this.selectedProvider);
493
- if (this.search) {
494
- const s = this.search.toLowerCase();
495
- filtered = filtered.filter(m => m.name.toLowerCase().includes(s) || m.id.toLowerCase().includes(s));
496
- }
497
- this.filteredModels = filtered.slice(0, 100);
498
- },
499
- async selectModel(modelId) {
500
- await fetch('/api/model', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ modelId }) });
501
- this.currentModel = modelId;
502
- this.refresh();
503
- },
504
- async resetSession() {
505
- await fetch('/api/reset-session', { method: 'POST' });
506
- this.refresh();
507
- },
508
- async refreshModels() {
509
- await fetch('/api/refresh-models', { method: 'POST' });
510
- this.refresh();
511
- },
512
- updateConfig() {
513
- this.configJson = JSON.stringify({ env: { ANTHROPIC_BASE_URL: 'http://localhost:8300', ANTHROPIC_API_KEY: 'test', ANTHROPIC_MODEL: this.currentModel } }, null, 2);
514
- },
515
- async applyToClaude() {
516
- try {
517
- const response = await fetch('/api/apply-claude-config', { method: 'POST' });
518
- const data = await response.json();
519
- alert(data.success ? 'Applied to Claude Code! Restart Claude Code to use.' : 'Failed: ' + data.error);
520
- } catch (e) { alert('Error: ' + e.message); }
521
- }
522
- };
523
- }
524
- </script>
525
- </body>
526
- </html>`;
527
- }
528
311
  app.listen(PORT, () => {
529
312
  console.error(`OpenCode Bridge running on http://localhost:${PORT}`);
530
- console.error(`Dashboard: http://localhost:${PORT}`);
531
313
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ocb-cli",
3
- "version": "1.0.9",
3
+ "version": "1.2.0",
4
4
  "description": "OpenCode Bridge - Use OpenCode AI models in Claude Code",
5
5
  "type": "module",
6
6
  "main": "dist/proxy.js",
package/src/cli.ts CHANGED
@@ -241,8 +241,86 @@ async function remove() {
241
241
  log('āœ… Bridge removed from Claude Code!', colors.green);
242
242
  }
243
243
 
244
+ async function listModels() {
245
+ log('\nšŸ“‹ Loading models...', colors.cyan);
246
+
247
+ try {
248
+ const res = await fetch('http://localhost:' + PROXY_PORT + '/api/models');
249
+ const data = await res.json();
250
+ const models = data.models || [];
251
+
252
+ log(`\nšŸ“Š Found ${models.length} models from ${data.providers?.length || 0} providers\n`, colors.green);
253
+
254
+ // Group by provider
255
+ const byProvider: Record<string, string[]> = {};
256
+ for (const m of models) {
257
+ if (!byProvider[m.provider]) byProvider[m.provider] = [];
258
+ byProvider[m.provider].push(m.name);
259
+ }
260
+
261
+ // Show first 5 providers with up to 3 models each
262
+ let count = 0;
263
+ for (const [provider, modelNames] of Object.entries(byProvider)) {
264
+ if (count >= 10) break;
265
+ log(`\n${colors.cyan}${provider}${colors.reset}:`, colors.blue);
266
+ for (const name of modelNames.slice(0, 5)) {
267
+ log(` - ${name}`, colors.reset);
268
+ }
269
+ count++;
270
+ }
271
+
272
+ log('\n\nUse: ocb model <model-id>', colors.yellow);
273
+ log('Example: ocb model 302ai/claude-sonnet-4-5-20250929\n', colors.yellow);
274
+ } catch (e) {
275
+ log('\nāŒ Failed to fetch models. Is OCB running?', colors.yellow);
276
+ log(' Run: ocb start', colors.yellow);
277
+ }
278
+ }
279
+
280
+ async function setModel(modelId: string) {
281
+ log(`\nšŸ”„ Switching to model: ${modelId}...`, colors.cyan);
282
+
283
+ try {
284
+ const res = await fetch('http://localhost:' + PROXY_PORT + '/api/model', {
285
+ method: 'POST',
286
+ headers: { 'Content-Type': 'application/json' },
287
+ body: JSON.stringify({ modelId })
288
+ });
289
+ const data = await res.json();
290
+
291
+ if (data.success) {
292
+ log('āœ… Model switched! Restart Claude Code to use new model.', colors.green);
293
+ } else {
294
+ log('āŒ ' + (data.error || 'Failed to switch model'), colors.yellow);
295
+ }
296
+ } catch (e) {
297
+ log('\nāŒ Failed to switch model. Is OCB running?', colors.yellow);
298
+ log(' Run: ocb start', colors.yellow);
299
+ }
300
+ }
301
+
302
+ async function showStatus() {
303
+ try {
304
+ const res = await fetch('http://localhost:' + PROXY_PORT + '/api/status');
305
+ const data = await res.json();
306
+
307
+ log('\n' + '='.repeat(40), colors.blue);
308
+ log('šŸ“Š OCB Status', colors.blue);
309
+ log('='.repeat(40), colors.blue);
310
+ log(`Current Model: ${data.currentModel}`, colors.green);
311
+ log(`Total Requests: ${data.totalRequests}`, colors.reset);
312
+ log(`Total Tokens: ${data.totalTokensUsed}`, colors.reset);
313
+ log(`Session: ${data.sessionId || 'inactive'}`, colors.reset);
314
+ log('='.repeat(40) + '\n', colors.blue);
315
+ } catch (e) {
316
+ log('\nāŒ OCB is not running', colors.yellow);
317
+ log(' Run: ocb start\n', colors.yellow);
318
+ }
319
+ }
320
+
244
321
  // CLI Commands
245
322
  const command = process.argv[2];
323
+ const args = process.argv.slice(3);
246
324
 
247
325
  switch (command) {
248
326
  case 'setup':
@@ -260,6 +338,21 @@ switch (command) {
260
338
  case 'install':
261
339
  setup().then(() => start());
262
340
  break;
341
+ case 'models':
342
+ listModels();
343
+ break;
344
+ case 'model':
345
+ if (args[0]) {
346
+ setModel(args[0]);
347
+ } else {
348
+ log('\nāŒ Please specify a model ID', colors.yellow);
349
+ log(' Usage: ocb model <model-id>', colors.yellow);
350
+ log(' List models: ocb models\n', colors.yellow);
351
+ }
352
+ break;
353
+ case 'status':
354
+ showStatus();
355
+ break;
263
356
  default:
264
357
  log('\nšŸ“– OCB - OpenCode Bridge CLI', colors.blue);
265
358
  log('\nUsage:', colors.cyan);
@@ -267,6 +360,9 @@ switch (command) {
267
360
  log(' ocb start - Start all services', colors.reset);
268
361
  log(' ocb install - Setup + Start (all in one)', colors.reset);
269
362
  log(' ocb stop - Stop all services', colors.reset);
363
+ log(' ocb status - Show current status', colors.reset);
364
+ log(' ocb models - List available models', colors.reset);
365
+ log(' ocb model <id> - Switch to a model', colors.reset);
270
366
  log(' ocb remove - Remove configuration', colors.reset);
271
367
  log('\nOr run with npx:', colors.yellow);
272
368
  log(' npx ocb install\n', colors.yellow);
package/src/proxy.ts CHANGED
@@ -154,12 +154,27 @@ app.use((req, res, next) => {
154
154
  res.header("Access-Control-Allow-Origin", "*");
155
155
  res.header("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS");
156
156
  res.header("Access-Control-Allow-Headers", "Content-Type, Authorization, x-api-key");
157
- res.header("Content-Type", "application/json");
158
157
  if (req.method === "OPTIONS") return res.sendStatus(200);
159
158
  next();
160
159
  });
161
160
 
162
- app.get("/", (req, res) => res.send(generateHTML()));
161
+ app.get("/", (req, res) => {
162
+ res.send(`OCB - OpenCode Bridge
163
+ ========================
164
+
165
+ Status: Running
166
+ Model: ${currentModel}
167
+ Requests: ${totalRequests}
168
+ Tokens: ${totalTokensUsed}
169
+
170
+ CLI Commands:
171
+ ocb status - Show status
172
+ ocb models - List models
173
+ ocb model <id> - Switch model
174
+
175
+ Docs: https://github.com/veokhan/ocb
176
+ `);
177
+ });
163
178
 
164
179
  app.get("/health", (req, res) => {
165
180
  res.json({ status: "ok", timestamp: Date.now(), models: availableModels.length, providers: Object.keys(providers).length });
@@ -331,240 +346,6 @@ app.post("/v1/messages/count_tokens", (req, res) => {
331
346
 
332
347
  const PORT = PROXY_PORT;
333
348
 
334
- function generateHTML() {
335
- return `<!DOCTYPE html>
336
- <html lang="en" data-theme="dark" class="dark">
337
- <head>
338
- <meta charset="UTF-8">
339
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
340
- <title>OCB - OpenCode Bridge</title>
341
- <script src="https://cdn.tailwindcss.com"></script>
342
- <script defer src="https://cdn.jsdelivr.net/npm/alpinejs@3.x.x/dist/cdn.min.js"></script>
343
- <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">
344
- <script>
345
- tailwind.config = {
346
- theme: {
347
- extend: {
348
- colors: {
349
- space: { 950: '#0a0a0f', 900: '#121218', 800: '#1a1a24', 700: '#24242e', border: '#2a2a38' },
350
- neon: { purple: '#a855f7', cyan: '#22d3ee', green: '#22c55e', pink: '#ec4899' }
351
- },
352
- fontFamily: { sans: ['Inter', 'sans-serif'], mono: ['JetBrains Mono', 'monospace'] }
353
- }
354
- }
355
- }
356
- </script>
357
- <style>
358
- * { box-sizing: border-box; margin: 0; padding: 0; }
359
- body { font-family: 'Inter', -apple-system, sans-serif; background: #0a0a0f; color: #e4e4e7; min-height: 100vh; }
360
- [x-cloak] { display: none !important; }
361
- ::-webkit-scrollbar { width: 6px; }
362
- ::-webkit-scrollbar-track { background: transparent; }
363
- ::-webkit-scrollbar-thumb { background: #3f3f46; border-radius: 3px; }
364
- .nav-item { transition: all 0.2s; }
365
- .nav-item.active { background: linear-gradient(90deg, rgba(168,85,247,0.2) 0%, transparent 100%); border-left: 2px solid #a855f7; color: white; }
366
- </style>
367
- </head>
368
- <body class="overflow-hidden" x-data="app()" x-init="init()">
369
- <div class="h-14 border-b border-space-border flex items-center px-4 justify-between bg-space-900/80 backdrop-blur-md">
370
- <div class="flex items-center gap-3">
371
- <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>
372
- <div class="flex flex-col">
373
- <span class="text-sm font-bold tracking-wide text-white">OCB</span>
374
- <span class="text-[10px] text-gray-500 font-mono">OPENCODE BRIDGE</span>
375
- </div>
376
- </div>
377
- <div class="flex items-center gap-4">
378
- <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'">
379
- <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>
380
- <span x-text="connected ? 'Connected' : 'Disconnected'"></span>
381
- </div>
382
- <button @click="refresh()" class="p-2 hover:bg-white/5 rounded-lg transition text-gray-400">
383
- <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>
384
- </button>
385
- </div>
386
- </div>
387
- <div class="flex h-[calc(100vh-56px)]">
388
- <nav class="w-56 bg-space-900 border-r border-space-border flex flex-col">
389
- <div class="p-4 space-y-1">
390
- <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':''">
391
- <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>
392
- Dashboard
393
- </button>
394
- <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':''">
395
- <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>
396
- Models
397
- </button>
398
- <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':''">
399
- <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>
400
- Settings
401
- </button>
402
- </div>
403
- <div class="mt-auto p-4 border-t border-space-border">
404
- <div class="text-xs text-gray-500 font-mono">
405
- <div class="flex justify-between mb-1"><span>Port:</span><span class="text-gray-400">8300</span></div>
406
- <div class="flex justify-between"><span>OpenCode:</span><span class="text-neon-green">localhost:4096</span></div>
407
- </div>
408
- </div>
409
- </nav>
410
- <main class="flex-1 overflow-auto bg-space-950 p-6">
411
- <div x-show="tab==='dashboard'" x-transition>
412
- <div class="grid grid-cols-1 md:grid-cols-3 gap-4 mb-6">
413
- <div class="bg-space-900 rounded-xl border border-space-border p-5">
414
- <div class="flex items-center gap-3 mb-3">
415
- <div class="w-10 h-10 bg-neon-purple/20 rounded-xl flex items-center justify-center text-neon-purple">šŸ“Š</div>
416
- <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>
417
- </div>
418
- </div>
419
- <div class="bg-space-900 rounded-xl border border-space-border p-5">
420
- <div class="flex items-center gap-3 mb-3">
421
- <div class="w-10 h-10 bg-neon-cyan/20 rounded-xl flex items-center justify-center text-neon-cyan">šŸŽÆ</div>
422
- <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>
423
- </div>
424
- </div>
425
- <div class="bg-space-900 rounded-xl border border-space-border p-5">
426
- <div class="flex items-center gap-3 mb-3">
427
- <div class="w-10 h-10 bg-neon-green/20 rounded-xl flex items-center justify-center text-neon-green">šŸ¤–</div>
428
- <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>
429
- </div>
430
- </div>
431
- </div>
432
- <div class="bg-space-900 rounded-xl border border-space-border p-5">
433
- <h3 class="text-lg font-semibold text-white mb-4">Current Session</h3>
434
- <div class="grid grid-cols-2 gap-4 text-sm">
435
- <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>
436
- <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>
437
- <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>
438
- <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>
439
- </div>
440
- <div class="mt-4 flex gap-3">
441
- <button @click="resetSession()" class="px-4 py-2 bg-space-700 hover:bg-space-600 rounded-lg text-sm transition">Reset Session</button>
442
- <button @click="refreshModels()" class="px-4 py-2 bg-space-700 hover:bg-space-600 rounded-lg text-sm transition">Refresh Models</button>
443
- </div>
444
- </div>
445
- </div>
446
- <div x-show="tab==='models'" x-transition style="display:none;">
447
- <div class="mb-4 flex gap-4">
448
- <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">
449
- <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">
450
- <option value="all">All Providers</option>
451
- <template x-for="p in providers" :key="p"><option :value="p" x-text="p"></option></template>
452
- </select>
453
- </div>
454
- <div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-3 max-h-[calc(100vh-220px)] overflow-y-auto">
455
- <template x-for="m in filteredModels" :key="m.id">
456
- <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'">
457
- <div class="flex items-start justify-between mb-2">
458
- <h3 class="font-semibold text-white truncate" x-text="m.name"></h3>
459
- <span x-show="m.id===currentModel" class="text-xs bg-neon-purple text-white px-2 py-0.5 rounded">Active</span>
460
- </div>
461
- <p class="text-xs text-gray-500 font-mono truncate mb-2" x-text="m.id"></p>
462
- <p class="text-xs text-gray-600" x-text="m.provider"></p>
463
- </div>
464
- </template>
465
- </div>
466
- </div>
467
- <div x-show="tab==='settings'" x-transition style="display:none;">
468
- <div class="bg-space-900 rounded-xl border border-space-border p-6 mb-4">
469
- <h3 class="text-lg font-semibold text-white mb-4">Claude Code Configuration</h3>
470
- <p class="text-gray-400 text-sm mb-4">Configure Claude Code to use OCB as the API endpoint:</p>
471
- <div class="bg-space-950 rounded-xl p-4 font-mono text-sm text-gray-300 overflow-x-auto mb-4">
472
- <pre x-text="configJson"></pre>
473
- </div>
474
- <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">
475
- <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>
476
- Apply to Claude Code
477
- </button>
478
- </div>
479
- <div class="bg-space-900 rounded-xl border border-space-border p-6">
480
- <h3 class="text-lg font-semibold text-white mb-4">API Endpoints</h3>
481
- <div class="space-y-2 font-mono text-sm">
482
- <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>
483
- <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>
484
- <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>
485
- <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>
486
- </div>
487
- </div>
488
- </div>
489
- </main>
490
- </div>
491
- <script>
492
- function app() {
493
- return {
494
- tab: 'dashboard',
495
- loading: false,
496
- connected: false,
497
- search: '',
498
- selectedProvider: 'all',
499
- models: [],
500
- providers: [],
501
- filteredModels: [],
502
- currentModel: '',
503
- currentModelName: '',
504
- stats: { requests: 0, tokens: 0, sessionId: '', models: 0, providers: 0 },
505
- configJson: '',
506
- async init() {
507
- await this.refresh();
508
- setInterval(() => this.refresh(), 3000);
509
- },
510
- async refresh() {
511
- this.loading = true;
512
- try {
513
- const [statusRes, modelsRes] = await Promise.all([fetch('/api/status'), fetch('/api/models')]);
514
- const status = await statusRes.json();
515
- const modelsData = await modelsRes.json();
516
- this.connected = status.sessionId === 'active';
517
- this.stats = { requests: status.totalRequests, tokens: status.totalTokensUsed, sessionId: status.sessionId, models: modelsData.models?.length || 0, providers: modelsData.providers?.length || 0 };
518
- this.currentModel = status.currentModel;
519
- this.models = modelsData.models || [];
520
- this.providers = [...new Set(this.models.map(m => m.provider))].sort();
521
- const current = this.models.find(m => m.id === this.currentModel);
522
- this.currentModelName = current?.name || this.currentModel;
523
- this.filterModels();
524
- this.updateConfig();
525
- } catch (e) { console.error(e); }
526
- this.loading = false;
527
- },
528
- filterModels() {
529
- let filtered = this.models;
530
- if (this.selectedProvider !== 'all') filtered = filtered.filter(m => m.provider === this.selectedProvider);
531
- if (this.search) {
532
- const s = this.search.toLowerCase();
533
- filtered = filtered.filter(m => m.name.toLowerCase().includes(s) || m.id.toLowerCase().includes(s));
534
- }
535
- this.filteredModels = filtered.slice(0, 100);
536
- },
537
- async selectModel(modelId) {
538
- await fetch('/api/model', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ modelId }) });
539
- this.currentModel = modelId;
540
- this.refresh();
541
- },
542
- async resetSession() {
543
- await fetch('/api/reset-session', { method: 'POST' });
544
- this.refresh();
545
- },
546
- async refreshModels() {
547
- await fetch('/api/refresh-models', { method: 'POST' });
548
- this.refresh();
549
- },
550
- updateConfig() {
551
- this.configJson = JSON.stringify({ env: { ANTHROPIC_BASE_URL: 'http://localhost:8300', ANTHROPIC_API_KEY: 'test', ANTHROPIC_MODEL: this.currentModel } }, null, 2);
552
- },
553
- async applyToClaude() {
554
- try {
555
- const response = await fetch('/api/apply-claude-config', { method: 'POST' });
556
- const data = await response.json();
557
- alert(data.success ? 'Applied to Claude Code! Restart Claude Code to use.' : 'Failed: ' + data.error);
558
- } catch (e) { alert('Error: ' + e.message); }
559
- }
560
- };
561
- }
562
- </script>
563
- </body>
564
- </html>`;
565
- }
566
-
567
349
  app.listen(PORT, () => {
568
350
  console.error(`OpenCode Bridge running on http://localhost:${PORT}`);
569
- console.error(`Dashboard: http://localhost:${PORT}`);
570
351
  });