@boxcrew/cli 0.1.5 → 0.1.6

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/dist/index.js +184 -104
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -359,9 +359,36 @@ function registerApiCommand(program2) {
359
359
  // src/commands/connect.ts
360
360
  import { spawn } from "child_process";
361
361
  import { createInterface as createInterface3 } from "readline";
362
+ import { openSync, mkdirSync, existsSync, writeFileSync, readFileSync, unlinkSync } from "fs";
363
+ import { join } from "path";
364
+ import { homedir } from "os";
362
365
  import WebSocket from "ws";
363
366
  var RECONNECT_BASE_MS = 1e3;
364
367
  var RECONNECT_MAX_MS = 3e4;
368
+ function getStateDir() {
369
+ const dir = join(homedir(), ".boxcrew");
370
+ if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
371
+ return dir;
372
+ }
373
+ function getPidFile(agentName) {
374
+ return join(getStateDir(), `${agentName}.pid`);
375
+ }
376
+ function getLogFile(agentName) {
377
+ return join(getStateDir(), `${agentName}.log`);
378
+ }
379
+ function readPid(agentName) {
380
+ const pidFile = getPidFile(agentName);
381
+ if (!existsSync(pidFile)) return null;
382
+ const pid = parseInt(readFileSync(pidFile, "utf-8").trim(), 10);
383
+ if (isNaN(pid)) return null;
384
+ try {
385
+ process.kill(pid, 0);
386
+ return pid;
387
+ } catch {
388
+ unlinkSync(pidFile);
389
+ return null;
390
+ }
391
+ }
365
392
  function parseStreamJsonLine(line) {
366
393
  let obj;
367
394
  try {
@@ -424,116 +451,169 @@ function parseStreamJsonLine(line) {
424
451
  }
425
452
  return null;
426
453
  }
454
+ function runDaemon(agentName) {
455
+ const wsUrl = process.env._BX_WS_URL;
456
+ const claudePath = process.env._BX_CLAUDE_PATH || "claude";
457
+ const agentDisplayName = process.env._BX_AGENT_NAME || agentName;
458
+ if (!wsUrl) {
459
+ console.error("Missing _BX_WS_URL");
460
+ process.exit(1);
461
+ }
462
+ writeFileSync(getPidFile(agentName), String(process.pid));
463
+ let activeProcess = null;
464
+ let sendToServer = null;
465
+ let reconnectAttempt = 0;
466
+ let shouldReconnect = true;
467
+ const handleChat = (msg) => {
468
+ if (activeProcess) {
469
+ activeProcess.kill("SIGTERM");
470
+ activeProcess = null;
471
+ }
472
+ const { messageId, message, sessionId } = msg;
473
+ const args = ["-p", message, "--output-format", "stream-json", "--verbose"];
474
+ if (sessionId) args.push("--resume", sessionId);
475
+ const childEnv = { ...process.env };
476
+ delete childEnv.CLAUDECODE;
477
+ const child = spawn(claudePath, args, {
478
+ stdio: ["ignore", "pipe", "pipe"],
479
+ env: childEnv
480
+ });
481
+ activeProcess = child;
482
+ const rl = createInterface3({ input: child.stdout });
483
+ rl.on("line", (line) => {
484
+ const event = parseStreamJsonLine(line);
485
+ if (event && sendToServer) {
486
+ sendToServer({ type: "event", messageId, event });
487
+ }
488
+ });
489
+ child.on("exit", (code) => {
490
+ if (activeProcess === child) activeProcess = null;
491
+ if (sendToServer) {
492
+ sendToServer({ type: "event", messageId, event: { kind: "done" } });
493
+ }
494
+ if (code && code !== 0) {
495
+ console.error(`Claude Code exited with code ${code}`);
496
+ sendToServer?.({ type: "error", messageId, error: `Claude Code exited with code ${code}` });
497
+ }
498
+ });
499
+ child.on("error", (err) => {
500
+ console.error(`Failed to spawn Claude Code: ${err.message}`);
501
+ sendToServer?.({ type: "error", messageId, error: `Failed to spawn: ${err.message}` });
502
+ });
503
+ };
504
+ const cleanup = () => {
505
+ try {
506
+ unlinkSync(getPidFile(agentName));
507
+ } catch {
508
+ }
509
+ };
510
+ const connect = () => {
511
+ const ws = new WebSocket(wsUrl);
512
+ const send = (msg) => {
513
+ if (ws.readyState === WebSocket.OPEN) {
514
+ ws.send(JSON.stringify(msg));
515
+ }
516
+ };
517
+ ws.on("open", () => {
518
+ reconnectAttempt = 0;
519
+ sendToServer = send;
520
+ console.log(`[${(/* @__PURE__ */ new Date()).toISOString()}] Connected`);
521
+ });
522
+ ws.on("message", (data) => {
523
+ let msg;
524
+ try {
525
+ msg = JSON.parse(data.toString());
526
+ } catch {
527
+ return;
528
+ }
529
+ if (msg.type === "ping") {
530
+ send({ type: "pong" });
531
+ return;
532
+ }
533
+ if (msg.type === "stop" && activeProcess) {
534
+ activeProcess.kill("SIGINT");
535
+ return;
536
+ }
537
+ if (msg.type === "chat") {
538
+ handleChat(msg);
539
+ }
540
+ });
541
+ ws.on("close", () => {
542
+ sendToServer = null;
543
+ if (activeProcess) {
544
+ activeProcess.kill("SIGTERM");
545
+ activeProcess = null;
546
+ }
547
+ if (shouldReconnect) {
548
+ const delay = Math.min(
549
+ RECONNECT_BASE_MS * Math.pow(2, reconnectAttempt),
550
+ RECONNECT_MAX_MS
551
+ );
552
+ reconnectAttempt++;
553
+ console.log(`[${(/* @__PURE__ */ new Date()).toISOString()}] Disconnected. Reconnecting in ${delay / 1e3}s...`);
554
+ setTimeout(connect, delay);
555
+ }
556
+ });
557
+ ws.on("error", (err) => {
558
+ console.error(`[${(/* @__PURE__ */ new Date()).toISOString()}] Error: ${err.message}`);
559
+ });
560
+ const shutdown = () => {
561
+ shouldReconnect = false;
562
+ if (activeProcess) activeProcess.kill("SIGTERM");
563
+ ws.close();
564
+ cleanup();
565
+ process.exit(0);
566
+ };
567
+ process.on("SIGINT", shutdown);
568
+ process.on("SIGTERM", shutdown);
569
+ };
570
+ connect();
571
+ }
427
572
  function registerConnectCommand(program2) {
573
+ program2.command("_daemon <agent-name>", { hidden: true }).action((agentName) => {
574
+ runDaemon(agentName);
575
+ });
428
576
  program2.command("connect <agent-name>").description("Connect a local Claude Code instance to a BoxCrew agent.").option("--claude-path <path>", "Path to claude CLI binary", "claude").action(async (agentName, options) => {
577
+ const existingPid = readPid(agentName);
578
+ if (existingPid) {
579
+ console.log(`Agent "${agentName}" is already connected (PID ${existingPid}).`);
580
+ return;
581
+ }
429
582
  const config2 = await apiFetchJson(
430
583
  `/agents/${encodeURIComponent(agentName)}/connection-config`
431
584
  );
432
- const wsUrl = config2.websocket_url;
433
- const claudePath = options.claudePath;
434
- let activeProcess = null;
435
- let sendToServer = null;
436
- let reconnectAttempt = 0;
437
- let shouldReconnect = true;
438
- const handleChat = (msg) => {
439
- if (activeProcess) {
440
- activeProcess.kill("SIGTERM");
441
- activeProcess = null;
585
+ const logFile = getLogFile(agentName);
586
+ const logFd = openSync(logFile, "a");
587
+ const child = spawn(process.argv[0], [process.argv[1], "_daemon", agentName], {
588
+ detached: true,
589
+ stdio: ["ignore", logFd, logFd],
590
+ env: {
591
+ ...process.env,
592
+ _BX_WS_URL: config2.websocket_url,
593
+ _BX_CLAUDE_PATH: options.claudePath,
594
+ _BX_AGENT_NAME: config2.agent_name
442
595
  }
443
- const { messageId, message, sessionId } = msg;
444
- const args = ["-p", message, "--output-format", "stream-json", "--verbose"];
445
- if (sessionId) args.push("--resume", sessionId);
446
- const childEnv = { ...process.env };
447
- delete childEnv.CLAUDECODE;
448
- const child = spawn(claudePath, args, {
449
- stdio: ["ignore", "pipe", "pipe"],
450
- env: childEnv
451
- });
452
- activeProcess = child;
453
- const rl = createInterface3({ input: child.stdout });
454
- rl.on("line", (line) => {
455
- const event = parseStreamJsonLine(line);
456
- if (event && sendToServer) {
457
- sendToServer({ type: "event", messageId, event });
458
- }
459
- });
460
- child.on("exit", (code) => {
461
- if (activeProcess === child) activeProcess = null;
462
- if (sendToServer) {
463
- sendToServer({ type: "event", messageId, event: { kind: "done" } });
464
- }
465
- if (code && code !== 0) {
466
- console.error(`Claude Code exited with code ${code}`);
467
- sendToServer?.({ type: "error", messageId, error: `Claude Code exited with code ${code}` });
468
- }
469
- });
470
- child.on("error", (err) => {
471
- console.error(`Failed to spawn Claude Code: ${err.message}`);
472
- sendToServer?.({ type: "error", messageId, error: `Failed to spawn: ${err.message}` });
473
- });
474
- };
475
- const connect = () => {
476
- const ws = new WebSocket(wsUrl);
477
- const send = (msg) => {
478
- if (ws.readyState === WebSocket.OPEN) {
479
- ws.send(JSON.stringify(msg));
480
- }
481
- };
482
- ws.on("open", () => {
483
- reconnectAttempt = 0;
484
- sendToServer = send;
485
- console.log(`Agent "${config2.agent_name}" is online. Press Ctrl+C to disconnect.`);
486
- });
487
- ws.on("message", (data) => {
488
- let msg;
489
- try {
490
- msg = JSON.parse(data.toString());
491
- } catch {
492
- return;
493
- }
494
- if (msg.type === "ping") {
495
- send({ type: "pong" });
496
- return;
497
- }
498
- if (msg.type === "stop" && activeProcess) {
499
- activeProcess.kill("SIGINT");
500
- return;
501
- }
502
- if (msg.type === "chat") {
503
- handleChat(msg);
504
- }
505
- });
506
- ws.on("close", () => {
507
- sendToServer = null;
508
- if (activeProcess) {
509
- activeProcess.kill("SIGTERM");
510
- activeProcess = null;
511
- }
512
- if (shouldReconnect) {
513
- const delay = Math.min(
514
- RECONNECT_BASE_MS * Math.pow(2, reconnectAttempt),
515
- RECONNECT_MAX_MS
516
- );
517
- reconnectAttempt++;
518
- console.log(`Disconnected. Reconnecting in ${delay / 1e3}s...`);
519
- setTimeout(connect, delay);
520
- }
521
- });
522
- ws.on("error", (err) => {
523
- console.error("Connection error:", err.message);
524
- });
525
- const shutdown = () => {
526
- shouldReconnect = false;
527
- if (activeProcess) activeProcess.kill("SIGTERM");
528
- ws.close();
529
- process.exit(0);
530
- };
531
- process.removeAllListeners("SIGINT");
532
- process.removeAllListeners("SIGTERM");
533
- process.on("SIGINT", shutdown);
534
- process.on("SIGTERM", shutdown);
535
- };
536
- connect();
596
+ });
597
+ child.unref();
598
+ console.log(`Agent "${config2.agent_name}" is online.`);
599
+ console.log(`Logs: ${logFile}`);
600
+ });
601
+ program2.command("disconnect <agent-name>").description("Disconnect a local agent.").action((agentName) => {
602
+ const pid = readPid(agentName);
603
+ if (!pid) {
604
+ console.log(`Agent "${agentName}" is not connected.`);
605
+ return;
606
+ }
607
+ try {
608
+ process.kill(pid, "SIGTERM");
609
+ try {
610
+ unlinkSync(getPidFile(agentName));
611
+ } catch {
612
+ }
613
+ console.log(`Agent "${agentName}" disconnected.`);
614
+ } catch {
615
+ console.error(`Failed to stop process ${pid}.`);
616
+ }
537
617
  });
538
618
  }
539
619
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@boxcrew/cli",
3
- "version": "0.1.5",
3
+ "version": "0.1.6",
4
4
  "description": "BoxCrew CLI — manage your agents from the terminal",
5
5
  "type": "module",
6
6
  "bin": {