coc-bridge 0.0.0-stage โ 1.0.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/CHANGELOG.md +31 -0
- package/LICENSE +21 -0
- package/README.md +177 -2
- package/bin/coc-bridge.js +360 -0
- package/package.json +54 -4
- package/scripts/doctor.js +126 -0
- package/scripts/setup-claude.js +152 -0
- package/src/server.js +436 -0
- package/test/test-client.js +161 -0
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to coc-bridge are documented here.
|
|
4
|
+
|
|
5
|
+
Format follows [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
|
|
6
|
+
Versioning follows [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
|
+
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
## [1.0.0] โ 2026-10-14
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
- Initial public release ๐
|
|
14
|
+
- Zero-dependency HTTP proxy serving the Anthropic `/v1/messages` and `/v1/models` API
|
|
15
|
+
- Streaming SSE support (`message_start โ content_block_delta โ message_stop`)
|
|
16
|
+
- Non-streaming JSON response support
|
|
17
|
+
- Model discovery endpoint (`GET /v1/models`) for Claude Code's `/model` picker
|
|
18
|
+
- 9 OpenCode free models available out of the box
|
|
19
|
+
- Model alias table: short names (`cat-jam`, `nemotron`, `muse-spark`, etc.) and Claude tier names (`opus`, `sonnet`, `haiku`, `fable`) all resolve correctly
|
|
20
|
+
- Auto-detection of `opencode` binary from `$PATH`, NVM locations, and common paths
|
|
21
|
+
- `coc-bridge setup` โ wraps the `claude` binary to auto-start the bridge and inject all env vars
|
|
22
|
+
- `coc-bridge doctor` โ checks Node.js version, opencode presence, claude wrapper status, bridge health, and log directory
|
|
23
|
+
- `coc-bridge help` โ full CLI help with all env vars and model list
|
|
24
|
+
- 5-minute request timeout with `BRIDGE_TIMEOUT_MS` override
|
|
25
|
+
- 50MB payload cap with `413` response
|
|
26
|
+
- Client disconnect detection โ kills orphan opencode child processes immediately
|
|
27
|
+
- `tool_use` block support in `buildPrompt()` โ preserves full agent tool-call history across turns
|
|
28
|
+
- CORS headers for VS Code extension compatibility
|
|
29
|
+
- `npm test` four-test suite: health, model discovery, non-streaming, streaming SSE
|
|
30
|
+
|
|
31
|
+
[1.0.0]: https://github.com/allannuwamanya/coc-bridge/releases/tag/v1.0.0
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Allan Nuwamanya
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,178 @@
|
|
|
1
|
-
#
|
|
1
|
+
# coc-bridge ๐
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
> **Claude Code โ OpenCode. Zero API keys. All free.**
|
|
4
|
+
|
|
5
|
+
[](https://www.npmjs.com/package/coc-bridge)
|
|
6
|
+
[](LICENSE)
|
|
7
|
+
[](https://nodejs.org)
|
|
8
|
+
|
|
9
|
+
`coc-bridge` is a tiny local proxy that lets **Claude Code** use **OpenCode's free hosted models** โ no Anthropic API key, no billing, no friction.
|
|
10
|
+
|
|
11
|
+
```
|
|
12
|
+
Claude Code โโ( Anthropic format )โโโบ coc-bridge โโโบ opencode run -m <model>
|
|
13
|
+
Claude Code โโโ( SSE stream )โโโ coc-bridge โโโ model output
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
---
|
|
17
|
+
|
|
18
|
+
## โจ Features
|
|
19
|
+
|
|
20
|
+
- **Zero dependencies** โ pure Node.js, nothing to `npm install`
|
|
21
|
+
- **Drop-in replacement** โ Claude Code doesn't know the difference
|
|
22
|
+
- **9 free models** out of the box (Space Bunny, Cat Jam, Nemotron Ultra, Muse Spark, and more)
|
|
23
|
+
- **Full streaming** โ Anthropic SSE events, same as the real API
|
|
24
|
+
- **VS Code model picker** โ custom model names show up in the sidebar dropdown
|
|
25
|
+
- **Auto-start** โ the bridge starts itself when you run `claude`, so you never have to think about it
|
|
26
|
+
- **`coc-bridge doctor`** โ one command checks everything is working
|
|
27
|
+
|
|
28
|
+
---
|
|
29
|
+
|
|
30
|
+
## ๐ Quick start (3 steps)
|
|
31
|
+
|
|
32
|
+
### Step 1 โ Install opencode
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
npm install -g opencode
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
> opencode is the only runtime requirement. coc-bridge itself has zero dependencies.
|
|
39
|
+
|
|
40
|
+
### Step 2 โ Install coc-bridge
|
|
41
|
+
|
|
42
|
+
```bash
|
|
43
|
+
npm install -g coc-bridge
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
### Step 3 โ Run setup
|
|
47
|
+
|
|
48
|
+
```bash
|
|
49
|
+
coc-bridge setup
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
That's it. From now on, running `claude` will automatically start the bridge and use free OpenCode models. ๐
|
|
53
|
+
|
|
54
|
+
---
|
|
55
|
+
|
|
56
|
+
## ๐งช Verify it's working
|
|
57
|
+
|
|
58
|
+
```bash
|
|
59
|
+
coc-bridge doctor
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
```
|
|
63
|
+
coc-bridge doctor ๐ฉบ
|
|
64
|
+
|
|
65
|
+
โ Node.js v24.21.0
|
|
66
|
+
โ opencode 0.3.x
|
|
67
|
+
โ claude wrapper /usr/local/bin/claude (patched โ)
|
|
68
|
+
โ Bridge server http://127.0.0.1:4045 โ
|
|
69
|
+
โ Default model opencode/space-bunny-free
|
|
70
|
+
โ Available models 9 models
|
|
71
|
+
โ Log directory /home/you/.coc-bridge
|
|
72
|
+
|
|
73
|
+
Everything looks great! ๐
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
Or run the full test suite:
|
|
77
|
+
|
|
78
|
+
```bash
|
|
79
|
+
npm test
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
---
|
|
83
|
+
|
|
84
|
+
## ๐๏ธ Available models
|
|
85
|
+
|
|
86
|
+
| Short name | Full OpenCode ID | Best for |
|
|
87
|
+
| :--------------- | :------------------------------------------------ | :------------------------------ |
|
|
88
|
+
| `space-bunny` | `opencode/space-bunny-free` | General coding (default) |
|
|
89
|
+
| `cat-jam` | `opencode/cat-jam-free` | Fast edits & quick answers |
|
|
90
|
+
| `muse-spark` | `opencode/muse-spark-1.3-contributor-free` | Reasoning & architecture |
|
|
91
|
+
| `nemotron` | `opencode/nemotron-3-ultra-free` | Deep reasoning (NVIDIA 500B) |
|
|
92
|
+
| `nemotron-lightning` | `opencode/nemotron-3.5-lightning-free` | Fast NVIDIA model |
|
|
93
|
+
| `ling-flash` | `opencode/ling-3.1-flash-free` | High-throughput tasks |
|
|
94
|
+
| `longcat` | `opencode/longcat-2.5-preview-free` | Long-context tasks |
|
|
95
|
+
| `mimo` | `opencode/mimo-v2.6-flash-free` | Multimodal flash |
|
|
96
|
+
| `big-pickle` | `opencode/big-pickle` | ๐ฅ |
|
|
97
|
+
|
|
98
|
+
**VS Code dropdown** maps to:
|
|
99
|
+
| Claude Code menu | Resolves to |
|
|
100
|
+
| :--------------- | :--------------------------------------------------- |
|
|
101
|
+
| Opus | `opencode/nemotron-3-ultra-free` (Nemotron 3 Ultra) |
|
|
102
|
+
| Sonnet | `opencode/muse-spark-1.3-contributor-free` (Muse Spark 1.3) |
|
|
103
|
+
| Haiku | `opencode/cat-jam-free` (Cat Jam) |
|
|
104
|
+
| Fable | `opencode/ling-3.1-flash-free` (Ling 3.1 Flash) |
|
|
105
|
+
| Custom | `opencode/space-bunny-free` (Space Bunny) |
|
|
106
|
+
|
|
107
|
+
---
|
|
108
|
+
|
|
109
|
+
## โ๏ธ Configuration
|
|
110
|
+
|
|
111
|
+
All configuration is via environment variables โ no config files.
|
|
112
|
+
|
|
113
|
+
| Variable | Default | Description |
|
|
114
|
+
| :------------------- | :----------------------------- | :------------------------------------------- |
|
|
115
|
+
| `BRIDGE_PORT` | `4045` | Port the bridge listens on |
|
|
116
|
+
| `OPENCODE_MODEL` | `opencode/space-bunny-free` | Default model when none is requested |
|
|
117
|
+
| `OPENCODE_BIN` | auto-detected | Path to the `opencode` binary |
|
|
118
|
+
| `BRIDGE_TIMEOUT_MS` | `300000` (5 min) | Max time per request before killing the child process |
|
|
119
|
+
|
|
120
|
+
### Use a custom port
|
|
121
|
+
|
|
122
|
+
```bash
|
|
123
|
+
BRIDGE_PORT=5000 coc-bridge
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
### Use a specific model as default
|
|
127
|
+
|
|
128
|
+
```bash
|
|
129
|
+
OPENCODE_MODEL=opencode/nemotron-3-ultra-free coc-bridge
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
---
|
|
133
|
+
|
|
134
|
+
## ๐ ๏ธ Manual setup (without `coc-bridge setup`)
|
|
135
|
+
|
|
136
|
+
If you prefer not to let the installer touch your `claude` binary:
|
|
137
|
+
|
|
138
|
+
```bash
|
|
139
|
+
# Terminal 1 โ start the bridge
|
|
140
|
+
coc-bridge
|
|
141
|
+
|
|
142
|
+
# Terminal 2 โ set env vars and launch Claude Code
|
|
143
|
+
export ANTHROPIC_BASE_URL="http://127.0.0.1:4045"
|
|
144
|
+
export ANTHROPIC_AUTH_TOKEN="coc-local"
|
|
145
|
+
claude
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
---
|
|
149
|
+
|
|
150
|
+
## ๐๏ธ Project structure
|
|
151
|
+
|
|
152
|
+
```
|
|
153
|
+
coc-bridge/
|
|
154
|
+
โโโ bin/
|
|
155
|
+
โ โโโ coc-bridge.js # CLI entry point
|
|
156
|
+
โโโ src/
|
|
157
|
+
โ โโโ server.js # Bridge proxy server
|
|
158
|
+
โโโ scripts/
|
|
159
|
+
โ โโโ setup-claude.js # Wraps the claude binary
|
|
160
|
+
โ โโโ doctor.js # Health check utility
|
|
161
|
+
โโโ test/
|
|
162
|
+
โ โโโ test-client.js # Test suite
|
|
163
|
+
โโโ CHANGELOG.md
|
|
164
|
+
โโโ CONTRIBUTING.md
|
|
165
|
+
โโโ LICENSE
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
---
|
|
169
|
+
|
|
170
|
+
## ๐ค Contributing
|
|
171
|
+
|
|
172
|
+
See [CONTRIBUTING.md](CONTRIBUTING.md). PRs welcome โ especially new model aliases and better error messages.
|
|
173
|
+
|
|
174
|
+
---
|
|
175
|
+
|
|
176
|
+
## ๐ License
|
|
177
|
+
|
|
178
|
+
[MIT](LICENSE) ยฉ Allan Nuwamanya
|
|
@@ -0,0 +1,360 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* coc-bridge CLI entry point
|
|
4
|
+
*
|
|
5
|
+
* Default behaviour: daemonize the server in the background, confirm it's
|
|
6
|
+
* connected, then return the terminal prompt immediately.
|
|
7
|
+
*
|
|
8
|
+
* Commands:
|
|
9
|
+
* (none / start) Start daemon in background and confirm connection
|
|
10
|
+
* stop Stop the running daemon
|
|
11
|
+
* status Show whether the daemon is running
|
|
12
|
+
* restart Stop then start
|
|
13
|
+
* logs Tail the live log file
|
|
14
|
+
* server Run the server IN the foreground (for debugging)
|
|
15
|
+
* setup Wire Claude Code to auto-start the bridge
|
|
16
|
+
* doctor Health-check the full installation
|
|
17
|
+
* help / --help Show this help
|
|
18
|
+
* --version / -v Show version
|
|
19
|
+
*/
|
|
20
|
+
'use strict';
|
|
21
|
+
|
|
22
|
+
const path = require('path');
|
|
23
|
+
const fs = require('fs');
|
|
24
|
+
const http = require('http');
|
|
25
|
+
const os = require('os');
|
|
26
|
+
const { spawn, execSync } = require('child_process');
|
|
27
|
+
|
|
28
|
+
// โโโ paths โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
29
|
+
const PORT = parseInt(process.env.BRIDGE_PORT || '4045', 10);
|
|
30
|
+
const LOG_DIR = path.join(os.homedir(), '.coc-bridge');
|
|
31
|
+
const LOG_FILE = path.join(LOG_DIR, 'bridge.log');
|
|
32
|
+
const PID_FILE = path.join(LOG_DIR, 'bridge.pid');
|
|
33
|
+
const CONFIG_FILE = path.join(LOG_DIR, 'config.json');
|
|
34
|
+
const SERVER_JS = path.join(__dirname, '..', 'src', 'server.js');
|
|
35
|
+
|
|
36
|
+
function readConfig() {
|
|
37
|
+
try { return JSON.parse(fs.readFileSync(CONFIG_FILE, 'utf8')); } catch (_) { return {}; }
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// โโโ colours โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
41
|
+
const G = '\x1b[32m', Y = '\x1b[33m', R = '\x1b[31m', B = '\x1b[34m';
|
|
42
|
+
const BOLD = '\x1b[1m', DIM = '\x1b[2m', RESET = '\x1b[0m';
|
|
43
|
+
|
|
44
|
+
// โโโ helpers โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
45
|
+
function ensureLogDir() {
|
|
46
|
+
if (!fs.existsSync(LOG_DIR)) fs.mkdirSync(LOG_DIR, { recursive: true });
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function readPid() {
|
|
50
|
+
try { return parseInt(fs.readFileSync(PID_FILE, 'utf8').trim(), 10); } catch (_) { return null; }
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function writePid(pid) {
|
|
54
|
+
ensureLogDir();
|
|
55
|
+
fs.writeFileSync(PID_FILE, String(pid));
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function clearPid() {
|
|
59
|
+
try { fs.unlinkSync(PID_FILE); } catch (_) {}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function isProcessAlive(pid) {
|
|
63
|
+
if (!pid) return false;
|
|
64
|
+
try { process.kill(pid, 0); return true; } catch (_) { return false; }
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function healthCheck() {
|
|
68
|
+
return new Promise(resolve => {
|
|
69
|
+
const req = http.get(`http://127.0.0.1:${PORT}/health`, { timeout: 2000 }, res => {
|
|
70
|
+
let body = '';
|
|
71
|
+
res.on('data', d => body += d);
|
|
72
|
+
res.on('end', () => {
|
|
73
|
+
try { resolve(JSON.parse(body)); } catch (_) { resolve(null); }
|
|
74
|
+
});
|
|
75
|
+
});
|
|
76
|
+
req.on('error', () => resolve(null));
|
|
77
|
+
req.on('timeout', () => { req.destroy(); resolve(null); });
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
async function waitForBridge(timeoutMs = 8000) {
|
|
82
|
+
const start = Date.now();
|
|
83
|
+
while (Date.now() - start < timeoutMs) {
|
|
84
|
+
const health = await healthCheck();
|
|
85
|
+
if (health && health.status === 'ok') return health;
|
|
86
|
+
await new Promise(r => setTimeout(r, 150));
|
|
87
|
+
}
|
|
88
|
+
return null;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// โโโ commands โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
92
|
+
|
|
93
|
+
async function cmdStart() {
|
|
94
|
+
// Already running?
|
|
95
|
+
const existingPid = readPid();
|
|
96
|
+
if (isProcessAlive(existingPid)) {
|
|
97
|
+
const health = await healthCheck();
|
|
98
|
+
if (health) {
|
|
99
|
+
console.log(`\n${G}${BOLD}โ coc-bridge already running${RESET} ${DIM}(pid ${existingPid}, port ${PORT})${RESET}`);
|
|
100
|
+
console.log(` ${DIM}Default model: ${health.defaultModel}${RESET}`);
|
|
101
|
+
console.log(` ${DIM}${health.availableModels.length} models available${RESET}\n`);
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
// PID alive but bridge not responding โ stale, clean up
|
|
105
|
+
clearPid();
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// Spawn the daemon โ stdout/stderr both go to the log file
|
|
109
|
+
ensureLogDir();
|
|
110
|
+
const logFd = fs.openSync(LOG_FILE, 'a');
|
|
111
|
+
const child = spawn(process.execPath, [SERVER_JS], {
|
|
112
|
+
detached: true,
|
|
113
|
+
stdio: ['ignore', logFd, logFd],
|
|
114
|
+
env: { ...process.env, COC_BRIDGE_DAEMON: '1' },
|
|
115
|
+
});
|
|
116
|
+
fs.closeSync(logFd);
|
|
117
|
+
child.unref();
|
|
118
|
+
|
|
119
|
+
writePid(child.pid);
|
|
120
|
+
|
|
121
|
+
process.stdout.write(`\n Starting coc-bridge`);
|
|
122
|
+
const health = await waitForBridge(10000);
|
|
123
|
+
|
|
124
|
+
if (!health) {
|
|
125
|
+
process.stdout.write(`\n${R}${BOLD}โ Bridge failed to start${RESET}\n`);
|
|
126
|
+
console.log(` Check logs: ${LOG_FILE}\n`);
|
|
127
|
+
clearPid();
|
|
128
|
+
process.exit(1);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
console.log(`\r${G}${BOLD}โ coc-bridge connected${RESET} ${DIM}http://127.0.0.1:${PORT} (pid ${child.pid})${RESET}`);
|
|
132
|
+
console.log(` ${DIM}Default model : ${health.defaultModel}${RESET}`);
|
|
133
|
+
console.log(` ${DIM}Models : ${health.availableModels.length} available${RESET}`);
|
|
134
|
+
console.log(` ${DIM}Logs : ${LOG_FILE}${RESET}`);
|
|
135
|
+
console.log(`\n ${DIM}Run ${RESET}${BOLD}coc-bridge stop${RESET}${DIM} to shut it down.${RESET}\n`);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
async function cmdStop() {
|
|
139
|
+
const pid = readPid();
|
|
140
|
+
if (!pid || !isProcessAlive(pid)) {
|
|
141
|
+
console.log(`\n${Y} coc-bridge is not running.${RESET}\n`);
|
|
142
|
+
clearPid();
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
145
|
+
try {
|
|
146
|
+
process.kill(pid, 'SIGTERM');
|
|
147
|
+
// Wait up to 3s for process to die
|
|
148
|
+
for (let i = 0; i < 30; i++) {
|
|
149
|
+
await new Promise(r => setTimeout(r, 100));
|
|
150
|
+
if (!isProcessAlive(pid)) break;
|
|
151
|
+
}
|
|
152
|
+
clearPid();
|
|
153
|
+
console.log(`\n${G} โ coc-bridge stopped.${RESET}\n`);
|
|
154
|
+
} catch (e) {
|
|
155
|
+
console.log(`\n${R} โ Could not stop process ${pid}: ${e.message}${RESET}\n`);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
async function cmdStatus() {
|
|
160
|
+
const pid = readPid();
|
|
161
|
+
const alive = isProcessAlive(pid);
|
|
162
|
+
const health = await healthCheck();
|
|
163
|
+
|
|
164
|
+
console.log('');
|
|
165
|
+
if (alive && health) {
|
|
166
|
+
console.log(`${G}${BOLD} โ coc-bridge is running${RESET}`);
|
|
167
|
+
console.log(` pid : ${pid}`);
|
|
168
|
+
console.log(` port : ${PORT}`);
|
|
169
|
+
console.log(` model : ${health.defaultModel}`);
|
|
170
|
+
console.log(` models : ${health.availableModels.length} available`);
|
|
171
|
+
console.log(` log : ${LOG_FILE}`);
|
|
172
|
+
} else if (alive && !health) {
|
|
173
|
+
console.log(`${Y}${BOLD} โ Process alive (pid ${pid}) but not responding on port ${PORT}${RESET}`);
|
|
174
|
+
} else {
|
|
175
|
+
console.log(`${R}${BOLD} โ coc-bridge is not running${RESET}`);
|
|
176
|
+
console.log(` Run ${BOLD}coc-bridge${RESET} to start it.`);
|
|
177
|
+
}
|
|
178
|
+
console.log('');
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
async function cmdRestart() {
|
|
182
|
+
await cmdStop();
|
|
183
|
+
await new Promise(r => setTimeout(r, 500));
|
|
184
|
+
await cmdStart();
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
function cmdLogs() {
|
|
188
|
+
ensureLogDir();
|
|
189
|
+
if (!fs.existsSync(LOG_FILE)) {
|
|
190
|
+
console.log(`\n${Y} No log file yet. Start the bridge first: coc-bridge${RESET}\n`);
|
|
191
|
+
return;
|
|
192
|
+
}
|
|
193
|
+
console.log(`\n${DIM} Tailing ${LOG_FILE} (Ctrl+C to exit)${RESET}\n`);
|
|
194
|
+
// Print last 20 lines then follow
|
|
195
|
+
try {
|
|
196
|
+
const lines = fs.readFileSync(LOG_FILE, 'utf8').split('\n').slice(-20).join('\n');
|
|
197
|
+
if (lines.trim()) console.log(lines);
|
|
198
|
+
} catch (_) {}
|
|
199
|
+
const tail = spawn('tail', ['-f', LOG_FILE], { stdio: 'inherit' });
|
|
200
|
+
tail.on('exit', () => process.exit(0));
|
|
201
|
+
process.on('SIGINT', () => { tail.kill(); process.exit(0); });
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
function cmdServer() {
|
|
205
|
+
// Foreground mode โ just run the server directly, no daemonizing
|
|
206
|
+
console.log(`\n${B}${BOLD} coc-bridge โ foreground mode${RESET} ${DIM}(Ctrl+C to stop)${RESET}\n`);
|
|
207
|
+
require(SERVER_JS);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
// โโ coc-bridge models โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
211
|
+
function cmdModels() {
|
|
212
|
+
const { spawnSync } = require('child_process');
|
|
213
|
+
const NODE = process.execPath;
|
|
214
|
+
|
|
215
|
+
// Ask the bridge's own health endpoint first (if running)
|
|
216
|
+
http.get(`http://127.0.0.1:${PORT}/v1/models`, { timeout: 2000 }, res => {
|
|
217
|
+
let body = '';
|
|
218
|
+
res.on('data', d => body += d);
|
|
219
|
+
res.on('end', () => {
|
|
220
|
+
try {
|
|
221
|
+
const data = JSON.parse(body);
|
|
222
|
+
printModelList(data.data, '(from running bridge)');
|
|
223
|
+
} catch (_) {
|
|
224
|
+
fetchAndPrint();
|
|
225
|
+
}
|
|
226
|
+
});
|
|
227
|
+
}).on('error', () => fetchAndPrint());
|
|
228
|
+
|
|
229
|
+
function fetchAndPrint() {
|
|
230
|
+
// Bridge not running โ call opencode models directly
|
|
231
|
+
let opencodebin;
|
|
232
|
+
try {
|
|
233
|
+
const { execSync } = require('child_process');
|
|
234
|
+
opencodebin = execSync('which opencode', { stdio: ['ignore', 'pipe', 'ignore'] }).toString().trim();
|
|
235
|
+
} catch (_) {
|
|
236
|
+
const candidates = [
|
|
237
|
+
path.join(os.homedir(), '.nvm', 'versions', 'node', 'v24.21.0', 'bin', 'opencode'),
|
|
238
|
+
path.join(os.homedir(), '.local', 'bin', 'opencode'),
|
|
239
|
+
'/usr/local/bin/opencode',
|
|
240
|
+
];
|
|
241
|
+
opencodebin = candidates.find(c => { try { fs.accessSync(c, fs.constants.X_OK); return true; } catch (_) { return false; } });
|
|
242
|
+
}
|
|
243
|
+
if (!opencodebin) {
|
|
244
|
+
console.log(`\n${R} Cannot find opencode. Install it: npm install -g opencode${RESET}\n`);
|
|
245
|
+
return;
|
|
246
|
+
}
|
|
247
|
+
const r = spawnSync(opencodebin, ['models'], { encoding: 'utf8', timeout: 10000, stdio: ['ignore', 'pipe', 'ignore'] });
|
|
248
|
+
if (r.status !== 0 || !r.stdout) {
|
|
249
|
+
console.log(`\n${R} opencode models failed.${RESET}\n`);
|
|
250
|
+
return;
|
|
251
|
+
}
|
|
252
|
+
const models = r.stdout.trim().split('\n').filter(Boolean).map(id => ({ id, display_name: id }));
|
|
253
|
+
printModelList(models, '(from opencode)');
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
function printModelList(models, source) {
|
|
257
|
+
const cfg = readConfig();
|
|
258
|
+
const current = cfg.defaultModel || 'opencode/space-bunny-free';
|
|
259
|
+
console.log(`\n${BOLD} Available OpenCode models ${DIM}${source}${RESET}\n`);
|
|
260
|
+
for (const m of models) {
|
|
261
|
+
const isDefault = m.id === current;
|
|
262
|
+
const tag = isDefault ? ` ${G}โ current default${RESET}` : '';
|
|
263
|
+
console.log(` ${isDefault ? G + BOLD : DIM}${m.id}${RESET}${tag}`);
|
|
264
|
+
}
|
|
265
|
+
console.log(`\n ${DIM}Set default: ${RESET}${BOLD}coc-bridge use <model-id>${RESET}\n`);
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
// โโ coc-bridge use <model> โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
270
|
+
function cmdUse(modelId) {
|
|
271
|
+
if (!modelId) {
|
|
272
|
+
console.log(`\n${R} Usage: coc-bridge use <model-id>${RESET}`);
|
|
273
|
+
console.log(` Example: coc-bridge use opencode/cat-jam-free\n`);
|
|
274
|
+
console.log(` Run ${BOLD}coc-bridge models${RESET} to see what's available.\n`);
|
|
275
|
+
return;
|
|
276
|
+
}
|
|
277
|
+
ensureLogDir();
|
|
278
|
+
const cfg = readConfig();
|
|
279
|
+
const previous = cfg.defaultModel || 'opencode/space-bunny-free';
|
|
280
|
+
cfg.defaultModel = modelId;
|
|
281
|
+
fs.writeFileSync(CONFIG_FILE, JSON.stringify(cfg, null, 2));
|
|
282
|
+
console.log(`\n${G}${BOLD} โ Default model updated${RESET}`);
|
|
283
|
+
console.log(` ${DIM}Was:${RESET} ${previous}`);
|
|
284
|
+
console.log(` ${DIM}Now:${RESET} ${G}${modelId}${RESET}`);
|
|
285
|
+
console.log(`\n ${DIM}Restart the bridge to apply: ${RESET}${BOLD}coc-bridge restart${RESET}\n`);
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
function showHelp() {
|
|
289
|
+
const pkg = require('../package.json');
|
|
290
|
+
console.log(`
|
|
291
|
+
${BOLD}โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
292
|
+
โ coc-bridge ๐ v${pkg.version} โ Claude โ OpenCode โ
|
|
293
|
+
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ${RESET}
|
|
294
|
+
|
|
295
|
+
${BOLD}USAGE${RESET}
|
|
296
|
+
coc-bridge [command]
|
|
297
|
+
|
|
298
|
+
${BOLD}COMMANDS${RESET}
|
|
299
|
+
${G}(default)${RESET} Start the bridge in the background, return your prompt
|
|
300
|
+
${G}stop${RESET} Stop the running background bridge
|
|
301
|
+
${G}restart${RESET} Stop then start again
|
|
302
|
+
${G}status${RESET} Show whether the bridge is running
|
|
303
|
+
${G}logs${RESET} Tail the live log file
|
|
304
|
+
${G}models${RESET} List all available OpenCode models
|
|
305
|
+
${G}use <id>${RESET} Set the default model (saved to ~/.coc-bridge/config.json)
|
|
306
|
+
${G}server${RESET} Run in the foreground (useful for debugging)
|
|
307
|
+
${G}setup${RESET} Wire Claude Code to auto-start the bridge on login
|
|
308
|
+
${G}doctor${RESET} Check that Node, opencode, and claude are all wired up
|
|
309
|
+
|
|
310
|
+
${BOLD}ENVIRONMENT VARIABLES${RESET}
|
|
311
|
+
BRIDGE_PORT Port to listen on (default: 4045)
|
|
312
|
+
OPENCODE_MODEL Default model (overrides config.json)
|
|
313
|
+
OPENCODE_BIN Path to opencode binary (auto-detected)
|
|
314
|
+
BRIDGE_TIMEOUT_MS Max ms per request (default: 300000)
|
|
315
|
+
|
|
316
|
+
${BOLD}EXAMPLES${RESET}
|
|
317
|
+
coc-bridge # Start daemon, get prompt back
|
|
318
|
+
coc-bridge models # See all models you can use
|
|
319
|
+
coc-bridge use opencode/cat-jam-free # Switch default model
|
|
320
|
+
coc-bridge restart # Apply model change
|
|
321
|
+
coc-bridge stop # Shut it down
|
|
322
|
+
coc-bridge status # Is it running?
|
|
323
|
+
coc-bridge logs # Watch live logs
|
|
324
|
+
coc-bridge setup # One-time Claude Code wiring
|
|
325
|
+
BRIDGE_PORT=5000 coc-bridge # Custom port
|
|
326
|
+
`);
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
// โโโ router โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
330
|
+
const cmd = process.argv[2] || 'start';
|
|
331
|
+
|
|
332
|
+
(async () => {
|
|
333
|
+
switch (cmd) {
|
|
334
|
+
case 'start': await cmdStart(); break;
|
|
335
|
+
case 'stop': await cmdStop(); break;
|
|
336
|
+
case 'status': await cmdStatus(); break;
|
|
337
|
+
case 'restart': await cmdRestart(); break;
|
|
338
|
+
case 'logs': cmdLogs(); break;
|
|
339
|
+
case 'server': cmdServer(); break;
|
|
340
|
+
case 'models': cmdModels(); break;
|
|
341
|
+
case 'use': cmdUse(process.argv[3]); break;
|
|
342
|
+
case 'setup': require(path.join(__dirname, '..', 'scripts', 'setup-claude.js')); break;
|
|
343
|
+
case 'doctor': require(path.join(__dirname, '..', 'scripts', 'doctor.js')); break;
|
|
344
|
+
case '--version':
|
|
345
|
+
case '-v': {
|
|
346
|
+
const pkg = require('../package.json');
|
|
347
|
+
console.log(`coc-bridge v${pkg.version}`);
|
|
348
|
+
break;
|
|
349
|
+
}
|
|
350
|
+
case 'help':
|
|
351
|
+
case '--help':
|
|
352
|
+
case '-h':
|
|
353
|
+
showHelp();
|
|
354
|
+
break;
|
|
355
|
+
default:
|
|
356
|
+
console.log(`\n${R} Unknown command: ${cmd}${RESET}\n Run ${BOLD}coc-bridge help${RESET} for available commands.\n`);
|
|
357
|
+
process.exit(1);
|
|
358
|
+
}
|
|
359
|
+
})();
|
|
360
|
+
|
package/package.json
CHANGED
|
@@ -1,6 +1,56 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "coc-bridge",
|
|
3
|
-
"version": "
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "A zero-dependency local bridge that connects Claude Code to OpenCode's free hosted models โ no API keys required.",
|
|
5
|
+
"main": "src/server.js",
|
|
6
|
+
"bin": {
|
|
7
|
+
"coc-bridge": "./bin/coc-bridge.js",
|
|
8
|
+
"coc-bridge-start": "./bin/coc-bridge.js"
|
|
9
|
+
},
|
|
10
|
+
"scripts": {
|
|
11
|
+
"start": "node src/server.js",
|
|
12
|
+
"dev": "BRIDGE_LOG_LEVEL=debug node src/server.js",
|
|
13
|
+
"test": "node test/test-client.js",
|
|
14
|
+
"install-claude": "node scripts/setup-claude.js",
|
|
15
|
+
"doctor": "node scripts/doctor.js"
|
|
16
|
+
},
|
|
17
|
+
"keywords": [
|
|
18
|
+
"claude-code",
|
|
19
|
+
"opencode",
|
|
20
|
+
"anthropic",
|
|
21
|
+
"proxy",
|
|
22
|
+
"bridge",
|
|
23
|
+
"ai",
|
|
24
|
+
"llm",
|
|
25
|
+
"free",
|
|
26
|
+
"claude",
|
|
27
|
+
"opencode-bridge"
|
|
28
|
+
],
|
|
29
|
+
"author": {
|
|
30
|
+
"name": "Allan Nuwamanya",
|
|
31
|
+
"email": "allan.info.comp@gmail.com",
|
|
32
|
+
"url": "https://github.com/allannuwamanya"
|
|
33
|
+
},
|
|
34
|
+
"license": "MIT",
|
|
35
|
+
"repository": {
|
|
36
|
+
"type": "git",
|
|
37
|
+
"url": "git+https://github.com/allannuwamanya/coc-bridge.git"
|
|
38
|
+
},
|
|
39
|
+
"bugs": {
|
|
40
|
+
"url": "https://github.com/allannuwamanya/coc-bridge/issues"
|
|
41
|
+
},
|
|
42
|
+
"homepage": "https://github.com/allannuwamanya/coc-bridge#readme",
|
|
43
|
+
"engines": {
|
|
44
|
+
"node": ">=18.0.0"
|
|
45
|
+
},
|
|
46
|
+
"files": [
|
|
47
|
+
"bin/",
|
|
48
|
+
"src/",
|
|
49
|
+
"scripts/",
|
|
50
|
+
"test/",
|
|
51
|
+
"README.md",
|
|
52
|
+
"LICENSE",
|
|
53
|
+
"CHANGELOG.md"
|
|
54
|
+
],
|
|
55
|
+
"preferGlobal": true
|
|
56
|
+
}
|