@willjackson/claude-code-bridge 0.3.0 → 0.5.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/dist/cli.js CHANGED
@@ -1,20 +1,22 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  Bridge,
4
+ BridgeMcpServer,
4
5
  ConnectionState,
5
6
  WebSocketTransport,
6
7
  createLogger,
7
8
  loadConfigSync
8
- } from "./chunk-LUL3SX2F.js";
9
+ } from "./chunk-MHUQYPTB.js";
9
10
 
10
11
  // src/cli/index.ts
11
- import { Command as Command6 } from "commander";
12
+ import { Command as Command7 } from "commander";
12
13
 
13
14
  // src/cli/commands/start.ts
14
15
  import { Command } from "commander";
15
16
  import * as fs from "fs";
16
17
  import * as path from "path";
17
18
  import * as os from "os";
19
+ import { spawn } from "child_process";
18
20
  import { minimatch } from "minimatch";
19
21
 
20
22
  // src/cli/utils.ts
@@ -72,10 +74,10 @@ Received ${signal}, shutting down gracefully...`);
72
74
  };
73
75
  }
74
76
  function handleUnhandledRejections(options = {}) {
75
- const { exit = false, logger: logger7 = console.error } = options;
77
+ const { exit = false, logger: logger8 = console.error } = options;
76
78
  process.on("unhandledRejection", (reason, promise) => {
77
79
  const error = reason instanceof Error ? reason : new Error(String(reason));
78
- logger7("Unhandled promise rejection:", error);
80
+ logger8("Unhandled promise rejection:", error);
79
81
  if (exit) {
80
82
  process.exit(1);
81
83
  }
@@ -296,6 +298,59 @@ function registerHandlers(bridge, config) {
296
298
  };
297
299
  }
298
300
  }
301
+ if (taskData?.action === "list_directory") {
302
+ const dirPath = taskData.path;
303
+ if (!dirPath) {
304
+ return {
305
+ success: false,
306
+ data: { error: "list_directory requires path" }
307
+ };
308
+ }
309
+ const fullPath = path.isAbsolute(dirPath) ? dirPath : path.join(cwd, dirPath);
310
+ const resolvedPath = path.resolve(fullPath);
311
+ const resolvedCwd = path.resolve(cwd);
312
+ if (!resolvedPath.startsWith(resolvedCwd)) {
313
+ return {
314
+ success: false,
315
+ data: { error: "Cannot list directories outside project directory" }
316
+ };
317
+ }
318
+ try {
319
+ if (!fs.existsSync(resolvedPath)) {
320
+ return {
321
+ success: false,
322
+ data: { error: `Directory not found: ${dirPath}` }
323
+ };
324
+ }
325
+ const stats = fs.statSync(resolvedPath);
326
+ if (!stats.isDirectory()) {
327
+ return {
328
+ success: false,
329
+ data: { error: `Not a directory: ${dirPath}` }
330
+ };
331
+ }
332
+ const entries = fs.readdirSync(resolvedPath, { withFileTypes: true });
333
+ const listing = entries.map((entry) => ({
334
+ name: entry.name,
335
+ type: entry.isDirectory() ? "directory" : "file"
336
+ }));
337
+ logger.info({ path: dirPath, count: listing.length }, "Directory listed successfully");
338
+ return {
339
+ success: true,
340
+ data: {
341
+ action: "list_directory",
342
+ path: dirPath,
343
+ entries: listing
344
+ }
345
+ };
346
+ } catch (err) {
347
+ logger.error({ error: err.message, path: dirPath }, "Failed to list directory");
348
+ return {
349
+ success: false,
350
+ data: { error: `Failed to list directory: ${err.message}` }
351
+ };
352
+ }
353
+ }
299
354
  const projectInfo = {
300
355
  cwd,
301
356
  platform: process.platform,
@@ -408,10 +463,12 @@ function buildBridgeConfig(options, globalOptions) {
408
463
  return finalConfig;
409
464
  }
410
465
  async function startBridge(options, globalOptions) {
411
- const { running, pid } = isAlreadyRunning();
412
- if (running) {
413
- console.error(`Bridge is already running (PID: ${pid})`);
414
- process.exit(1);
466
+ if (!process.env.CLAUDE_BRIDGE_DAEMON_CHILD) {
467
+ const { running, pid } = isAlreadyRunning();
468
+ if (running) {
469
+ console.error(`Bridge is already running (PID: ${pid})`);
470
+ process.exit(1);
471
+ }
415
472
  }
416
473
  const config = buildBridgeConfig(options, globalOptions);
417
474
  console.log("Starting Claude Code Bridge...");
@@ -423,9 +480,34 @@ async function startBridge(options, globalOptions) {
423
480
  if (config.connect) {
424
481
  console.log(` Connecting to: ${config.connect.url}`);
425
482
  }
426
- if (options.daemon) {
427
- console.log(" Running in daemon mode");
428
- writePidFile(process.pid);
483
+ if (options.daemon && !process.env.CLAUDE_BRIDGE_DAEMON_CHILD) {
484
+ const logFile = path.join(os.homedir(), ".claude-bridge", "bridge.log");
485
+ ensureBridgeDir();
486
+ const args = process.argv.slice(2).filter((arg) => arg !== "-d" && arg !== "--daemon");
487
+ const out = fs.openSync(logFile, "a");
488
+ const err = fs.openSync(logFile, "a");
489
+ const child = spawn(process.execPath, [process.argv[1], ...args], {
490
+ detached: true,
491
+ stdio: ["ignore", out, err],
492
+ env: { ...process.env, CLAUDE_BRIDGE_DAEMON_CHILD: "1" }
493
+ });
494
+ child.unref();
495
+ writePidFile(child.pid);
496
+ console.log(` Running in background (PID: ${child.pid})`);
497
+ console.log(` Log file: ${logFile}`);
498
+ console.log(` Use 'claude-bridge status' to check status`);
499
+ console.log(` Use 'claude-bridge stop' to stop the bridge`);
500
+ if (options.launchClaude) {
501
+ console.log("\nLaunching Claude Code...");
502
+ await new Promise((resolve2) => setTimeout(resolve2, 500));
503
+ const { spawnSync } = await import("child_process");
504
+ const result = spawnSync("claude", [], {
505
+ stdio: "inherit",
506
+ shell: true
507
+ });
508
+ process.exit(result.status ?? 0);
509
+ }
510
+ process.exit(0);
429
511
  }
430
512
  const bridge = new Bridge(config);
431
513
  setupGracefulShutdown({
@@ -434,7 +516,7 @@ async function startBridge(options, globalOptions) {
434
516
  await bridge.stop();
435
517
  logger.info("Bridge stopped");
436
518
  },
437
- afterCleanup: options.daemon ? removePidFile : void 0,
519
+ afterCleanup: process.env.CLAUDE_BRIDGE_DAEMON_CHILD ? removePidFile : void 0,
438
520
  verbose: true,
439
521
  timeout: 1e4
440
522
  });
@@ -451,7 +533,7 @@ async function startBridge(options, globalOptions) {
451
533
  excludePatterns: fileConfig.contextSharing.excludePatterns
452
534
  });
453
535
  }
454
- if (options.daemon) {
536
+ if (process.env.CLAUDE_BRIDGE_DAEMON_CHILD) {
455
537
  writePidFile(process.pid);
456
538
  }
457
539
  console.log("Bridge started successfully.");
@@ -461,13 +543,13 @@ async function startBridge(options, globalOptions) {
461
543
  To connect from another bridge:`);
