claude-bridge-cli 2.0.12 → 2.0.18

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 (5) hide show
  1. package/README.md +74 -74
  2. package/bin/cli.js +391 -391
  3. package/lib/bridge.js +1147 -1037
  4. package/lib/relay-client.js +152 -152
  5. package/package.json +34 -26
package/bin/cli.js CHANGED
@@ -1,391 +1,391 @@
1
- #!/usr/bin/env node
2
- "use strict";
3
-
4
- const { parseArgs } = require("node:util");
5
- const fs = require("node:fs");
6
- const path = require("node:path");
7
- const os = require("node:os");
8
- const { startBridge } = require("../lib/bridge");
9
- const { startRelay } = require("../lib/relay-client");
10
-
11
- // ── Pair flow (browser-OTP based auth) ────────────────────────────────────
12
- // When --token isn't supplied, the CLI:
13
- // 1. POSTs /pair-cli/start to the relay's HTTP base (public endpoint)
14
- // 2. Prints the short code + pair URL — user opens it, OTPs into CF Access,
15
- // confirms in the browser
16
- // 3. Long-polls /pair-cli/claim until the bearer arrives
17
- // 4. Saves the bearer to ~/.claude-bridge/auth.json so subsequent starts
18
- // pick it up automatically
19
- // Replaces the old workflow of running `claude-relay-ctl provision <m>` on
20
- // the host and copy-pasting a long bearer onto the CLI command line.
21
-
22
- function authFilePath() {
23
- return path.join(os.homedir(), ".claude-bridge", "auth.json");
24
- }
25
-
26
- function readAuthFile() {
27
- try { return JSON.parse(fs.readFileSync(authFilePath(), "utf8")); }
28
- catch { return { bearers: {} }; }
29
- }
30
-
31
- function writeAuthFile(data) {
32
- const fp = authFilePath();
33
- fs.mkdirSync(path.dirname(fp), { recursive: true });
34
- fs.writeFileSync(fp, JSON.stringify(data, null, 2), { mode: 0o600 });
35
- }
36
-
37
- function relayHttpBase(wsUrl) {
38
- // wss://host/agent/ws → https://host
39
- const u = new URL(wsUrl);
40
- const proto = u.protocol === "wss:" ? "https:" : "http:";
41
- return `${proto}//${u.host}`;
42
- }
43
-
44
- async function pairInteractive(relayWsUrl, machine, ephemeral) {
45
- const base = relayHttpBase(relayWsUrl);
46
- process.stdout.write(`[bridge] Starting pair flow for "${machine}" against ${base}\n`);
47
- if (ephemeral) process.stdout.write(`[bridge] Ephemeral mode — bearer will NOT be saved to disk.\n`);
48
-
49
- const startResp = await fetch(`${base}/pair-cli/start`, {
50
- method: "POST",
51
- headers: { "Content-Type": "application/json" },
52
- body: JSON.stringify({ machine }),
53
- });
54
- if (!startResp.ok) {
55
- const text = await startResp.text();
56
- throw new Error(`pair start failed (${startResp.status}): ${text}`);
57
- }
58
- const { code, poll_token, pair_url, expires_in } = await startResp.json();
59
-
60
- console.log("\n ┌─────────────────────────────────────────────────┐");
61
- console.log(" │ Open this URL in a browser to pair the device │");
62
- console.log(" ├─────────────────────────────────────────────────┤");
63
- console.log(` │ ${pair_url.padEnd(47)} │`);
64
- console.log(" │ │");
65
- console.log(` │ Confirm this code matches: ${code.padEnd(17)}│`);
66
- console.log(" └─────────────────────────────────────────────────┘");
67
- console.log(` Code expires in ${expires_in}s. Waiting for confirmation…\n`);
68
-
69
- const deadline = Date.now() + expires_in * 1000;
70
- while (Date.now() < deadline) {
71
- let resp;
72
- try {
73
- resp = await fetch(`${base}/pair-cli/claim`, {
74
- method: "POST",
75
- headers: { "Content-Type": "application/json" },
76
- body: JSON.stringify({ poll_token }),
77
- });
78
- } catch (e) {
79
- await new Promise(r => setTimeout(r, 2000));
80
- continue;
81
- }
82
- if (resp.status === 202) continue; // still waiting, re-poll
83
- if (resp.status === 404 || resp.status === 410) {
84
- throw new Error("pair expired or invalidated — re-run to start over");
85
- }
86
- if (!resp.ok) {
87
- const text = await resp.text();
88
- throw new Error(`pair claim failed (${resp.status}): ${text}`);
89
- }
90
- const { bearer, machine: confirmed } = await resp.json();
91
- if (ephemeral) {
92
- console.log(`[bridge] ✓ Paired as "${confirmed}" (in-memory only, nothing written to disk).`);
93
- } else {
94
- console.log(`[bridge] ✓ Paired as "${confirmed}". Bearer saved to ${authFilePath()}`);
95
- const auth = readAuthFile();
96
- auth.bearers = auth.bearers || {};
97
- auth.bearers[base] = auth.bearers[base] || {};
98
- auth.bearers[base][confirmed] = bearer;
99
- writeAuthFile(auth);
100
- }
101
- return bearer;
102
- }
103
- throw new Error("pair window expired — re-run to start over");
104
- }
105
-
106
- function lookupSavedBearer(relayWsUrl, machine) {
107
- const auth = readAuthFile();
108
- return auth.bearers?.[relayHttpBase(relayWsUrl)]?.[machine] || null;
109
- }
110
-
111
- const HELP = `
112
- claude-code-bridge — Bridge server for Claude Code CLI
113
-
114
- Usage:
115
- claude-code-bridge start [options] Start the bridge (+ optional relay)
116
- claude-code-bridge install-service Register as a system service (auto-start on boot)
117
- claude-code-bridge uninstall-service Remove the system service
118
- claude-code-bridge --help Show this help
119
- claude-code-bridge --version Print the installed version
120
-
121
- Options:
122
- --port <n> HTTP port for the local bridge (default: 8091)
123
- --host <ip> Bind address (default: 127.0.0.1)
124
- --token <secret> Bridge password (use the same in the extension). Auto-generated if omitted.
125
- --cwd <path> Default working directory for Claude (default: current dir)
126
- --claude-bin <path> Path to claude CLI binary (default: auto-detect)
127
- --timeout <seconds> Max time for a single Claude call (default: 7200)
128
-
129
- Relay options (connect to a remote relay server):
130
- --relay-url <url> WebSocket URL of the relay server
131
- --machine <name> Machine name for the relay
132
- --token <bearer> Machine bearer (skips both auth.json and pair flow)
133
- --save-auth Cache the bearer in ~/.claude-bridge/auth.json so the
134
- next start reuses it. Required for unattended /
135
- systemd-service installs. WITHOUT this flag the CLI
136
- re-pairs (browser OTP) on every start — the default.
137
- --cf-id <id> Cloudflare Access Client ID (optional)
138
- --cf-secret <secret> Cloudflare Access Client Secret (optional)
139
-
140
- Environment variables:
141
- All options can be set via env vars with BRIDGE_ prefix:
142
- BRIDGE_PORT, BRIDGE_HOST, BRIDGE_CWD, BRIDGE_TIMEOUT,
143
- BRIDGE_RELAY_URL, BRIDGE_MACHINE_NAME, BRIDGE_MACHINE_TOKEN,
144
- BRIDGE_SAVE_AUTH=1 (cache the bearer to ~/.claude-bridge/auth.json
145
- for unattended restarts; default re-pairs every
146
- start so nothing is persisted),
147
- BRIDGE_CF_ID, BRIDGE_CF_SECRET
148
- `;
149
-
150
- function env(key, def) {
151
- return process.env["BRIDGE_" + key] || def;
152
- }
153
-
154
- function main() {
155
- const args = process.argv.slice(2);
156
- if (args.includes("--version") || args.includes("-v") || args[0] === "version") {
157
- console.log(require("../package.json").version);
158
- process.exit(0);
159
- }
160
- if (args.includes("--help") || args.includes("-h") || args.length === 0) {
161
- console.log(HELP);
162
- process.exit(0);
163
- }
164
-
165
- const command = args[0];
166
- if (command === "start") {
167
- const { values } = parseArgs({
168
- args: args.slice(1),
169
- options: {
170
- port: { type: "string", default: env("PORT", "8091") },
171
- host: { type: "string", default: env("HOST", "127.0.0.1") },
172
- cwd: { type: "string", default: env("CWD", process.cwd()) },
173
- "claude-bin": { type: "string", default: env("CLAUDE_BIN", "") },
174
- timeout: { type: "string", default: env("TIMEOUT", "7200") },
175
- "relay-url": { type: "string", default: env("RELAY_URL", "") },
176
- machine: { type: "string", default: env("MACHINE_NAME", "") },
177
- token: { type: "string", default: env("MACHINE_TOKEN", "") },
178
- "save-auth":{ type: "boolean", default: env("SAVE_AUTH", "") === "1" },
179
- ephemeral: { type: "boolean", default: false }, // deprecated, now default behavior — kept as a no-op for compatibility
180
- "cf-id": { type: "string", default: env("CF_ID", "") },
181
- "cf-secret": { type: "string", default: env("CF_SECRET", "") },
182
- },
183
- strict: false,
184
- });
185
-
186
- const config = {
187
- port: parseInt(values.port, 10),
188
- host: values.host,
189
- cwd: values.cwd,
190
- claudeBin: values["claude-bin"],
191
- timeout: parseInt(values.timeout, 10) * 1000,
192
- relay: values["relay-url"] ? {
193
- url: values["relay-url"],
194
- machine: values.machine,
195
- token: values.token,
196
- // Default = ephemeral (pair on every start, never persist). Opt in
197
- // to caching with --save-auth when running unattended / as a service.
198
- // The legacy --ephemeral flag is a no-op (kept for back-compat).
199
- saveAuth: !!values["save-auth"],
200
- cfId: values["cf-id"],
201
- cfSecret: values["cf-secret"],
202
- } : null,
203
- };
204
-
205
- const crypto = require("node:crypto");
206
- if (config.relay) {
207
- config.bearerToken = crypto.randomBytes(32).toString("hex");
208
- } else {
209
- config.bearerToken = values.token || crypto.randomBytes(32).toString("hex");
210
- }
211
-
212
- run(config);
213
- } else if (command === "install-service") {
214
- installService();
215
- } else if (command === "uninstall-service") {
216
- uninstallService();
217
- } else {
218
- console.error(`Unknown command: ${command}\nRun claude-code-bridge --help`);
219
- process.exit(1);
220
- }
221
- }
222
-
223
- async function run(config) {
224
- console.log(`[bridge] claude-bridge-cli v${require("../package.json").version}`);
225
- console.log(`[bridge] Starting on http://${config.host}:${config.port}`);
226
- console.log(`[bridge] Claude CWD: ${config.cwd}`);
227
-
228
- // Find claude binary
229
- if (!config.claudeBin) {
230
- const { execSync } = require("node:child_process");
231
- try {
232
- const lines = execSync(
233
- process.platform === "win32" ? "where claude" : "which claude",
234
- { encoding: "utf8" }
235
- ).trim().split(/\r?\n/);
236
- if (process.platform === "win32") {
237
- config.claudeBin = lines.find(l => l.endsWith(".cmd")) || lines[0];
238
- } else {
239
- config.claudeBin = lines[0];
240
- }
241
- } catch {
242
- console.error("[bridge] ERROR: claude CLI not found. Install it: npm install -g @anthropic-ai/claude-code");
243
- process.exit(1);
244
- }
245
- }
246
- console.log(`[bridge] Claude CLI: ${config.claudeBin}`);
247
-
248
- // Start bridge HTTP server
249
- const bridge = await startBridge(config);
250
- console.log(`[bridge] Bridge ready on http://${config.host}:${config.port}`);
251
- if (!config.relay) {
252
- console.log(`[bridge] Bearer token: ${config.bearerToken}`);
253
- console.log(`[bridge] Use this token when adding the endpoint in the extension.`);
254
- }
255
-
256
- // Start relay client if configured
257
- if (config.relay) {
258
- if (!config.relay.machine) {
259
- console.error("[bridge] ERROR: --machine required when using --relay-url");
260
- process.exit(1);
261
- }
262
- // Resolve a bearer:
263
- // --token → use it directly (never look at auth.json)
264
- // --save-auth → reuse saved auth.json; pair only if no entry exists
265
- // default (no flags) → pair fresh on every start, never read or write
266
- // auth.json (most secure; requires user at the
267
- // keyboard for every launch)
268
- if (!config.relay.token) {
269
- const saved = config.relay.saveAuth
270
- ? lookupSavedBearer(config.relay.url, config.relay.machine)
271
- : null;
272
- if (saved) {
273
- config.relay.token = saved;
274
- console.log(`[bridge] Using saved bearer from ${authFilePath()}`);
275
- } else {
276
- try {
277
- // ephemeral == NOT saveAuth: don't write back when pair completes.
278
- config.relay.token = await pairInteractive(
279
- config.relay.url, config.relay.machine, /* ephemeral */ !config.relay.saveAuth
280
- );
281
- } catch (e) {
282
- console.error(`[bridge] ERROR: ${e.message}`);
283
- process.exit(1);
284
- }
285
- }
286
- }
287
- console.log(`[bridge] Connecting to relay as "${config.relay.machine}"...`);
288
- startRelay(config);
289
- } else {
290
- console.log("[bridge] No relay configured — running in local-only mode.");
291
- }
292
-
293
- // Graceful shutdown
294
- const cleanup = () => {
295
- console.log("\n[bridge] Shutting down...");
296
- bridge.close();
297
- process.exit(0);
298
- };
299
- process.on("SIGINT", cleanup);
300
- process.on("SIGTERM", cleanup);
301
- }
302
-
303
- function installService() {
304
- const os = require("node:os");
305
- const fs = require("node:fs");
306
- const path = require("node:path");
307
-
308
- // Save current args to a config file for the service
309
- const configPath = path.join(os.homedir(), ".claude-code-bridge.env");
310
- const args = process.argv.slice(2).filter(a => a !== "install-service");
311
-
312
- if (process.platform === "win32") {
313
- // Windows: create a scheduled task
314
- const { execSync } = require("node:child_process");
315
- const script = `claude-code-bridge start ${args.join(" ")}`;
316
- const taskCmd = `schtasks /Create /TN "Claude Code Bridge" /TR "cmd /c ${script}" /SC ONLOGON /F /RL HIGHEST`;
317
- try {
318
- execSync(taskCmd, { stdio: "inherit" });
319
- console.log("[bridge] Service installed (Windows Scheduled Task).");
320
- console.log("[bridge] To remove: claude-code-bridge uninstall-service");
321
- } catch (e) {
322
- console.error("[bridge] Failed to create scheduled task:", e.message);
323
- }
324
- } else if (process.platform === "darwin") {
325
- // macOS: launchd
326
- const plist = `<?xml version="1.0" encoding="UTF-8"?>
327
- <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
328
- <plist version="1.0"><dict>
329
- <key>Label</key><string>com.claude-code-bridge</string>
330
- <key>ProgramArguments</key><array><string>claude-code-bridge</string><string>start</string>${args.map(a => `<string>${a}</string>`).join("")}</array>
331
- <key>RunAtLoad</key><true/>
332
- <key>KeepAlive</key><true/>
333
- </dict></plist>`;
334
- const plistPath = path.join(os.homedir(), "Library/LaunchAgents/com.claude-code-bridge.plist");
335
- fs.mkdirSync(path.dirname(plistPath), { recursive: true });
336
- fs.writeFileSync(plistPath, plist);
337
- try { require("node:child_process").execSync(`launchctl load -w "${plistPath}"`); } catch {}
338
- console.log("[bridge] Service installed (macOS LaunchAgent).");
339
- } else {
340
- // Linux: systemd user unit
341
- const unit = `[Unit]
342
- Description=Claude Code Bridge
343
- After=network-online.target
344
-
345
- [Service]
346
- Type=simple
347
- ExecStart=claude-code-bridge start ${args.join(" ")}
348
- Restart=on-failure
349
- RestartSec=5s
350
-
351
- [Install]
352
- WantedBy=default.target`;
353
- const unitDir = path.join(os.homedir(), ".config/systemd/user");
354
- fs.mkdirSync(unitDir, { recursive: true });
355
- fs.writeFileSync(path.join(unitDir, "claude-code-bridge.service"), unit);
356
- try {
357
- const { execSync } = require("node:child_process");
358
- execSync("systemctl --user daemon-reload");
359
- execSync("systemctl --user enable claude-code-bridge.service");
360
- console.log("[bridge] Service installed (systemd user unit).");
361
- } catch {}
362
- }
363
- }
364
-
365
- function uninstallService() {
366
- const os = require("node:os");
367
- const path = require("node:path");
368
- const fs = require("node:fs");
369
-
370
- if (process.platform === "win32") {
371
- try {
372
- require("node:child_process").execSync('schtasks /Delete /TN "Claude Code Bridge" /F', { stdio: "inherit" });
373
- } catch {}
374
- console.log("[bridge] Service removed (Windows).");
375
- } else if (process.platform === "darwin") {
376
- const p = path.join(os.homedir(), "Library/LaunchAgents/com.claude-code-bridge.plist");
377
- try { require("node:child_process").execSync(`launchctl unload -w "${p}"`); } catch {}
378
- try { fs.unlinkSync(p); } catch {}
379
- console.log("[bridge] Service removed (macOS).");
380
- } else {
381
- try {
382
- const { execSync } = require("node:child_process");
383
- execSync("systemctl --user disable --now claude-code-bridge.service");
384
- fs.unlinkSync(path.join(os.homedir(), ".config/systemd/user/claude-code-bridge.service"));
385
- execSync("systemctl --user daemon-reload");
386
- } catch {}
387
- console.log("[bridge] Service removed (Linux).");
388
- }
389
- }
390
-
391
- main();
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+
4
+ const { parseArgs } = require("node:util");
5
+ const fs = require("node:fs");
6
+ const path = require("node:path");
7
+ const os = require("node:os");
8
+ const { startBridge } = require("../lib/bridge");
9
+ const { startRelay } = require("../lib/relay-client");
10
+
11
+ // ── Pair flow (browser-OTP based auth) ────────────────────────────────────
12
+ // When --token isn't supplied, the CLI:
13
+ // 1. POSTs /pair-cli/start to the relay's HTTP base (public endpoint)
14
+ // 2. Prints the short code + pair URL — user opens it, OTPs into CF Access,
15
+ // confirms in the browser
16
+ // 3. Long-polls /pair-cli/claim until the bearer arrives
17
+ // 4. Saves the bearer to ~/.claude-bridge/auth.json so subsequent starts
18
+ // pick it up automatically
19
+ // Replaces the old workflow of running `claude-relay-ctl provision <m>` on
20
+ // the host and copy-pasting a long bearer onto the CLI command line.
21
+
22
+ function authFilePath() {
23
+ return path.join(os.homedir(), ".claude-bridge", "auth.json");
24
+ }
25
+
26
+ function readAuthFile() {
27
+ try { return JSON.parse(fs.readFileSync(authFilePath(), "utf8")); }
28
+ catch { return { bearers: {} }; }
29
+ }
30
+
31
+ function writeAuthFile(data) {
32
+ const fp = authFilePath();
33
+ fs.mkdirSync(path.dirname(fp), { recursive: true });
34
+ fs.writeFileSync(fp, JSON.stringify(data, null, 2), { mode: 0o600 });
35
+ }
36
+
37
+ function relayHttpBase(wsUrl) {
38
+ // wss://host/agent/ws → https://host
39
+ const u = new URL(wsUrl);
40
+ const proto = u.protocol === "wss:" ? "https:" : "http:";
41
+ return `${proto}//${u.host}`;
42
+ }
43
+
44
+ async function pairInteractive(relayWsUrl, machine, ephemeral) {
45
+ const base = relayHttpBase(relayWsUrl);
46
+ process.stdout.write(`[bridge] Starting pair flow for "${machine}" against ${base}\n`);
47
+ if (ephemeral) process.stdout.write(`[bridge] Ephemeral mode — bearer will NOT be saved to disk.\n`);
48
+
49
+ const startResp = await fetch(`${base}/pair-cli/start`, {
50
+ method: "POST",
51
+ headers: { "Content-Type": "application/json" },
52
+ body: JSON.stringify({ machine }),
53
+ });
54
+ if (!startResp.ok) {
55
+ const text = await startResp.text();
56
+ throw new Error(`pair start failed (${startResp.status}): ${text}`);
57
+ }
58
+ const { code, poll_token, pair_url, expires_in } = await startResp.json();
59
+
60
+ console.log("\n ┌─────────────────────────────────────────────────┐");
61
+ console.log(" │ Open this URL in a browser to pair the device │");
62
+ console.log(" ├─────────────────────────────────────────────────┤");
63
+ console.log(` │ ${pair_url.padEnd(47)} │`);
64
+ console.log(" │ │");
65
+ console.log(` │ Confirm this code matches: ${code.padEnd(17)}│`);
66
+ console.log(" └─────────────────────────────────────────────────┘");
67
+ console.log(` Code expires in ${expires_in}s. Waiting for confirmation…\n`);
68
+
69
+ const deadline = Date.now() + expires_in * 1000;
70
+ while (Date.now() < deadline) {
71
+ let resp;
72
+ try {
73
+ resp = await fetch(`${base}/pair-cli/claim`, {
74
+ method: "POST",
75
+ headers: { "Content-Type": "application/json" },
76
+ body: JSON.stringify({ poll_token }),
77
+ });
78
+ } catch (e) {
79
+ await new Promise(r => setTimeout(r, 2000));
80
+ continue;
81
+ }
82
+ if (resp.status === 202) continue; // still waiting, re-poll
83
+ if (resp.status === 404 || resp.status === 410) {
84
+ throw new Error("pair expired or invalidated — re-run to start over");
85
+ }
86
+ if (!resp.ok) {
87
+ const text = await resp.text();
88
+ throw new Error(`pair claim failed (${resp.status}): ${text}`);
89
+ }
90
+ const { bearer, machine: confirmed } = await resp.json();
91
+ if (ephemeral) {
92
+ console.log(`[bridge] ✓ Paired as "${confirmed}" (in-memory only, nothing written to disk).`);
93
+ } else {
94
+ console.log(`[bridge] ✓ Paired as "${confirmed}". Bearer saved to ${authFilePath()}`);
95
+ const auth = readAuthFile();
96
+ auth.bearers = auth.bearers || {};
97
+ auth.bearers[base] = auth.bearers[base] || {};
98
+ auth.bearers[base][confirmed] = bearer;
99
+ writeAuthFile(auth);
100
+ }
101
+ return bearer;
102
+ }
103
+ throw new Error("pair window expired — re-run to start over");
104
+ }
105
+
106
+ function lookupSavedBearer(relayWsUrl, machine) {
107
+ const auth = readAuthFile();
108
+ return auth.bearers?.[relayHttpBase(relayWsUrl)]?.[machine] || null;
109
+ }
110
+
111
+ const HELP = `
112
+ claude-code-bridge — Bridge server for Claude Code CLI
113
+
114
+ Usage:
115
+ claude-code-bridge start [options] Start the bridge (+ optional relay)
116
+ claude-code-bridge install-service Register as a system service (auto-start on boot)
117
+ claude-code-bridge uninstall-service Remove the system service
118
+ claude-code-bridge --help Show this help
119
+ claude-code-bridge --version Print the installed version
120
+
121
+ Options:
122
+ --port <n> HTTP port for the local bridge (default: 8091)
123
+ --host <ip> Bind address (default: 127.0.0.1)
124
+ --token <secret> Bridge password (use the same in the extension). Auto-generated if omitted.
125
+ --cwd <path> Default working directory for Claude (default: current dir)
126
+ --claude-bin <path> Path to claude CLI binary (default: auto-detect)
127
+ --timeout <seconds> Max time for a single Claude call (default: 10800)
128
+
129
+ Relay options (connect to a remote relay server):
130
+ --relay-url <url> WebSocket URL of the relay server
131
+ --machine <name> Machine name for the relay
132
+ --token <bearer> Machine bearer (skips both auth.json and pair flow)
133
+ --save-auth Cache the bearer in ~/.claude-bridge/auth.json so the
134
+ next start reuses it. Required for unattended /
135
+ systemd-service installs. WITHOUT this flag the CLI
136
+ re-pairs (browser OTP) on every start — the default.
137
+ --cf-id <id> Cloudflare Access Client ID (optional)
138
+ --cf-secret <secret> Cloudflare Access Client Secret (optional)
139
+
140
+ Environment variables:
141
+ All options can be set via env vars with BRIDGE_ prefix:
142
+ BRIDGE_PORT, BRIDGE_HOST, BRIDGE_CWD, BRIDGE_TIMEOUT,
143
+ BRIDGE_RELAY_URL, BRIDGE_MACHINE_NAME, BRIDGE_MACHINE_TOKEN,
144
+ BRIDGE_SAVE_AUTH=1 (cache the bearer to ~/.claude-bridge/auth.json
145
+ for unattended restarts; default re-pairs every
146
+ start so nothing is persisted),
147
+ BRIDGE_CF_ID, BRIDGE_CF_SECRET
148
+ `;
149
+
150
+ function env(key, def) {
151
+ return process.env["BRIDGE_" + key] || def;
152
+ }
153
+
154
+ function main() {
155
+ const args = process.argv.slice(2);
156
+ if (args.includes("--version") || args.includes("-v") || args[0] === "version") {
157
+ console.log(require("../package.json").version);
158
+ process.exit(0);
159
+ }
160
+ if (args.includes("--help") || args.includes("-h") || args.length === 0) {
161
+ console.log(HELP);
162
+ process.exit(0);
163
+ }
164
+
165
+ const command = args[0];
166
+ if (command === "start") {
167
+ const { values } = parseArgs({
168
+ args: args.slice(1),
169
+ options: {
170
+ port: { type: "string", default: env("PORT", "8091") },
171
+ host: { type: "string", default: env("HOST", "127.0.0.1") },
172
+ cwd: { type: "string", default: env("CWD", process.cwd()) },
173
+ "claude-bin": { type: "string", default: env("CLAUDE_BIN", "") },
174
+ timeout: { type: "string", default: env("TIMEOUT", "10800") },
175
+ "relay-url": { type: "string", default: env("RELAY_URL", "") },
176
+ machine: { type: "string", default: env("MACHINE_NAME", "") },
177
+ token: { type: "string", default: env("MACHINE_TOKEN", "") },
178
+ "save-auth":{ type: "boolean", default: env("SAVE_AUTH", "") === "1" },
179
+ ephemeral: { type: "boolean", default: false }, // deprecated, now default behavior — kept as a no-op for compatibility
180
+ "cf-id": { type: "string", default: env("CF_ID", "") },
181
+ "cf-secret": { type: "string", default: env("CF_SECRET", "") },
182
+ },
183
+ strict: false,
184
+ });
185
+
186
+ const config = {
187
+ port: parseInt(values.port, 10),
188
+ host: values.host,
189
+ cwd: values.cwd,
190
+ claudeBin: values["claude-bin"],
191
+ timeout: parseInt(values.timeout, 10) * 1000,
192
+ relay: values["relay-url"] ? {
193
+ url: values["relay-url"],
194
+ machine: values.machine,
195
+ token: values.token,
196
+ // Default = ephemeral (pair on every start, never persist). Opt in
197
+ // to caching with --save-auth when running unattended / as a service.
198
+ // The legacy --ephemeral flag is a no-op (kept for back-compat).
199
+ saveAuth: !!values["save-auth"],
200
+ cfId: values["cf-id"],
201
+ cfSecret: values["cf-secret"],
202
+ } : null,
203
+ };
204
+
205
+ const crypto = require("node:crypto");
206
+ if (config.relay) {
207
+ config.bearerToken = crypto.randomBytes(32).toString("hex");
208
+ } else {
209
+ config.bearerToken = values.token || crypto.randomBytes(32).toString("hex");
210
+ }
211
+
212
+ run(config);
213
+ } else if (command === "install-service") {
214
+ installService();
215
+ } else if (command === "uninstall-service") {
216
+ uninstallService();
217
+ } else {
218
+ console.error(`Unknown command: ${command}\nRun claude-code-bridge --help`);
219
+ process.exit(1);
220
+ }
221
+ }
222
+
223
+ async function run(config) {
224
+ console.log(`[bridge] claude-bridge-cli v${require("../package.json").version}`);
225
+ console.log(`[bridge] Starting on http://${config.host}:${config.port}`);
226
+ console.log(`[bridge] Claude CWD: ${config.cwd}`);
227
+
228
+ // Find claude binary
229
+ if (!config.claudeBin) {
230
+ const { execSync } = require("node:child_process");
231
+ try {
232
+ const lines = execSync(
233
+ process.platform === "win32" ? "where claude" : "which claude",
234
+ { encoding: "utf8" }
235
+ ).trim().split(/\r?\n/);
236
+ if (process.platform === "win32") {
237
+ config.claudeBin = lines.find(l => l.endsWith(".cmd")) || lines[0];
238
+ } else {
239
+ config.claudeBin = lines[0];
240
+ }
241
+ } catch {
242
+ console.error("[bridge] ERROR: claude CLI not found. Install it: npm install -g @anthropic-ai/claude-code");
243
+ process.exit(1);
244
+ }
245
+ }
246
+ console.log(`[bridge] Claude CLI: ${config.claudeBin}`);
247
+
248
+ // Start bridge HTTP server
249
+ const bridge = await startBridge(config);
250
+ console.log(`[bridge] Bridge ready on http://${config.host}:${config.port}`);
251
+ if (!config.relay) {
252
+ console.log(`[bridge] Bearer token: ${config.bearerToken}`);
253
+ console.log(`[bridge] Use this token when adding the endpoint in the extension.`);
254
+ }
255
+
256
+ // Start relay client if configured
257
+ if (config.relay) {
258
+ if (!config.relay.machine) {
259
+ console.error("[bridge] ERROR: --machine required when using --relay-url");
260
+ process.exit(1);
261
+ }
262
+ // Resolve a bearer:
263
+ // --token → use it directly (never look at auth.json)
264
+ // --save-auth → reuse saved auth.json; pair only if no entry exists
265
+ // default (no flags) → pair fresh on every start, never read or write
266
+ // auth.json (most secure; requires user at the
267
+ // keyboard for every launch)
268
+ if (!config.relay.token) {
269
+ const saved = config.relay.saveAuth
270
+ ? lookupSavedBearer(config.relay.url, config.relay.machine)
271
+ : null;
272
+ if (saved) {
273
+ config.relay.token = saved;
274
+ console.log(`[bridge] Using saved bearer from ${authFilePath()}`);
275
+ } else {
276
+ try {
277
+ // ephemeral == NOT saveAuth: don't write back when pair completes.
278
+ config.relay.token = await pairInteractive(
279
+ config.relay.url, config.relay.machine, /* ephemeral */ !config.relay.saveAuth
280
+ );
281
+ } catch (e) {
282
+ console.error(`[bridge] ERROR: ${e.message}`);
283
+ process.exit(1);
284
+ }
285
+ }
286
+ }
287
+ console.log(`[bridge] Connecting to relay as "${config.relay.machine}"...`);
288
+ startRelay(config);
289
+ } else {
290
+ console.log("[bridge] No relay configured — running in local-only mode.");
291
+ }
292
+
293
+ // Graceful shutdown
294
+ const cleanup = () => {
295
+ console.log("\n[bridge] Shutting down...");
296
+ bridge.close();
297
+ process.exit(0);
298
+ };
299
+ process.on("SIGINT", cleanup);
300
+ process.on("SIGTERM", cleanup);
301
+ }
302
+
303
+ function installService() {
304
+ const os = require("node:os");
305
+ const fs = require("node:fs");
306
+ const path = require("node:path");
307
+
308
+ // Save current args to a config file for the service
309
+ const configPath = path.join(os.homedir(), ".claude-code-bridge.env");
310
+ const args = process.argv.slice(2).filter(a => a !== "install-service");
311
+
312
+ if (process.platform === "win32") {
313
+ // Windows: create a scheduled task
314
+ const { execSync } = require("node:child_process");
315
+ const script = `claude-code-bridge start ${args.join(" ")}`;
316
+ const taskCmd = `schtasks /Create /TN "Claude Code Bridge" /TR "cmd /c ${script}" /SC ONLOGON /F /RL HIGHEST`;
317
+ try {
318
+ execSync(taskCmd, { stdio: "inherit" });
319
+ console.log("[bridge] Service installed (Windows Scheduled Task).");
320
+ console.log("[bridge] To remove: claude-code-bridge uninstall-service");
321
+ } catch (e) {
322
+ console.error("[bridge] Failed to create scheduled task:", e.message);
323
+ }
324
+ } else if (process.platform === "darwin") {
325
+ // macOS: launchd
326
+ const plist = `<?xml version="1.0" encoding="UTF-8"?>
327
+ <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
328
+ <plist version="1.0"><dict>
329
+ <key>Label</key><string>com.claude-code-bridge</string>
330
+ <key>ProgramArguments</key><array><string>claude-code-bridge</string><string>start</string>${args.map(a => `<string>${a}</string>`).join("")}</array>
331
+ <key>RunAtLoad</key><true/>
332
+ <key>KeepAlive</key><true/>
333
+ </dict></plist>`;
334
+ const plistPath = path.join(os.homedir(), "Library/LaunchAgents/com.claude-code-bridge.plist");
335
+ fs.mkdirSync(path.dirname(plistPath), { recursive: true });
336
+ fs.writeFileSync(plistPath, plist);
337
+ try { require("node:child_process").execSync(`launchctl load -w "${plistPath}"`); } catch {}
338
+ console.log("[bridge] Service installed (macOS LaunchAgent).");
339
+ } else {
340
+ // Linux: systemd user unit
341
+ const unit = `[Unit]
342
+ Description=Claude Code Bridge
343
+ After=network-online.target
344
+
345
+ [Service]
346
+ Type=simple
347
+ ExecStart=claude-code-bridge start ${args.join(" ")}
348
+ Restart=on-failure
349
+ RestartSec=5s
350
+
351
+ [Install]
352
+ WantedBy=default.target`;
353
+ const unitDir = path.join(os.homedir(), ".config/systemd/user");
354
+ fs.mkdirSync(unitDir, { recursive: true });
355
+ fs.writeFileSync(path.join(unitDir, "claude-code-bridge.service"), unit);
356
+ try {
357
+ const { execSync } = require("node:child_process");
358
+ execSync("systemctl --user daemon-reload");
359
+ execSync("systemctl --user enable claude-code-bridge.service");
360
+ console.log("[bridge] Service installed (systemd user unit).");
361
+ } catch {}
362
+ }
363
+ }
364
+
365
+ function uninstallService() {
366
+ const os = require("node:os");
367
+ const path = require("node:path");
368
+ const fs = require("node:fs");
369
+
370
+ if (process.platform === "win32") {
371
+ try {
372
+ require("node:child_process").execSync('schtasks /Delete /TN "Claude Code Bridge" /F', { stdio: "inherit" });
373
+ } catch {}
374
+ console.log("[bridge] Service removed (Windows).");
375
+ } else if (process.platform === "darwin") {
376
+ const p = path.join(os.homedir(), "Library/LaunchAgents/com.claude-code-bridge.plist");
377
+ try { require("node:child_process").execSync(`launchctl unload -w "${p}"`); } catch {}
378
+ try { fs.unlinkSync(p); } catch {}
379
+ console.log("[bridge] Service removed (macOS).");
380
+ } else {
381
+ try {
382
+ const { execSync } = require("node:child_process");
383
+ execSync("systemctl --user disable --now claude-code-bridge.service");
384
+ fs.unlinkSync(path.join(os.homedir(), ".config/systemd/user/claude-code-bridge.service"));
385
+ execSync("systemctl --user daemon-reload");
386
+ } catch {}
387
+ console.log("[bridge] Service removed (Linux).");
388
+ }
389
+ }
390
+
391
+ main();