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.
- package/bin/cli.js +119 -3
- 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
|
|
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
|
|
137
|
-
console.error("[bridge] ERROR: --machine
|
|
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