claude-bridge-cli 1.3.1 → 1.4.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.
Files changed (2) hide show
  1. package/bin/cli.js +119 -3
  2. package/package.json +1 -1
package/bin/cli.js CHANGED
@@ -2,9 +2,108 @@
2
2
  "use strict";
3
3
 
4
4
  const { parseArgs } = require("node:util");
5
+ const fs = require("node:fs");
6
+ const path = require("node:path");
7
+ const os = require("node:os");
5
8
  const { startBridge } = require("../lib/bridge");
6
9
  const { startRelay } = require("../lib/relay-client");
7
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) {
45
+ const base = relayHttpBase(relayWsUrl);
46
+ process.stdout.write(`[bridge] Starting pair flow for "${machine}" against ${base}\n`);
47
+
48
+ const startResp = await fetch(`${base}/pair-cli/start`, {
49
+ method: "POST",
50
+ headers: { "Content-Type": "application/json" },
51
+ body: JSON.stringify({ machine }),
52
+ });
53
+ if (!startResp.ok) {
54
+ const text = await startResp.text();
55
+ throw new Error(`pair start failed (${startResp.status}): ${text}`);
56
+ }
57
+ const { code, poll_token, pair_url, expires_in } = await startResp.json();
58
+
59
+ console.log("\n ┌─────────────────────────────────────────────────┐");
60
+ console.log(" │ Open this URL in a browser to pair the device │");
61
+ console.log(" ├─────────────────────────────────────────────────┤");
62
+ console.log(` │ ${pair_url.padEnd(47)} │`);
63
+ console.log(" │ │");
64
+ console.log(` │ Confirm this code matches: ${code.padEnd(17)}│`);
65
+ console.log(" └─────────────────────────────────────────────────┘");
66
+ console.log(` Code expires in ${expires_in}s. Waiting for confirmation…\n`);
67
+
68
+ const deadline = Date.now() + expires_in * 1000;
69
+ while (Date.now() < deadline) {
70
+ let resp;
71
+ try {
72
+ resp = await fetch(`${base}/pair-cli/claim`, {
73
+ method: "POST",
74
+ headers: { "Content-Type": "application/json" },
75
+ body: JSON.stringify({ poll_token }),
76
+ });
77
+ } catch (e) {
78
+ await new Promise(r => setTimeout(r, 2000));
79
+ continue;
80
+ }
81
+ if (resp.status === 202) continue; // still waiting, re-poll
82
+ if (resp.status === 404 || resp.status === 410) {
83
+ throw new Error("pair expired or invalidated — re-run to start over");
84
+ }
85
+ if (!resp.ok) {
86
+ const text = await resp.text();
87
+ throw new Error(`pair claim failed (${resp.status}): ${text}`);
88
+ }
89
+ const { bearer, machine: confirmed } = await resp.json();
90
+ console.log(`[bridge] ✓ Paired as "${confirmed}". Bearer saved to ${authFilePath()}`);
91
+
92
+ const auth = readAuthFile();
93
+ auth.bearers = auth.bearers || {};
94
+ auth.bearers[base] = auth.bearers[base] || {};
95
+ auth.bearers[base][confirmed] = bearer;
96
+ writeAuthFile(auth);
97
+ return bearer;
98
+ }
99
+ throw new Error("pair window expired — re-run to start over");
100
+ }
101
+
102
+ function lookupSavedBearer(relayWsUrl, machine) {
103
+ const auth = readAuthFile();
104
+ return auth.bearers?.[relayHttpBase(relayWsUrl)]?.[machine] || null;
105
+ }
106
+
8
107
  const HELP = `
9
108
  claude-code-bridge — Bridge server for Claude Code CLI
10
109
 
@@ -25,7 +124,9 @@ Options:
25
124
  Relay options (connect to a remote relay server):
26
125
  --relay-url <url> WebSocket URL of the relay server
27
126
  --machine <name> Machine name for the relay
28
- --token <bearer> Machine bearer token (when using relay, --token is the relay auth)
127
+ --token <bearer> Machine bearer (optional if omitted, the CLI starts
128
+ a browser-OTP pair flow and caches the bearer at
129
+ ~/.claude-bridge/auth.json for next time)
29
130
  --cf-id <id> Cloudflare Access Client ID (optional)
30
131
  --cf-secret <secret> Cloudflare Access Client Secret (optional)
31
132
 
@@ -133,10 +234,25 @@ async function run(config) {
133
234
 
134
235
  // Start relay client if configured
135
236
  if (config.relay) {
136
- if (!config.relay.machine || !config.relay.token) {
137
- console.error("[bridge] ERROR: --machine and --token required when using --relay-url");
237
+ if (!config.relay.machine) {
238
+ console.error("[bridge] ERROR: --machine required when using --relay-url");
138
239
  process.exit(1);
139
240
  }
241
+ // Resolve a bearer: --token > saved auth.json > interactive pair flow.
242
+ if (!config.relay.token) {
243
+ const saved = lookupSavedBearer(config.relay.url, config.relay.machine);
244
+ if (saved) {
245
+ config.relay.token = saved;
246
+ console.log(`[bridge] Using saved bearer from ${authFilePath()}`);
247
+ } else {
248
+ try {
249
+ config.relay.token = await pairInteractive(config.relay.url, config.relay.machine);
250
+ } catch (e) {
251
+ console.error(`[bridge] ERROR: ${e.message}`);
252
+ process.exit(1);
253
+ }
254
+ }
255
+ }
140
256
  console.log(`[bridge] Connecting to relay as "${config.relay.machine}"...`);
141
257
  startRelay(config);
142
258
  } else {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-bridge-cli",
3
- "version": "1.3.1",
3
+ "version": "1.4.0",
4
4
  "description": "Use Claude Code from your browser. Runs a local server that connects your browser tools to the Claude CLI.",
5
5
  "main": "lib/bridge.js",
6
6
  "bin": {