462
544
  console.log(` claude-bridge connect ws://localhost:${config.listen.port}`);
463
545
  }
464
- if (!options.daemon) {
546
+ if (!process.env.CLAUDE_BRIDGE_DAEMON_CHILD) {
465
547
  console.log("\nPress Ctrl+C to stop.");
466
548
  }
467
549
  } catch (error) {
468
550
  logger.error({ error: error.message }, "Failed to start bridge");
469
551
  console.error(`Failed to start bridge: ${error.message}`);
470
- if (options.daemon) {
552
+ if (process.env.CLAUDE_BRIDGE_DAEMON_CHILD) {
471
553
  removePidFile();
472
554
  }
473
555
  process.exit(1);
@@ -475,7 +557,10 @@ To connect from another bridge:`);
475
557
  }
476
558
  function createStartCommand() {
477
559
  const command = new Command("start");
478
- command.description("Start the bridge server").option("-p, --port <port>", "Port to listen on (default: 8765)").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("-d, --daemon", "Run in background").option("--with-handlers", "Enable file reading and task handling capabilities").action(async (options) => {
560
+ command.description("Start the bridge server").option("-p, --port <port>", "Port to listen on (default: 8765)").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("-d, --daemon", "Run in background").option("--with-handlers", "Enable file reading and task handling capabilities").option("--launch-claude", "Start bridge daemon and launch Claude Code").action(async (options) => {
561
+ if (options.launchClaude) {
562
+ options.daemon = true;
563
+ }
479
564
  const globalOptions = command.parent?.opts();
480
565
  await startBridge(options, globalOptions);
481
566
  });
@@ -935,11 +1020,140 @@ function createInfoCommand() {
935
1020
  return command;
936
1021
  }
937
1022
 
1023
+ // src/cli/commands/mcp-server.ts
1024
+ import { Command as Command6 } from "commander";
1025
+ import * as fs5 from "fs";
1026
+ import * as path5 from "path";
1027
+ import * as os5 from "os";
1028
+ import { spawn as spawn2 } from "child_process";
1029
+ var logger6 = createLogger("cli:mcp-server");
1030
+ var DEFAULT_DAEMON_PORT = 8766;
1031
+ function getStatusFilePath2() {
1032
+ const bridgeDir = path5.join(os5.homedir(), ".claude-bridge");
1033
+ return path5.join(bridgeDir, "status.json");
1034
+ }
1035
+ function getPidFilePath4() {
1036
+ const bridgeDir = path5.join(os5.homedir(), ".claude-bridge");
1037
+ return path5.join(bridgeDir, "bridge.pid");
1038
+ }
1039
+ function isDaemonRunning() {
1040
+ const pidFile = getPidFilePath4();
1041
+ if (!fs5.existsSync(pidFile)) {
1042
+ return { running: false };
1043
+ }
1044
+ try {
1045
+ const pid = parseInt(fs5.readFileSync(pidFile, "utf-8").trim(), 10);
1046
+ process.kill(pid, 0);
1047
+ const statusFile = getStatusFilePath2();
1048
+ if (fs5.existsSync(statusFile)) {
1049
+ const status = JSON.parse(fs5.readFileSync(statusFile, "utf-8"));
1050
+ return { running: true, pid, port: status.port };
1051
+ }
1052
+ return { running: true, pid };
1053
+ } catch {
1054
+ return { running: false };
1055
+ }
1056
+ }
1057
+ async function waitForDaemon(port, timeoutMs) {
1058
+ const startTime = Date.now();
1059
+ while (Date.now() - startTime < timeoutMs) {
1060
+ const { running, port: runningPort } = isDaemonRunning();
1061
+ if (running && (runningPort === port || runningPort === void 0)) {
1062
+ await new Promise((resolve2) => setTimeout(resolve2, 500));
1063
+ return;
1064
+ }
1065
+ await new Promise((resolve2) => setTimeout(resolve2, 200));
1066
+ }
1067
+ throw new Error(`Daemon did not start within ${timeoutMs}ms`);
1068
+ }
1069
+ function ensureBridgeDir2() {
1070
+ const bridgeDir = path5.join(os5.homedir(), ".claude-bridge");
1071
+ if (!fs5.existsSync(bridgeDir)) {
1072
+ fs5.mkdirSync(bridgeDir, { recursive: true });
1073
+ }
1074
+ }
1075
+ async function ensureDaemonRunning(port) {
1076
+ const { running, pid, port: runningPort } = isDaemonRunning();
1077
+ if (running) {
1078
+ const effectivePort = runningPort ?? port;
1079
+ console.error(`[MCP] Bridge daemon already running (PID: ${pid}, port: ${effectivePort})`);
1080
+ return effectivePort;
1081
+ }
1082
+ console.error("[MCP] Starting bridge daemon...");
1083
+ ensureBridgeDir2();
1084
+ const logFile = path5.join(os5.homedir(), ".claude-bridge", "bridge.log");
1085
+ const out = fs5.openSync(logFile, "a");
1086
+ const err = fs5.openSync(logFile, "a");
1087
+ const cliPath = process.argv[1];
1088
+ const child = spawn2(process.execPath, [cliPath, "start", "--daemon", "--port", String(port)], {
1089
+ detached: true,
1090
+ stdio: ["ignore", out, err]
1091
+ });
1092
+ child.unref();
1093
+ console.error(`[MCP] Daemon starting (PID: ${child.pid})`);
1094
+ try {
1095
+ await waitForDaemon(port, 5e3);
1096
+ console.error(`[MCP] Daemon ready on port ${port}`);
1097
+ return port;
1098
+ } catch (error) {
1099
+ console.error(`[MCP] Warning: Could not verify daemon is ready: ${error.message}`);
1100
+ return port;
1101
+ }
1102
+ }
1103
+ async function startMcpServer(options, _globalOptions) {
1104
+ let bridgeUrl = options.connect ?? `ws://localhost:${DEFAULT_DAEMON_PORT}`;
1105
+ if (!options.connect) {
1106
+ try {
1107
+ const port = await ensureDaemonRunning(DEFAULT_DAEMON_PORT);
1108
+ bridgeUrl = `ws://localhost:${port}`;
1109
+ } catch (error) {
1110
+ console.error(`[MCP] Warning: Could not ensure daemon is running: ${error.message}`);
1111
+ }
1112
+ }
1113
+ const config = {
1114
+ bridgeUrl,
1115
+ name: options.name ?? "claude-bridge",
1116
+ version: "0.4.0",
1117
+ instanceName: `mcp-server-${process.pid}`,
1118
+ taskTimeout: 6e4
1119
+ };
1120
+ const server = new BridgeMcpServer(config);
1121
+ setupGracefulShutdown({
1122
+ cleanup: async () => {
1123
+ console.error("[MCP] Shutting down...");
1124
+ await server.stop();
1125
+ },
1126
+ verbose: false,
1127
+ timeout: 5e3
1128
+ });
1129
+ handleUnhandledRejections({
1130
+ exit: true,
1131
+ logger: (msg, err) => {
1132
+ console.error(`[MCP] ${msg}: ${err.message}`);
1133
+ logger6.error({ error: err.message }, msg);
1134
+ }
1135
+ });
1136
+ try {
1137
+ await server.start();
1138
+ } catch (error) {
1139
+ console.error(`[MCP] Failed to start MCP server: ${error.message}`);
1140
+ process.exit(1);
1141
+ }
1142
+ }
1143
+ function createMcpServerCommand() {
1144
+ const command = new Command6("mcp-server");
1145
+ command.description("Start the MCP server for Claude Code integration").option("-c, --connect <url>", `Bridge WebSocket URL (default: ws://localhost:${DEFAULT_DAEMON_PORT})`).option("--name <name>", "MCP server name (default: claude-bridge)").action(async (options) => {
1146
+ const globalOptions = command.parent?.opts();
1147
+ await startMcpServer(options, globalOptions);
1148
+ });
1149
+ return command;
1150
+ }
1151
+
938
1152
  // src/cli/index.ts
939
1153
  var VERSION = "0.1.0";
940
- var logger6 = createLogger("cli");
1154
+ var logger7 = createLogger("cli");
941
1155
  function createProgram() {
942
- const program = new Command6();
1156
+ const program = new Command7();
943
1157
  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");
944
1158
  program.hook("preAction", (thisCommand) => {
945
1159
  const opts = thisCommand.opts();
@@ -959,10 +1173,11 @@ async function main() {
959
1173
  program.addCommand(createStatusCommand());
960
1174
  program.addCommand(createConnectCommand());
961
1175
  program.addCommand(createInfoCommand());
1176
+ program.addCommand(createMcpServerCommand());
962
1177
  await program.parseAsync(process.argv);
963
1178
  }
964
1179
  main().catch((error) => {
965
- logger6.error({ err: error }, "CLI error");
1180
+ logger7.error({ err: error }, "CLI error");
966
1181
  process.exit(1);
967
1182
  });
968
1183
  export {