@willjackson/claude-code-bridge 0.1.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/LICENSE +21 -0
- package/README.md +690 -0
- package/dist/chunk-BRH476VK.js +1993 -0
- package/dist/chunk-BRH476VK.js.map +1 -0
- package/dist/cli.d.ts +40 -0
- package/dist/cli.js +844 -0
- package/dist/cli.js.map +1 -0
- package/dist/index.d.ts +1848 -0
- package/dist/index.js +976 -0
- package/dist/index.js.map +1 -0
- package/package.json +71 -0
package/dist/cli.js
ADDED
|
@@ -0,0 +1,844 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
Bridge,
|
|
4
|
+
ConnectionState,
|
|
5
|
+
WebSocketTransport,
|
|
6
|
+
createLogger,
|
|
7
|
+
detectEnvironment,
|
|
8
|
+
discoverDdevProjects,
|
|
9
|
+
discoverDockerPeers,
|
|
10
|
+
discoverDocksalProjects,
|
|
11
|
+
discoverLandoProjects,
|
|
12
|
+
getDefaultConfig,
|
|
13
|
+
getHostGateway,
|
|
14
|
+
loadConfigSync
|
|
15
|
+
} from "./chunk-BRH476VK.js";
|
|
16
|
+
|
|
17
|
+
// src/cli/index.ts
|
|
18
|
+
import { Command as Command7 } from "commander";
|
|
19
|
+
|
|
20
|
+
// src/cli/commands/start.ts
|
|
21
|
+
import { Command } from "commander";
|
|
22
|
+
import * as fs from "fs";
|
|
23
|
+
import * as path from "path";
|
|
24
|
+
import * as os from "os";
|
|
25
|
+
|
|
26
|
+
// src/cli/utils.ts
|
|
27
|
+
function setupGracefulShutdown(options) {
|
|
28
|
+
const {
|
|
29
|
+
cleanup,
|
|
30
|
+
afterCleanup,
|
|
31
|
+
verbose = true,
|
|
32
|
+
timeout = 1e4
|
|
33
|
+
} = options;
|
|
34
|
+
let isShuttingDown = false;
|
|
35
|
+
const handler = async (signal) => {
|
|
36
|
+
if (isShuttingDown) {
|
|
37
|
+
if (verbose) {
|
|
38
|
+
console.log("Shutdown already in progress...");
|
|
39
|
+
}
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
isShuttingDown = true;
|
|
43
|
+
if (verbose) {
|
|
44
|
+
console.log(`
|
|
45
|
+
Received ${signal}, shutting down gracefully...`);
|
|
46
|
+
}
|
|
47
|
+
const forceExitTimeout = setTimeout(() => {
|
|
48
|
+
if (verbose) {
|
|
49
|
+
console.error("Shutdown timeout - forcing exit");
|
|
50
|
+
}
|
|
51
|
+
process.exit(1);
|
|
52
|
+
}, timeout);
|
|
53
|
+
try {
|
|
54
|
+
await cleanup();
|
|
55
|
+
if (afterCleanup) {
|
|
56
|
+
afterCleanup();
|
|
57
|
+
}
|
|
58
|
+
clearTimeout(forceExitTimeout);
|
|
59
|
+
if (verbose) {
|
|
60
|
+
console.log("Shutdown complete.");
|
|
61
|
+
}
|
|
62
|
+
process.exit(0);
|
|
63
|
+
} catch (error) {
|
|
64
|
+
clearTimeout(forceExitTimeout);
|
|
65
|
+
if (verbose) {
|
|
66
|
+
console.error(`Error during shutdown: ${error.message}`);
|
|
67
|
+
}
|
|
68
|
+
process.exit(1);
|
|
69
|
+
}
|
|
70
|
+
};
|
|
71
|
+
const sigintHandler = () => handler("SIGINT");
|
|
72
|
+
const sigtermHandler = () => handler("SIGTERM");
|
|
73
|
+
process.on("SIGINT", sigintHandler);
|
|
74
|
+
process.on("SIGTERM", sigtermHandler);
|
|
75
|
+
return () => {
|
|
76
|
+
process.off("SIGINT", sigintHandler);
|
|
77
|
+
process.off("SIGTERM", sigtermHandler);
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
function handleUnhandledRejections(options = {}) {
|
|
81
|
+
const { exit = false, logger: logger8 = console.error } = options;
|
|
82
|
+
process.on("unhandledRejection", (reason, promise) => {
|
|
83
|
+
const error = reason instanceof Error ? reason : new Error(String(reason));
|
|
84
|
+
logger8("Unhandled promise rejection:", error);
|
|
85
|
+
if (exit) {
|
|
86
|
+
process.exit(1);
|
|
87
|
+
}
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// src/cli/commands/start.ts
|
|
92
|
+
var logger = createLogger("cli:start");
|
|
93
|
+
function getPidFilePath() {
|
|
94
|
+
const bridgeDir = path.join(os.homedir(), ".claude-bridge");
|
|
95
|
+
return path.join(bridgeDir, "bridge.pid");
|
|
96
|
+
}
|
|
97
|
+
function ensureBridgeDir() {
|
|
98
|
+
const bridgeDir = path.join(os.homedir(), ".claude-bridge");
|
|
99
|
+
if (!fs.existsSync(bridgeDir)) {
|
|
100
|
+
fs.mkdirSync(bridgeDir, { recursive: true });
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
function writePidFile(pid) {
|
|
104
|
+
ensureBridgeDir();
|
|
105
|
+
const pidFile = getPidFilePath();
|
|
106
|
+
fs.writeFileSync(pidFile, pid.toString(), "utf-8");
|
|
107
|
+
}
|
|
108
|
+
function removePidFile() {
|
|
109
|
+
const pidFile = getPidFilePath();
|
|
110
|
+
if (fs.existsSync(pidFile)) {
|
|
111
|
+
fs.unlinkSync(pidFile);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
function isAlreadyRunning() {
|
|
115
|
+
const pidFile = getPidFilePath();
|
|
116
|
+
if (!fs.existsSync(pidFile)) {
|
|
117
|
+
return { running: false };
|
|
118
|
+
}
|
|
119
|
+
try {
|
|
120
|
+
const pid = parseInt(fs.readFileSync(pidFile, "utf-8").trim(), 10);
|
|
121
|
+
process.kill(pid, 0);
|
|
122
|
+
return { running: true, pid };
|
|
123
|
+
} catch {
|
|
124
|
+
removePidFile();
|
|
125
|
+
return { running: false };
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
function buildBridgeConfig(options, globalOptions) {
|
|
129
|
+
const fileConfig = loadConfigSync(globalOptions.config);
|
|
130
|
+
let envConfig = {};
|
|
131
|
+
if (options.auto) {
|
|
132
|
+
const env = detectEnvironment();
|
|
133
|
+
const defaultEnvConfig = getDefaultConfig(env);
|
|
134
|
+
logger.info({ environment: env.type, isContainer: env.isContainer }, "Auto-detected environment");
|
|
135
|
+
envConfig = {
|
|
136
|
+
mode: defaultEnvConfig.mode,
|
|
137
|
+
instanceName: defaultEnvConfig.instanceName,
|
|
138
|
+
listen: defaultEnvConfig.listen,
|
|
139
|
+
connect: defaultEnvConfig.connect
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
const cliConfig = {};
|
|
143
|
+
if (options.port) {
|
|
144
|
+
cliConfig.listen = {
|
|
145
|
+
...cliConfig.listen,
|
|
146
|
+
port: parseInt(options.port, 10),
|
|
147
|
+
host: options.host ?? "0.0.0.0"
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
if (options.host && !cliConfig.listen) {
|
|
151
|
+
cliConfig.listen = {
|
|
152
|
+
port: fileConfig.listen.port,
|
|
153
|
+
host: options.host
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
if (options.connect) {
|
|
157
|
+
cliConfig.connect = {
|
|
158
|
+
url: options.connect
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
let mode = "peer";
|
|
162
|
+
const hasListen = cliConfig.listen || envConfig.listen || fileConfig.listen;
|
|
163
|
+
const hasConnect = cliConfig.connect || envConfig.connect || fileConfig.connect;
|
|
164
|
+
if (hasListen && !hasConnect) {
|
|
165
|
+
mode = "host";
|
|
166
|
+
} else if (hasConnect && !hasListen) {
|
|
167
|
+
mode = "client";
|
|
168
|
+
}
|
|
169
|
+
const finalConfig = {
|
|
170
|
+
mode: cliConfig.mode ?? envConfig.mode ?? fileConfig.mode ?? mode,
|
|
171
|
+
instanceName: cliConfig.instanceName ?? envConfig.instanceName ?? fileConfig.instanceName ?? `bridge-${process.pid}`,
|
|
172
|
+
listen: {
|
|
173
|
+
port: cliConfig.listen?.port ?? envConfig.listen?.port ?? fileConfig.listen.port,
|
|
174
|
+
host: cliConfig.listen?.host ?? envConfig.listen?.host ?? fileConfig.listen.host
|
|
175
|
+
},
|
|
176
|
+
taskTimeout: fileConfig.interaction.taskTimeout,
|
|
177
|
+
contextSharing: {
|
|
178
|
+
autoSync: fileConfig.contextSharing.autoSync,
|
|
179
|
+
syncInterval: fileConfig.contextSharing.syncInterval
|
|
180
|
+
}
|
|
181
|
+
};
|
|
182
|
+
const connectUrl = cliConfig.connect?.url ?? envConfig.connect?.url ?? fileConfig.connect?.url;
|
|
183
|
+
if (connectUrl) {
|
|
184
|
+
finalConfig.connect = {
|
|
185
|
+
url: connectUrl
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
return finalConfig;
|
|
189
|
+
}
|
|
190
|
+
async function startBridge(options, globalOptions) {
|
|
191
|
+
const { running, pid } = isAlreadyRunning();
|
|
192
|
+
if (running) {
|
|
193
|
+
console.error(`Bridge is already running (PID: ${pid})`);
|
|
194
|
+
process.exit(1);
|
|
195
|
+
}
|
|
196
|
+
const config = buildBridgeConfig(options, globalOptions);
|
|
197
|
+
console.log("Starting Claude Code Bridge...");
|
|
198
|
+
console.log(` Instance: ${config.instanceName}`);
|
|
199
|
+
console.log(` Mode: ${config.mode}`);
|
|
200
|
+
if (config.listen) {
|
|
201
|
+
console.log(` Listening: ${config.listen.host}:${config.listen.port}`);
|
|
202
|
+
}
|
|
203
|
+
if (config.connect) {
|
|
204
|
+
console.log(` Connecting to: ${config.connect.url}`);
|
|
205
|
+
}
|
|
206
|
+
if (options.daemon) {
|
|
207
|
+
console.log(" Running in daemon mode");
|
|
208
|
+
writePidFile(process.pid);
|
|
209
|
+
}
|
|
210
|
+
const bridge = new Bridge(config);
|
|
211
|
+
setupGracefulShutdown({
|
|
212
|
+
cleanup: async () => {
|
|
213
|
+
logger.info("Stopping bridge...");
|
|
214
|
+
await bridge.stop();
|
|
215
|
+
logger.info("Bridge stopped");
|
|
216
|
+
},
|
|
217
|
+
afterCleanup: options.daemon ? removePidFile : void 0,
|
|
218
|
+
verbose: true,
|
|
219
|
+
timeout: 1e4
|
|
220
|
+
});
|
|
221
|
+
handleUnhandledRejections({
|
|
222
|
+
exit: false,
|
|
223
|
+
logger: (msg, err) => logger.error({ error: err.message }, msg)
|
|
224
|
+
});
|
|
225
|
+
try {
|
|
226
|
+
await bridge.start();
|
|
227
|
+
if (options.daemon) {
|
|
228
|
+
writePidFile(process.pid);
|
|
229
|
+
}
|
|
230
|
+
console.log("Bridge started successfully.");
|
|
231
|
+
console.log(`Connected peers: ${bridge.getPeerCount()}`);
|
|
232
|
+
if (config.listen) {
|
|
233
|
+
console.log(`
|
|
234
|
+
To connect from another bridge:`);
|
|
235
|
+
console.log(` claude-bridge connect ws://localhost:${config.listen.port}`);
|
|
236
|
+
}
|
|
237
|
+
if (!options.daemon) {
|
|
238
|
+
console.log("\nPress Ctrl+C to stop.");
|
|
239
|
+
}
|
|
240
|
+
} catch (error) {
|
|
241
|
+
logger.error({ error: error.message }, "Failed to start bridge");
|
|
242
|
+
console.error(`Failed to start bridge: ${error.message}`);
|
|
243
|
+
if (options.daemon) {
|
|
244
|
+
removePidFile();
|
|
245
|
+
}
|
|
246
|
+
process.exit(1);
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
function createStartCommand() {
|
|
250
|
+
const command = new Command("start");
|
|
251
|
+
command.description("Start the bridge server").option("-p, --port <port>", "Port to listen on (default: 8765 in container, 8766 native)").option("-h, --host <host>", "Host to bind to (default: 0.0.0.0)").option("-c, --connect <url>", "URL to connect to on startup (e.g., ws://localhost:8765)").option("-a, --auto", "Auto-detect environment and configure").option("-d, --daemon", "Run in background").action(async (options) => {
|
|
252
|
+
const globalOptions = command.parent?.opts();
|
|
253
|
+
await startBridge(options, globalOptions);
|
|
254
|
+
});
|
|
255
|
+
return command;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
// src/cli/commands/stop.ts
|
|
259
|
+
import { Command as Command2 } from "commander";
|
|
260
|
+
import * as fs2 from "fs";
|
|
261
|
+
import * as path2 from "path";
|
|
262
|
+
import * as os2 from "os";
|
|
263
|
+
var logger2 = createLogger("cli:stop");
|
|
264
|
+
function getPidFilePath2() {
|
|
265
|
+
const bridgeDir = path2.join(os2.homedir(), ".claude-bridge");
|
|
266
|
+
return path2.join(bridgeDir, "bridge.pid");
|
|
267
|
+
}
|
|
268
|
+
function removePidFile2() {
|
|
269
|
+
const pidFile = getPidFilePath2();
|
|
270
|
+
if (fs2.existsSync(pidFile)) {
|
|
271
|
+
fs2.unlinkSync(pidFile);
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
function readPidFile() {
|
|
275
|
+
const pidFile = getPidFilePath2();
|
|
276
|
+
if (!fs2.existsSync(pidFile)) {
|
|
277
|
+
return null;
|
|
278
|
+
}
|
|
279
|
+
try {
|
|
280
|
+
const content = fs2.readFileSync(pidFile, "utf-8").trim();
|
|
281
|
+
const pid = parseInt(content, 10);
|
|
282
|
+
if (isNaN(pid) || pid <= 0) {
|
|
283
|
+
logger2.warn({ content }, "Invalid PID file content");
|
|
284
|
+
return null;
|
|
285
|
+
}
|
|
286
|
+
return pid;
|
|
287
|
+
} catch (error) {
|
|
288
|
+
logger2.error({ error: error.message }, "Failed to read PID file");
|
|
289
|
+
return null;
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
function isProcessRunning(pid) {
|
|
293
|
+
try {
|
|
294
|
+
process.kill(pid, 0);
|
|
295
|
+
return true;
|
|
296
|
+
} catch {
|
|
297
|
+
return false;
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
async function waitForProcessExit(pid, timeoutMs = 5e3) {
|
|
301
|
+
const startTime = Date.now();
|
|
302
|
+
const checkInterval = 100;
|
|
303
|
+
while (Date.now() - startTime < timeoutMs) {
|
|
304
|
+
if (!isProcessRunning(pid)) {
|
|
305
|
+
return true;
|
|
306
|
+
}
|
|
307
|
+
await new Promise((resolve) => setTimeout(resolve, checkInterval));
|
|
308
|
+
}
|
|
309
|
+
return false;
|
|
310
|
+
}
|
|
311
|
+
async function stopBridge() {
|
|
312
|
+
const pid = readPidFile();
|
|
313
|
+
if (pid === null) {
|
|
314
|
+
console.log("Bridge is not running (no PID file found).");
|
|
315
|
+
return;
|
|
316
|
+
}
|
|
317
|
+
if (!isProcessRunning(pid)) {
|
|
318
|
+
console.log(`Bridge is not running (stale PID file, process ${pid} not found).`);
|
|
319
|
+
removePidFile2();
|
|
320
|
+
return;
|
|
321
|
+
}
|
|
322
|
+
console.log(`Stopping bridge (PID: ${pid})...`);
|
|
323
|
+
try {
|
|
324
|
+
process.kill(pid, "SIGTERM");
|
|
325
|
+
} catch (error) {
|
|
326
|
+
if (error.code === "ESRCH") {
|
|
327
|
+
console.log("Bridge process not found.");
|
|
328
|
+
removePidFile2();
|
|
329
|
+
return;
|
|
330
|
+
}
|
|
331
|
+
if (error.code === "EPERM") {
|
|
332
|
+
console.error(`Permission denied: cannot stop bridge (PID: ${pid}).`);
|
|
333
|
+
console.error("Try running with elevated privileges.");
|
|
334
|
+
process.exit(1);
|
|
335
|
+
}
|
|
336
|
+
throw error;
|
|
337
|
+
}
|
|
338
|
+
const exited = await waitForProcessExit(pid);
|
|
339
|
+
if (exited) {
|
|
340
|
+
console.log("Bridge stopped successfully.");
|
|
341
|
+
removePidFile2();
|
|
342
|
+
} else {
|
|
343
|
+
console.log("Bridge is still shutting down. Check status with: claude-bridge status");
|
|
344
|
+
console.log(`To force stop: kill -9 ${pid}`);
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
function createStopCommand() {
|
|
348
|
+
const command = new Command2("stop");
|
|
349
|
+
command.description("Stop the running bridge").action(async () => {
|
|
350
|
+
try {
|
|
351
|
+
await stopBridge();
|
|
352
|
+
} catch (error) {
|
|
353
|
+
logger2.error({ error: error.message }, "Failed to stop bridge");
|
|
354
|
+
console.error(`Failed to stop bridge: ${error.message}`);
|
|
355
|
+
process.exit(1);
|
|
356
|
+
}
|
|
357
|
+
});
|
|
358
|
+
return command;
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
// src/cli/commands/status.ts
|
|
362
|
+
import { Command as Command3 } from "commander";
|
|
363
|
+
import * as fs3 from "fs";
|
|
364
|
+
import * as path3 from "path";
|
|
365
|
+
import * as os3 from "os";
|
|
366
|
+
var logger3 = createLogger("cli:status");
|
|
367
|
+
function getPidFilePath3() {
|
|
368
|
+
const bridgeDir = path3.join(os3.homedir(), ".claude-bridge");
|
|
369
|
+
return path3.join(bridgeDir, "bridge.pid");
|
|
370
|
+
}
|
|
371
|
+
function getStatusFilePath() {
|
|
372
|
+
const bridgeDir = path3.join(os3.homedir(), ".claude-bridge");
|
|
373
|
+
return path3.join(bridgeDir, "status.json");
|
|
374
|
+
}
|
|
375
|
+
function readPidFile2() {
|
|
376
|
+
const pidFile = getPidFilePath3();
|
|
377
|
+
if (!fs3.existsSync(pidFile)) {
|
|
378
|
+
return null;
|
|
379
|
+
}
|
|
380
|
+
try {
|
|
381
|
+
const content = fs3.readFileSync(pidFile, "utf-8").trim();
|
|
382
|
+
const pid = parseInt(content, 10);
|
|
383
|
+
if (isNaN(pid) || pid <= 0) {
|
|
384
|
+
return null;
|
|
385
|
+
}
|
|
386
|
+
return pid;
|
|
387
|
+
} catch {
|
|
388
|
+
return null;
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
function isProcessRunning2(pid) {
|
|
392
|
+
try {
|
|
393
|
+
process.kill(pid, 0);
|
|
394
|
+
return true;
|
|
395
|
+
} catch {
|
|
396
|
+
return false;
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
function readStatusFile() {
|
|
400
|
+
const statusFile = getStatusFilePath();
|
|
401
|
+
if (!fs3.existsSync(statusFile)) {
|
|
402
|
+
return null;
|
|
403
|
+
}
|
|
404
|
+
try {
|
|
405
|
+
const content = fs3.readFileSync(statusFile, "utf-8");
|
|
406
|
+
return JSON.parse(content);
|
|
407
|
+
} catch {
|
|
408
|
+
return null;
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
function formatDate(dateStr) {
|
|
412
|
+
try {
|
|
413
|
+
const date = new Date(dateStr);
|
|
414
|
+
return date.toLocaleString();
|
|
415
|
+
} catch {
|
|
416
|
+
return dateStr;
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
function padRight(str, width) {
|
|
420
|
+
return str.padEnd(width);
|
|
421
|
+
}
|
|
422
|
+
function printPeerTable(peers) {
|
|
423
|
+
if (peers.length === 0) {
|
|
424
|
+
console.log(" No peers connected.");
|
|
425
|
+
return;
|
|
426
|
+
}
|
|
427
|
+
const cols = {
|
|
428
|
+
id: { title: "ID", width: 38 },
|
|
429
|
+
name: { title: "Name", width: 20 },
|
|
430
|
+
connected: { title: "Connected", width: 22 },
|
|
431
|
+
lastActivity: { title: "Last Activity", width: 22 }
|
|
432
|
+
};
|
|
433
|
+
console.log("");
|
|
434
|
+
console.log(
|
|
435
|
+
" " + padRight(cols.id.title, cols.id.width) + padRight(cols.name.title, cols.name.width) + padRight(cols.connected.title, cols.connected.width) + padRight(cols.lastActivity.title, cols.lastActivity.width)
|
|
436
|
+
);
|
|
437
|
+
const separator = " " + "-".repeat(cols.id.width - 1) + " " + "-".repeat(cols.name.width - 1) + " " + "-".repeat(cols.connected.width - 1) + " " + "-".repeat(cols.lastActivity.width - 1);
|
|
438
|
+
console.log(separator);
|
|
439
|
+
for (const peer of peers) {
|
|
440
|
+
const row = " " + padRight(peer.id, cols.id.width) + padRight(peer.name.slice(0, cols.name.width - 1), cols.name.width) + padRight(formatDate(peer.connectedAt), cols.connected.width) + padRight(formatDate(peer.lastActivity), cols.lastActivity.width);
|
|
441
|
+
console.log(row);
|
|
442
|
+
}
|
|
443
|
+
console.log("");
|
|
444
|
+
}
|
|
445
|
+
function getBridgeStatus() {
|
|
446
|
+
const pid = readPidFile2();
|
|
447
|
+
if (pid === null) {
|
|
448
|
+
return { running: false };
|
|
449
|
+
}
|
|
450
|
+
if (!isProcessRunning2(pid)) {
|
|
451
|
+
return { running: false };
|
|
452
|
+
}
|
|
453
|
+
const statusInfo = readStatusFile();
|
|
454
|
+
return {
|
|
455
|
+
running: true,
|
|
456
|
+
pid,
|
|
457
|
+
port: statusInfo?.port,
|
|
458
|
+
peers: statusInfo?.peers
|
|
459
|
+
};
|
|
460
|
+
}
|
|
461
|
+
function showStatus(options) {
|
|
462
|
+
console.log("Claude Code Bridge Status");
|
|
463
|
+
console.log("=".repeat(26));
|
|
464
|
+
console.log("");
|
|
465
|
+
const status = getBridgeStatus();
|
|
466
|
+
if (!status.running) {
|
|
467
|
+
console.log("Status: stopped");
|
|
468
|
+
console.log("");
|
|
469
|
+
console.log("To start the bridge:");
|
|
470
|
+
console.log(" claude-bridge start");
|
|
471
|
+
return;
|
|
472
|
+
}
|
|
473
|
+
console.log("Status: running");
|
|
474
|
+
console.log(`PID: ${status.pid}`);
|
|
475
|
+
if (status.port !== void 0) {
|
|
476
|
+
console.log(`Port: ${status.port}`);
|
|
477
|
+
}
|
|
478
|
+
console.log("");
|
|
479
|
+
console.log("Connected Peers:");
|
|
480
|
+
if (status.peers) {
|
|
481
|
+
printPeerTable(status.peers);
|
|
482
|
+
} else {
|
|
483
|
+
console.log(" Unable to retrieve peer information.");
|
|
484
|
+
console.log(" (Status file not found or unreadable)");
|
|
485
|
+
}
|
|
486
|
+
console.log("To stop the bridge:");
|
|
487
|
+
console.log(" claude-bridge stop");
|
|
488
|
+
}
|
|
489
|
+
function createStatusCommand() {
|
|
490
|
+
const command = new Command3("status");
|
|
491
|
+
command.description("Show bridge status and connected peers").option("-p, --port <port>", "Check status for a specific port").action((options) => {
|
|
492
|
+
try {
|
|
493
|
+
showStatus(options);
|
|
494
|
+
} catch (error) {
|
|
495
|
+
logger3.error({ error: error.message }, "Failed to get status");
|
|
496
|
+
console.error(`Failed to get status: ${error.message}`);
|
|
497
|
+
process.exit(1);
|
|
498
|
+
}
|
|
499
|
+
});
|
|
500
|
+
return command;
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
// src/cli/commands/connect.ts
|
|
504
|
+
import { Command as Command4 } from "commander";
|
|
505
|
+
var logger4 = createLogger("cli:connect");
|
|
506
|
+
function validateWebSocketUrl(url) {
|
|
507
|
+
if (!url.startsWith("ws://") && !url.startsWith("wss://")) {
|
|
508
|
+
throw new Error(`Invalid URL protocol. Expected ws:// or wss://, got: ${url}`);
|
|
509
|
+
}
|
|
510
|
+
try {
|
|
511
|
+
const parsed = new URL(url);
|
|
512
|
+
if (!parsed.hostname) {
|
|
513
|
+
throw new Error("URL must include a hostname");
|
|
514
|
+
}
|
|
515
|
+
if (!parsed.port) {
|
|
516
|
+
const defaultPort = parsed.protocol === "wss:" ? "443" : "80";
|
|
517
|
+
logger4.debug(`No port specified, will use default: ${defaultPort}`);
|
|
518
|
+
}
|
|
519
|
+
return true;
|
|
520
|
+
} catch (error) {
|
|
521
|
+
if (error instanceof Error && error.message.includes("Invalid URL")) {
|
|
522
|
+
throw new Error(`Invalid URL format: ${url}`);
|
|
523
|
+
}
|
|
524
|
+
throw error;
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
async function connectToBridge(url) {
|
|
528
|
+
try {
|
|
529
|
+
validateWebSocketUrl(url);
|
|
530
|
+
} catch (error) {
|
|
531
|
+
console.error(`Error: ${error.message}`);
|
|
532
|
+
process.exit(1);
|
|
533
|
+
}
|
|
534
|
+
console.log(`Connecting to bridge at ${url}...`);
|
|
535
|
+
const transport = new WebSocketTransport();
|
|
536
|
+
const timeoutMs = 1e4;
|
|
537
|
+
const timeout = setTimeout(() => {
|
|
538
|
+
console.error(`Error: Connection timeout after ${timeoutMs / 1e3} seconds`);
|
|
539
|
+
transport.disconnect().catch(() => {
|
|
540
|
+
});
|
|
541
|
+
process.exit(1);
|
|
542
|
+
}, timeoutMs);
|
|
543
|
+
try {
|
|
544
|
+
await transport.connect({
|
|
545
|
+
url,
|
|
546
|
+
reconnect: false
|
|
547
|
+
// Don't auto-reconnect for this test
|
|
548
|
+
});
|
|
549
|
+
clearTimeout(timeout);
|
|
550
|
+
if (transport.getState() === "CONNECTED" /* CONNECTED */) {
|
|
551
|
+
console.log("Successfully connected to bridge!");
|
|
552
|
+
console.log("");
|
|
553
|
+
console.log("Connection details:");
|
|
554
|
+
console.log(` URL: ${url}`);
|
|
555
|
+
console.log(` State: ${ConnectionState[transport.getState()]}`);
|
|
556
|
+
console.log("");
|
|
557
|
+
console.log("To start a bridge that auto-connects on startup:");
|
|
558
|
+
console.log(` claude-bridge start --connect ${url}`);
|
|
559
|
+
} else {
|
|
560
|
+
console.error(`Connection failed. State: ${ConnectionState[transport.getState()]}`);
|
|
561
|
+
process.exit(1);
|
|
562
|
+
}
|
|
563
|
+
await transport.disconnect();
|
|
564
|
+
console.log("\nConnection test complete. Disconnected.");
|
|
565
|
+
} catch (error) {
|
|
566
|
+
clearTimeout(timeout);
|
|
567
|
+
const errorMessage = error.message;
|
|
568
|
+
console.error(`Error: Failed to connect - ${errorMessage}`);
|
|
569
|
+
if (errorMessage.includes("ECONNREFUSED")) {
|
|
570
|
+
console.log("");
|
|
571
|
+
console.log("Suggestions:");
|
|
572
|
+
console.log(" - Ensure a bridge is running at the specified address");
|
|
573
|
+
console.log(" - Check the port number is correct");
|
|
574
|
+
console.log(" - Verify no firewall is blocking the connection");
|
|
575
|
+
} else if (errorMessage.includes("ENOTFOUND")) {
|
|
576
|
+
console.log("");
|
|
577
|
+
console.log("Suggestions:");
|
|
578
|
+
console.log(" - Check the hostname is correct");
|
|
579
|
+
console.log(" - Ensure DNS resolution is working");
|
|
580
|
+
}
|
|
581
|
+
process.exit(1);
|
|
582
|
+
}
|
|
583
|
+
}
|
|
584
|
+
function createConnectCommand() {
|
|
585
|
+
const command = new Command4("connect");
|
|
586
|
+
command.description("Connect to a remote bridge").argument("<url>", "WebSocket URL to connect to (e.g., ws://localhost:8765)").action(async (url) => {
|
|
587
|
+
try {
|
|
588
|
+
await connectToBridge(url);
|
|
589
|
+
} catch (error) {
|
|
590
|
+
logger4.error({ error: error.message }, "Connect command failed");
|
|
591
|
+
console.error(`Error: ${error.message}`);
|
|
592
|
+
process.exit(1);
|
|
593
|
+
}
|
|
594
|
+
});
|
|
595
|
+
return command;
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
// src/cli/commands/discover.ts
|
|
599
|
+
import { Command as Command5 } from "commander";
|
|
600
|
+
var logger5 = createLogger("cli:discover");
|
|
601
|
+
function printPeersTable(peers) {
|
|
602
|
+
if (peers.length === 0) {
|
|
603
|
+
console.log("No bridges found.");
|
|
604
|
+
console.log("");
|
|
605
|
+
console.log("Suggestions:");
|
|
606
|
+
console.log(" - Start a bridge: claude-bridge start");
|
|
607
|
+
console.log(" - Add bridge labels to Docker containers");
|
|
608
|
+
console.log(" - Install bridge addon in your Docksal/DDEV project");
|
|
609
|
+
return;
|
|
610
|
+
}
|
|
611
|
+
const cols = {
|
|
612
|
+
name: { title: "Name", width: 25 },
|
|
613
|
+
source: { title: "Source", width: 12 },
|
|
614
|
+
url: { title: "URL", width: 35 },
|
|
615
|
+
status: { title: "Status", width: 12 }
|
|
616
|
+
};
|
|
617
|
+
console.log(
|
|
618
|
+
cols.name.title.padEnd(cols.name.width) + cols.source.title.padEnd(cols.source.width) + cols.url.title.padEnd(cols.url.width) + cols.status.title.padEnd(cols.status.width)
|
|
619
|
+
);
|
|
620
|
+
console.log(
|
|
621
|
+
"-".repeat(cols.name.width - 1) + " " + "-".repeat(cols.source.width - 1) + " " + "-".repeat(cols.url.width - 1) + " " + "-".repeat(cols.status.width - 1)
|
|
622
|
+
);
|
|
623
|
+
for (const peer of peers) {
|
|
624
|
+
const row = peer.name.slice(0, cols.name.width - 1).padEnd(cols.name.width) + peer.source.padEnd(cols.source.width) + peer.url.slice(0, cols.url.width - 1).padEnd(cols.url.width) + (peer.status || "unknown").padEnd(cols.status.width);
|
|
625
|
+
console.log(row);
|
|
626
|
+
}
|
|
627
|
+
console.log("");
|
|
628
|
+
console.log("To connect to a bridge:");
|
|
629
|
+
console.log(" claude-bridge connect <url>");
|
|
630
|
+
}
|
|
631
|
+
async function discoverBridges() {
|
|
632
|
+
console.log("Discovering bridges on local network...\n");
|
|
633
|
+
const allPeers = [];
|
|
634
|
+
const dockerPeers = discoverDockerPeers();
|
|
635
|
+
allPeers.push(...dockerPeers);
|
|
636
|
+
logger5.debug({ count: dockerPeers.length }, "Docker peers discovered");
|
|
637
|
+
const docksalPeers = discoverDocksalProjects();
|
|
638
|
+
allPeers.push(...docksalPeers);
|
|
639
|
+
logger5.debug({ count: docksalPeers.length }, "Docksal peers discovered");
|
|
640
|
+
const ddevPeers = discoverDdevProjects();
|
|
641
|
+
allPeers.push(...ddevPeers);
|
|
642
|
+
logger5.debug({ count: ddevPeers.length }, "DDEV peers discovered");
|
|
643
|
+
const landoPeers = discoverLandoProjects();
|
|
644
|
+
allPeers.push(...landoPeers);
|
|
645
|
+
logger5.debug({ count: landoPeers.length }, "Lando peers discovered");
|
|
646
|
+
printPeersTable(allPeers);
|
|
647
|
+
}
|
|
648
|
+
function createDiscoverCommand() {
|
|
649
|
+
const command = new Command5("discover");
|
|
650
|
+
command.description("Discover bridges on local network").action(async () => {
|
|
651
|
+
try {
|
|
652
|
+
await discoverBridges();
|
|
653
|
+
} catch (error) {
|
|
654
|
+
logger5.error({ error: error.message }, "Discover command failed");
|
|
655
|
+
console.error(`Error: ${error.message}`);
|
|
656
|
+
process.exit(1);
|
|
657
|
+
}
|
|
658
|
+
});
|
|
659
|
+
return command;
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
// src/cli/commands/info.ts
|
|
663
|
+
import { Command as Command6 } from "commander";
|
|
664
|
+
import * as fs4 from "fs";
|
|
665
|
+
import * as path4 from "path";
|
|
666
|
+
import * as os4 from "os";
|
|
667
|
+
var logger6 = createLogger("cli:info");
|
|
668
|
+
function getConfigFilePath() {
|
|
669
|
+
const localConfig = path4.join(process.cwd(), ".claude-bridge.yml");
|
|
670
|
+
if (fs4.existsSync(localConfig)) {
|
|
671
|
+
return localConfig;
|
|
672
|
+
}
|
|
673
|
+
const homeConfig = path4.join(os4.homedir(), ".claude-bridge", "config.yml");
|
|
674
|
+
if (fs4.existsSync(homeConfig)) {
|
|
675
|
+
return homeConfig;
|
|
676
|
+
}
|
|
677
|
+
return null;
|
|
678
|
+
}
|
|
679
|
+
function formatValue(value) {
|
|
680
|
+
if (value === void 0) {
|
|
681
|
+
return "(not set)";
|
|
682
|
+
}
|
|
683
|
+
if (value === null) {
|
|
684
|
+
return "(null)";
|
|
685
|
+
}
|
|
686
|
+
if (typeof value === "object") {
|
|
687
|
+
return JSON.stringify(value);
|
|
688
|
+
}
|
|
689
|
+
return String(value);
|
|
690
|
+
}
|
|
691
|
+
function printSection(title) {
|
|
692
|
+
console.log("");
|
|
693
|
+
console.log(title);
|
|
694
|
+
console.log("-".repeat(title.length));
|
|
695
|
+
}
|
|
696
|
+
function printKeyValue(key, value, indent = 0) {
|
|
697
|
+
const prefix = " ".repeat(indent);
|
|
698
|
+
console.log(`${prefix}${key}: ${formatValue(value)}`);
|
|
699
|
+
}
|
|
700
|
+
function showInfo(globalOptions) {
|
|
701
|
+
console.log("Claude Code Bridge - Environment Information");
|
|
702
|
+
console.log("=".repeat(45));
|
|
703
|
+
printSection("Environment");
|
|
704
|
+
const env = detectEnvironment();
|
|
705
|
+
printKeyValue("Type", env.type);
|
|
706
|
+
printKeyValue("Platform", env.platform);
|
|
707
|
+
printKeyValue("Is Container", env.isContainer);
|
|
708
|
+
printKeyValue("Project Name", env.projectName);
|
|
709
|
+
printKeyValue("Project Root", env.projectRoot);
|
|
710
|
+
printSection("Network");
|
|
711
|
+
const hostGateway = getHostGateway();
|
|
712
|
+
const envDefaults = getDefaultConfig(env);
|
|
713
|
+
const listenPort = envDefaults.listen?.port ?? 8765;
|
|
714
|
+
const listenHost = envDefaults.listen?.host ?? "0.0.0.0";
|
|
715
|
+
printKeyValue("Host Gateway", hostGateway);
|
|
716
|
+
printKeyValue("Recommended Port", listenPort);
|
|
717
|
+
printKeyValue("Default Host", listenHost);
|
|
718
|
+
if (env.isContainer) {
|
|
719
|
+
console.log("");
|
|
720
|
+
console.log(" Note: To connect from host machine:");
|
|
721
|
+
console.log(` ws://localhost:${listenPort}`);
|
|
722
|
+
console.log("");
|
|
723
|
+
console.log(" To connect to host from this container:");
|
|
724
|
+
console.log(` ws://${hostGateway}:8766`);
|
|
725
|
+
}
|
|
726
|
+
printSection("Configuration");
|
|
727
|
+
const configPath = globalOptions.config || getConfigFilePath();
|
|
728
|
+
printKeyValue("Config File", configPath || "(using defaults)");
|
|
729
|
+
const config = loadConfigSync(globalOptions.config);
|
|
730
|
+
console.log("");
|
|
731
|
+
console.log(" Current Settings:");
|
|
732
|
+
printKeyValue("Mode", config.mode, 2);
|
|
733
|
+
printKeyValue("Instance Name", config.instanceName, 2);
|
|
734
|
+
console.log("");
|
|
735
|
+
console.log(" Listen:");
|
|
736
|
+
printKeyValue("Port", config.listen.port, 2);
|
|
737
|
+
printKeyValue("Host", config.listen.host, 2);
|
|
738
|
+
console.log("");
|
|
739
|
+
console.log(" Connect:");
|
|
740
|
+
printKeyValue("URL", config.connect?.url, 2);
|
|
741
|
+
console.log("");
|
|
742
|
+
console.log(" Context Sharing:");
|
|
743
|
+
printKeyValue("Auto Sync", config.contextSharing.autoSync, 2);
|
|
744
|
+
printKeyValue("Sync Interval", `${config.contextSharing.syncInterval}ms`, 2);
|
|
745
|
+
printKeyValue("Max Chunk Tokens", config.contextSharing.maxChunkTokens, 2);
|
|
746
|
+
if (config.contextSharing.includePatterns.length > 0) {
|
|
747
|
+
console.log(" Include Patterns:");
|
|
748
|
+
for (const pattern of config.contextSharing.includePatterns) {
|
|
749
|
+
console.log(` - ${pattern}`);
|
|
750
|
+
}
|
|
751
|
+
}
|
|
752
|
+
if (config.contextSharing.excludePatterns.length > 0) {
|
|
753
|
+
console.log(" Exclude Patterns:");
|
|
754
|
+
for (const pattern of config.contextSharing.excludePatterns) {
|
|
755
|
+
console.log(` - ${pattern}`);
|
|
756
|
+
}
|
|
757
|
+
}
|
|
758
|
+
console.log("");
|
|
759
|
+
console.log(" Interaction:");
|
|
760
|
+
printKeyValue("Require Confirmation", config.interaction.requireConfirmation, 2);
|
|
761
|
+
printKeyValue("Notify On Activity", config.interaction.notifyOnActivity, 2);
|
|
762
|
+
printKeyValue("Task Timeout", `${config.interaction.taskTimeout}ms`, 2);
|
|
763
|
+
printSection("System");
|
|
764
|
+
printKeyValue("Node Version", process.version);
|
|
765
|
+
printKeyValue("OS", `${os4.type()} ${os4.release()}`);
|
|
766
|
+
printKeyValue("Arch", os4.arch());
|
|
767
|
+
printKeyValue("Home Directory", os4.homedir());
|
|
768
|
+
printKeyValue("Working Directory", process.cwd());
|
|
769
|
+
printSection("Environment Variables");
|
|
770
|
+
const relevantEnvVars = [
|
|
771
|
+
"DOCKSAL_STACK",
|
|
772
|
+
"DOCKSAL_PROJECT",
|
|
773
|
+
"IS_DDEV_PROJECT",
|
|
774
|
+
"DDEV_PROJECT",
|
|
775
|
+
"LANDO",
|
|
776
|
+
"LANDO_APP_NAME",
|
|
777
|
+
"LOG_LEVEL"
|
|
778
|
+
];
|
|
779
|
+
let hasEnvVars = false;
|
|
780
|
+
for (const varName of relevantEnvVars) {
|
|
781
|
+
const value = process.env[varName];
|
|
782
|
+
if (value !== void 0) {
|
|
783
|
+
printKeyValue(varName, value);
|
|
784
|
+
hasEnvVars = true;
|
|
785
|
+
}
|
|
786
|
+
}
|
|
787
|
+
if (!hasEnvVars) {
|
|
788
|
+
console.log(" (no relevant environment variables set)");
|
|
789
|
+
}
|
|
790
|
+
console.log("");
|
|
791
|
+
}
|
|
792
|
+
function createInfoCommand() {
|
|
793
|
+
const command = new Command6("info");
|
|
794
|
+
command.description("Show environment and configuration info").action(() => {
|
|
795
|
+
try {
|
|
796
|
+
const globalOptions = command.parent?.opts() ?? {};
|
|
797
|
+
showInfo(globalOptions);
|
|
798
|
+
} catch (error) {
|
|
799
|
+
logger6.error({ error: error.message }, "Info command failed");
|
|
800
|
+
console.error(`Error: ${error.message}`);
|
|
801
|
+
process.exit(1);
|
|
802
|
+
}
|
|
803
|
+
});
|
|
804
|
+
return command;
|
|
805
|
+
}
|
|
806
|
+
|
|
807
|
+
// src/cli/index.ts
|
|
808
|
+
var VERSION = "0.1.0";
|
|
809
|
+
var logger7 = createLogger("cli");
|
|
810
|
+
function createProgram() {
|
|
811
|
+
const program = new Command7();
|
|
812
|
+
program.name("claude-bridge").description("Bidirectional communication system for Claude Code instances across environments").version(VERSION, "-V, --version", "Output the version number").option("-v, --verbose", "Enable verbose logging").option("--config <path>", "Path to config file");
|
|
813
|
+
program.hook("preAction", (thisCommand) => {
|
|
814
|
+
const opts = thisCommand.opts();
|
|
815
|
+
if (opts.verbose) {
|
|
816
|
+
process.env.LOG_LEVEL = "debug";
|
|
817
|
+
}
|
|
818
|
+
});
|
|
819
|
+
return program;
|
|
820
|
+
}
|
|
821
|
+
function getGlobalOptions(program) {
|
|
822
|
+
return program.opts();
|
|
823
|
+
}
|
|
824
|
+
async function main() {
|
|
825
|
+
const program = createProgram();
|
|
826
|
+
program.addCommand(createStartCommand());
|
|
827
|
+
program.addCommand(createStopCommand());
|
|
828
|
+
program.addCommand(createStatusCommand());
|
|
829
|
+
program.addCommand(createConnectCommand());
|
|
830
|
+
program.addCommand(createDiscoverCommand());
|
|
831
|
+
program.addCommand(createInfoCommand());
|
|
832
|
+
await program.parseAsync(process.argv);
|
|
833
|
+
}
|
|
834
|
+
main().catch((error) => {
|
|
835
|
+
logger7.error({ err: error }, "CLI error");
|
|
836
|
+
process.exit(1);
|
|
837
|
+
});
|
|
838
|
+
export {
|
|
839
|
+
createProgram as createCLI,
|
|
840
|
+
createProgram,
|
|
841
|
+
getGlobalOptions,
|
|
842
|
+
main
|
|
843
|
+
};
|
|
844
|
+
//# sourceMappingURL=cli.js.map
|