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
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* coc-bridge doctor — checks that everything is properly installed and working
|
|
4
|
+
*/
|
|
5
|
+
'use strict';
|
|
6
|
+
|
|
7
|
+
const { execSync } = require('child_process');
|
|
8
|
+
const http = require('http');
|
|
9
|
+
const os = require('os');
|
|
10
|
+
const path = require('path');
|
|
11
|
+
const fs = require('fs');
|
|
12
|
+
|
|
13
|
+
const GREEN = '\x1b[32m';
|
|
14
|
+
const YELLOW = '\x1b[33m';
|
|
15
|
+
const RED = '\x1b[31m';
|
|
16
|
+
const RESET = '\x1b[0m';
|
|
17
|
+
const BOLD = '\x1b[1m';
|
|
18
|
+
const DIM = '\x1b[2m';
|
|
19
|
+
|
|
20
|
+
function ok(label, value) { console.log(` ${GREEN}✔${RESET} ${label.padEnd(30)} ${DIM}${value || ''}${RESET}`); }
|
|
21
|
+
function warn(label, value) { console.log(` ${YELLOW}⚠${RESET} ${label.padEnd(30)} ${DIM}${value || ''}${RESET}`); }
|
|
22
|
+
function fail(label, value) { console.log(` ${RED}✖${RESET} ${label.padEnd(30)} ${DIM}${value || ''}${RESET}`); }
|
|
23
|
+
|
|
24
|
+
const PORT = parseInt(process.env.BRIDGE_PORT || '4045', 10);
|
|
25
|
+
let issues = 0;
|
|
26
|
+
|
|
27
|
+
console.log(`\n${BOLD}coc-bridge doctor 🩺${RESET}\n`);
|
|
28
|
+
|
|
29
|
+
// ── Node.js version ───────────────────────────────────────────────
|
|
30
|
+
const [major] = process.versions.node.split('.').map(Number);
|
|
31
|
+
if (major >= 18) {
|
|
32
|
+
ok('Node.js', `v${process.versions.node}`);
|
|
33
|
+
} else {
|
|
34
|
+
fail('Node.js', `v${process.versions.node} — need ≥18`);
|
|
35
|
+
issues++;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// ── opencode binary ───────────────────────────────────────────────
|
|
39
|
+
let opencodeVersion;
|
|
40
|
+
try {
|
|
41
|
+
opencodeVersion = execSync('opencode --version 2>/dev/null || true', { stdio: ['ignore', 'pipe', 'ignore'] }).toString().trim();
|
|
42
|
+
if (!opencodeVersion) throw new Error();
|
|
43
|
+
ok('opencode', opencodeVersion);
|
|
44
|
+
} catch (_) {
|
|
45
|
+
// Try known paths
|
|
46
|
+
const candidates = [
|
|
47
|
+
path.join(os.homedir(), '.nvm', 'versions', 'node', 'v24.21.0', 'bin', 'opencode'),
|
|
48
|
+
path.join(os.homedir(), '.local', 'bin', 'opencode'),
|
|
49
|
+
'/usr/local/bin/opencode',
|
|
50
|
+
];
|
|
51
|
+
const found = candidates.find(c => { try { fs.accessSync(c, fs.constants.X_OK); return true; } catch (_) { return false; } });
|
|
52
|
+
if (found) {
|
|
53
|
+
ok('opencode', `found at ${found}`);
|
|
54
|
+
} else {
|
|
55
|
+
fail('opencode', 'not found — run: npm install -g opencode');
|
|
56
|
+
issues++;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// ── claude binary ─────────────────────────────────────────────────
|
|
61
|
+
try {
|
|
62
|
+
const claudeBin = execSync('which claude', { stdio: ['ignore', 'pipe', 'ignore'] }).toString().trim();
|
|
63
|
+
const content = fs.readFileSync(claudeBin, 'utf8');
|
|
64
|
+
if (content.includes('coc-bridge') || content.includes('ANTHROPIC_BASE_URL')) {
|
|
65
|
+
ok('claude wrapper', `${claudeBin} (patched ✓)`);
|
|
66
|
+
} else {
|
|
67
|
+
warn('claude wrapper', `${claudeBin} — not yet patched. Run: coc-bridge setup`);
|
|
68
|
+
}
|
|
69
|
+
} catch (_) {
|
|
70
|
+
fail('claude', 'not found — install Claude Code: https://claude.ai/download');
|
|
71
|
+
issues++;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// ── Bridge server ─────────────────────────────────────────────────
|
|
75
|
+
function checkBridge() {
|
|
76
|
+
return new Promise(resolve => {
|
|
77
|
+
const req = http.get(`http://127.0.0.1:${PORT}/health`, { timeout: 3000 }, res => {
|
|
78
|
+
let body = '';
|
|
79
|
+
res.on('data', d => body += d);
|
|
80
|
+
res.on('end', () => {
|
|
81
|
+
try {
|
|
82
|
+
const data = JSON.parse(body);
|
|
83
|
+
if (data.status === 'ok') {
|
|
84
|
+
ok('Bridge server', `http://127.0.0.1:${PORT} ✓`);
|
|
85
|
+
ok('Default model', data.defaultModel);
|
|
86
|
+
ok('Available models', `${data.availableModels.length} models`);
|
|
87
|
+
} else {
|
|
88
|
+
warn('Bridge server', 'running but returned unexpected response');
|
|
89
|
+
}
|
|
90
|
+
} catch (_) {
|
|
91
|
+
warn('Bridge server', 'running but returned non-JSON');
|
|
92
|
+
}
|
|
93
|
+
resolve(true);
|
|
94
|
+
});
|
|
95
|
+
});
|
|
96
|
+
req.on('error', () => {
|
|
97
|
+
warn('Bridge server', `not running on :${PORT} — start it with: coc-bridge`);
|
|
98
|
+
resolve(false);
|
|
99
|
+
});
|
|
100
|
+
req.on('timeout', () => {
|
|
101
|
+
warn('Bridge server', `timeout on :${PORT}`);
|
|
102
|
+
req.destroy();
|
|
103
|
+
resolve(false);
|
|
104
|
+
});
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// ── Log directory ─────────────────────────────────────────────────
|
|
109
|
+
const logDir = path.join(os.homedir(), '.coc-bridge');
|
|
110
|
+
if (fs.existsSync(logDir)) {
|
|
111
|
+
ok('Log directory', logDir);
|
|
112
|
+
} else {
|
|
113
|
+
warn('Log directory', `${logDir} missing — run: coc-bridge setup`);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
checkBridge().then(bridgeUp => {
|
|
117
|
+
console.log('');
|
|
118
|
+
if (issues === 0 && bridgeUp) {
|
|
119
|
+
console.log(`${GREEN}${BOLD}Everything looks great! 🎉${RESET}\n`);
|
|
120
|
+
} else if (issues === 0) {
|
|
121
|
+
console.log(`${YELLOW}${BOLD}Almost there — start the bridge with: coc-bridge${RESET}\n`);
|
|
122
|
+
} else {
|
|
123
|
+
console.log(`${RED}${BOLD}${issues} issue(s) found — fix them and re-run: coc-bridge doctor${RESET}\n`);
|
|
124
|
+
process.exit(1);
|
|
125
|
+
}
|
|
126
|
+
});
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* coc-bridge setup — wires Claude Code's `claude` wrapper to auto-start the bridge
|
|
4
|
+
* and point ANTHROPIC_BASE_URL at localhost:4045
|
|
5
|
+
*/
|
|
6
|
+
'use strict';
|
|
7
|
+
|
|
8
|
+
const fs = require('fs');
|
|
9
|
+
const path = require('path');
|
|
10
|
+
const os = require('os');
|
|
11
|
+
const { execSync } = require('child_process');
|
|
12
|
+
|
|
13
|
+
const GREEN = '\x1b[32m';
|
|
14
|
+
const YELLOW = '\x1b[33m';
|
|
15
|
+
const RED = '\x1b[31m';
|
|
16
|
+
const RESET = '\x1b[0m';
|
|
17
|
+
const BOLD = '\x1b[1m';
|
|
18
|
+
|
|
19
|
+
function ok(msg) { console.log(`${GREEN} ✔${RESET} ${msg}`); }
|
|
20
|
+
function warn(msg) { console.log(`${YELLOW} ⚠${RESET} ${msg}`); }
|
|
21
|
+
function fail(msg) { console.log(`${RED} ✖${RESET} ${msg}`); }
|
|
22
|
+
function step(msg) { console.log(`\n${BOLD}${msg}${RESET}`); }
|
|
23
|
+
|
|
24
|
+
// ─────────────────────────────────────────────
|
|
25
|
+
// 1. Find the claude binary
|
|
26
|
+
// ─────────────────────────────────────────────
|
|
27
|
+
step('1. Finding claude binary...');
|
|
28
|
+
|
|
29
|
+
let claudeBin;
|
|
30
|
+
try {
|
|
31
|
+
claudeBin = execSync('which claude', { stdio: ['ignore', 'pipe', 'ignore'] }).toString().trim();
|
|
32
|
+
ok(`Found: ${claudeBin}`);
|
|
33
|
+
} catch (_) {
|
|
34
|
+
fail('claude not found in PATH. Install Claude Code first:');
|
|
35
|
+
console.log(' https://claude.ai/download');
|
|
36
|
+
process.exit(1);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// ─────────────────────────────────────────────
|
|
40
|
+
// 2. Find the coc-bridge bin (this package)
|
|
41
|
+
// ─────────────────────────────────────────────
|
|
42
|
+
step('2. Locating coc-bridge...');
|
|
43
|
+
|
|
44
|
+
let bridgeBin;
|
|
45
|
+
try {
|
|
46
|
+
bridgeBin = execSync('which coc-bridge', { stdio: ['ignore', 'pipe', 'ignore'] }).toString().trim();
|
|
47
|
+
ok(`Found: ${bridgeBin}`);
|
|
48
|
+
} catch (_) {
|
|
49
|
+
// Fallback: use the absolute path of this script's package
|
|
50
|
+
bridgeBin = path.resolve(__dirname, '..', 'bin', 'coc-bridge.js');
|
|
51
|
+
warn(`coc-bridge not in PATH, will use: ${bridgeBin}`);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// ─────────────────────────────────────────────
|
|
55
|
+
// 3. Backup existing claude wrapper if it's already a wrapper
|
|
56
|
+
// ─────────────────────────────────────────────
|
|
57
|
+
step('3. Wrapping claude binary...');
|
|
58
|
+
|
|
59
|
+
const claudeBinReal = claudeBin + '-bin';
|
|
60
|
+
|
|
61
|
+
// Check if claude is already a shell wrapper (not the binary itself)
|
|
62
|
+
let alreadyWrapped = false;
|
|
63
|
+
try {
|
|
64
|
+
const content = fs.readFileSync(claudeBin, 'utf8');
|
|
65
|
+
if (content.includes('coc-bridge') || content.includes('opencode-claude-bridge')) {
|
|
66
|
+
alreadyWrapped = true;
|
|
67
|
+
}
|
|
68
|
+
} catch (_) {}
|
|
69
|
+
|
|
70
|
+
if (!alreadyWrapped) {
|
|
71
|
+
// Only move the original binary if -bin doesn't already exist
|
|
72
|
+
if (!fs.existsSync(claudeBinReal)) {
|
|
73
|
+
fs.copyFileSync(claudeBin, claudeBinReal);
|
|
74
|
+
fs.chmodSync(claudeBinReal, 0o755);
|
|
75
|
+
ok(`Backed up real binary → ${claudeBinReal}`);
|
|
76
|
+
} else {
|
|
77
|
+
ok(`Backup already exists at ${claudeBinReal}`);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// Write the wrapper
|
|
81
|
+
const wrapperContent = `#!/usr/bin/env bash
|
|
82
|
+
# Generated by coc-bridge setup — do not edit manually
|
|
83
|
+
# Re-run: coc-bridge setup
|
|
84
|
+
|
|
85
|
+
BRIDGE_PORT=\${BRIDGE_PORT:-4045}
|
|
86
|
+
BRIDGE_BIN="${bridgeBin}"
|
|
87
|
+
|
|
88
|
+
# Auto-start coc-bridge if not already running
|
|
89
|
+
if ! curl -sf "http://127.0.0.1:\${BRIDGE_PORT}/health" > /dev/null 2>&1; then
|
|
90
|
+
nohup node "\${BRIDGE_BIN}" >> "\${HOME}/.coc-bridge/bridge.log" 2>&1 &
|
|
91
|
+
disown
|
|
92
|
+
# Wait up to 3 seconds for startup
|
|
93
|
+
for i in $(seq 1 60); do
|
|
94
|
+
sleep 0.05
|
|
95
|
+
if curl -sf "http://127.0.0.1:\${BRIDGE_PORT}/health" > /dev/null 2>&1; then break; fi
|
|
96
|
+
done
|
|
97
|
+
fi
|
|
98
|
+
|
|
99
|
+
# Point Claude Code to the bridge
|
|
100
|
+
export ANTHROPIC_BASE_URL="http://127.0.0.1:\${BRIDGE_PORT}"
|
|
101
|
+
export ANTHROPIC_AUTH_TOKEN="coc-local"
|
|
102
|
+
export ANTHROPIC_API_KEY=""
|
|
103
|
+
|
|
104
|
+
# Model picker labels
|
|
105
|
+
export ANTHROPIC_MODEL="\${ANTHROPIC_MODEL:-opencode/space-bunny-free}"
|
|
106
|
+
export ANTHROPIC_CUSTOM_MODEL_OPTION_NAME="Space Bunny"
|
|
107
|
+
export ANTHROPIC_DEFAULT_OPUS_MODEL="opencode/nemotron-3-ultra-free"
|
|
108
|
+
export ANTHROPIC_DEFAULT_OPUS_MODEL_NAME="Nemotron 3 Ultra"
|
|
109
|
+
export ANTHROPIC_DEFAULT_SONNET_MODEL="opencode/muse-spark-1.3-contributor-free"
|
|
110
|
+
export ANTHROPIC_DEFAULT_SONNET_MODEL_NAME="Muse Spark 1.3"
|
|
111
|
+
export ANTHROPIC_DEFAULT_HAIKU_MODEL="opencode/cat-jam-free"
|
|
112
|
+
export ANTHROPIC_DEFAULT_HAIKU_MODEL_NAME="Cat Jam"
|
|
113
|
+
export ANTHROPIC_DEFAULT_FABLE_MODEL="opencode/ling-3.1-flash-free"
|
|
114
|
+
export ANTHROPIC_DEFAULT_FABLE_MODEL_NAME="Ling 3.1 Flash"
|
|
115
|
+
|
|
116
|
+
export CLAUDE_CODE_DISABLE_UNKNOWN_MODEL_WINDOW_ENFORCEMENT=1
|
|
117
|
+
export CLAUDE_CODE_ENABLE_GATEWAY_MODEL_DISCOVERY=1
|
|
118
|
+
|
|
119
|
+
exec "${claudeBinReal}" "\$@"
|
|
120
|
+
`;
|
|
121
|
+
|
|
122
|
+
fs.writeFileSync(claudeBin, wrapperContent, { mode: 0o755 });
|
|
123
|
+
ok('Wrapper written to: ' + claudeBin);
|
|
124
|
+
} else {
|
|
125
|
+
ok('Wrapper already installed.');
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// ─────────────────────────────────────────────
|
|
129
|
+
// 4. Create log directory
|
|
130
|
+
// ─────────────────────────────────────────────
|
|
131
|
+
step('4. Creating log directory...');
|
|
132
|
+
const logDir = path.join(os.homedir(), '.coc-bridge');
|
|
133
|
+
if (!fs.existsSync(logDir)) {
|
|
134
|
+
fs.mkdirSync(logDir, { recursive: true });
|
|
135
|
+
ok(`Created ${logDir}`);
|
|
136
|
+
} else {
|
|
137
|
+
ok(`Log dir exists: ${logDir}`);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// ─────────────────────────────────────────────
|
|
141
|
+
// Done
|
|
142
|
+
// ─────────────────────────────────────────────
|
|
143
|
+
console.log(`
|
|
144
|
+
${GREEN}${BOLD}✅ Setup complete!${RESET}
|
|
145
|
+
|
|
146
|
+
Run ${BOLD}claude${RESET} as normal — coc-bridge will auto-start in the background.
|
|
147
|
+
|
|
148
|
+
Verify anytime with:
|
|
149
|
+
${BOLD}coc-bridge doctor${RESET}
|
|
150
|
+
|
|
151
|
+
Logs: ${logDir}/bridge.log
|
|
152
|
+
`);
|