@boxcrew/cli 0.1.12 → 0.1.14

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 +200 -37
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -359,12 +359,17 @@ 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";
362
+ import { openSync, mkdirSync, existsSync, writeFileSync, readFileSync, unlinkSync, readdirSync } from "fs";
363
363
  import { join } from "path";
364
364
  import { homedir } from "os";
365
365
  import WebSocket from "ws";
366
366
  var RECONNECT_BASE_MS = 1e3;
367
367
  var RECONNECT_MAX_MS = 3e4;
368
+ var RUNTIME_NAMES = {
369
+ "claude-code": "Claude Code",
370
+ "opencode": "OpenCode",
371
+ "openclaw": "OpenClaw"
372
+ };
368
373
  function getStateDir() {
369
374
  const dir = join(homedir(), ".boxcrew");
370
375
  if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
@@ -373,6 +378,18 @@ function getStateDir() {
373
378
  function getPidFile(agentName) {
374
379
  return join(getStateDir(), `${agentName}.pid`);
375
380
  }
381
+ function getMetaFile(agentName) {
382
+ return join(getStateDir(), `${agentName}.meta.json`);
383
+ }
384
+ function readMeta(agentName) {
385
+ const metaFile = getMetaFile(agentName);
386
+ if (!existsSync(metaFile)) return null;
387
+ try {
388
+ return JSON.parse(readFileSync(metaFile, "utf-8"));
389
+ } catch {
390
+ return null;
391
+ }
392
+ }
376
393
  function getLogFile(agentName) {
377
394
  return join(getStateDir(), `${agentName}.log`);
378
395
  }
@@ -509,18 +526,25 @@ function runDaemon(agentName) {
509
526
  let sendToServer = null;
510
527
  let reconnectAttempt = 0;
511
528
  let shouldReconnect = true;
512
- const handleChat = (msg) => {
513
- if (activeProcess) {
514
- activeProcess.kill("SIGTERM");
515
- activeProcess = null;
529
+ const eventBuffer = [];
530
+ const enqueueMessage = (msg) => {
531
+ if (sendToServer) {
532
+ sendToServer(msg);
533
+ } else {
534
+ eventBuffer.push(msg);
516
535
  }
517
- const { messageId, message, sessionId } = msg;
536
+ };
537
+ const buildChildEnv = () => {
518
538
  const childEnv = { ...process.env };
519
539
  delete childEnv.CLAUDECODE;
520
540
  delete childEnv.CLAUDE_CODE_ENTRYPOINT;
521
541
  for (const key of Object.keys(childEnv)) {
522
542
  if (key.startsWith("_BX_")) delete childEnv[key];
523
543
  }
544
+ return childEnv;
545
+ };
546
+ const spawnRuntime = (messageId, message, sessionId, retriedWithoutSession) => {
547
+ const childEnv = buildChildEnv();
524
548
  let cmd;
525
549
  let args;
526
550
  if (runtime === "opencode") {
@@ -543,6 +567,7 @@ function runDaemon(agentName) {
543
567
  child.stdin.write(message);
544
568
  child.stdin.end();
545
569
  }
570
+ let producedOutput = false;
546
571
  if (runtime === "openclaw") {
547
572
  let stdout = "";
548
573
  let stderrChunks = "";
@@ -554,19 +579,17 @@ function runDaemon(agentName) {
554
579
  });
555
580
  child.on("exit", (code) => {
556
581
  if (activeProcess === child) activeProcess = null;
557
- if (sendToServer) {
558
- if (code && code !== 0) {
559
- const errorMsg = stderrChunks.trim() || `OpenClaw exited with code ${code}`;
560
- console.error(`OpenClaw exited with code ${code}: ${stderrChunks.trim()}`);
561
- sendToServer({ type: "event", messageId, event: { kind: "text", text: `Error: ${errorMsg}` } });
562
- } else {
563
- const events = parseOpenClawOutput(stdout);
564
- for (const event of events) {
565
- sendToServer({ type: "event", messageId, event });
566
- }
582
+ if (code && code !== 0) {
583
+ const errorMsg = stderrChunks.trim() || `OpenClaw exited with code ${code}`;
584
+ console.error(`OpenClaw exited with code ${code}: ${stderrChunks.trim()}`);
585
+ enqueueMessage({ type: "event", messageId, event: { kind: "text", text: `Error: ${errorMsg}` } });
586
+ } else {
587
+ const events = parseOpenClawOutput(stdout);
588
+ for (const event of events) {
589
+ enqueueMessage({ type: "event", messageId, event });
567
590
  }
568
- sendToServer({ type: "event", messageId, event: { kind: "done" } });
569
591
  }
592
+ enqueueMessage({ type: "event", messageId, event: { kind: "done" } });
570
593
  });
571
594
  } else {
572
595
  const parseLine = runtime === "opencode" ? parseOpenCodeLine : parseStreamJsonLine;
@@ -577,27 +600,38 @@ function runDaemon(agentName) {
577
600
  });
578
601
  rl.on("line", (line) => {
579
602
  const event = parseLine(line);
580
- if (event && sendToServer) {
581
- sendToServer({ type: "event", messageId, event });
603
+ if (event) {
604
+ producedOutput = true;
605
+ enqueueMessage({ type: "event", messageId, event });
582
606
  }
583
607
  });
584
608
  child.on("exit", (code) => {
585
609
  if (activeProcess === child) activeProcess = null;
586
- if (code && code !== 0 && sendToServer) {
610
+ if (code && code !== 0 && sessionId && !retriedWithoutSession && !producedOutput) {
611
+ console.log(`[${(/* @__PURE__ */ new Date()).toISOString()}] Session resume failed, retrying without --resume`);
612
+ spawnRuntime(messageId, message, void 0, true);
613
+ return;
614
+ }
615
+ if (code && code !== 0) {
587
616
  const errorMsg = stderrChunks.trim() || `${runtime} exited with code ${code}`;
588
617
  console.error(`${runtime} exited with code ${code}: ${stderrChunks.trim()}`);
589
- sendToServer({ type: "event", messageId, event: { kind: "text", text: `Error: ${errorMsg}` } });
590
- }
591
- if (sendToServer) {
592
- sendToServer({ type: "event", messageId, event: { kind: "done" } });
618
+ enqueueMessage({ type: "event", messageId, event: { kind: "text", text: `Error: ${errorMsg}` } });
593
619
  }
620
+ enqueueMessage({ type: "event", messageId, event: { kind: "done" } });
594
621
  });
595
622
  }
596
623
  child.on("error", (err) => {
597
624
  console.error(`Failed to spawn ${runtime}: ${err.message}`);
598
- sendToServer?.({ type: "error", messageId, error: `Failed to spawn: ${err.message}` });
625
+ enqueueMessage({ type: "error", messageId, error: `Failed to spawn: ${err.message}` });
599
626
  });
600
627
  };
628
+ const handleChat = (msg) => {
629
+ if (activeProcess) {
630
+ activeProcess.kill("SIGTERM");
631
+ activeProcess = null;
632
+ }
633
+ spawnRuntime(msg.messageId, msg.message, msg.sessionId, false);
634
+ };
601
635
  const cleanup = () => {
602
636
  try {
603
637
  unlinkSync(getPidFile(agentName));
@@ -615,6 +649,10 @@ function runDaemon(agentName) {
615
649
  reconnectAttempt = 0;
616
650
  sendToServer = send;
617
651
  console.log(`[${(/* @__PURE__ */ new Date()).toISOString()}] Connected`);
652
+ while (eventBuffer.length > 0) {
653
+ const msg = eventBuffer.shift();
654
+ send(msg);
655
+ }
618
656
  });
619
657
  ws.on("message", (data) => {
620
658
  let msg;
@@ -637,12 +675,13 @@ function runDaemon(agentName) {
637
675
  });
638
676
  ws.on("close", (code, reason) => {
639
677
  sendToServer = null;
640
- if (activeProcess) {
641
- activeProcess.kill("SIGTERM");
642
- activeProcess = null;
643
- }
644
- if (code === 4006) {
645
- console.log(`[${(/* @__PURE__ */ new Date()).toISOString()}] Agent was deleted. Shutting down.`);
678
+ if (code === 4004 || code === 4006) {
679
+ if (activeProcess) {
680
+ activeProcess.kill("SIGTERM");
681
+ activeProcess = null;
682
+ }
683
+ const msg = code === 4004 ? "Replaced by new connection. Shutting down." : "Agent was deleted. Shutting down.";
684
+ console.log(`[${(/* @__PURE__ */ new Date()).toISOString()}] ${msg}`);
646
685
  cleanup();
647
686
  process.exit(0);
648
687
  return;
@@ -653,7 +692,7 @@ function runDaemon(agentName) {
653
692
  RECONNECT_MAX_MS
654
693
  );
655
694
  reconnectAttempt++;
656
- console.log(`[${(/* @__PURE__ */ new Date()).toISOString()}] Disconnected. Reconnecting in ${delay / 1e3}s...`);
695
+ console.log(`[${(/* @__PURE__ */ new Date()).toISOString()}] Disconnected (code ${code}). Active process kept alive. Reconnecting in ${delay / 1e3}s...`);
657
696
  setTimeout(connect, delay);
658
697
  }
659
698
  });
@@ -702,12 +741,16 @@ function registerConnectCommand(program2) {
702
741
  env: daemonEnv
703
742
  });
704
743
  child.unref();
705
- const runtimeNames = {
706
- "claude-code": "Claude Code",
707
- "opencode": "OpenCode",
708
- "openclaw": "OpenClaw"
744
+ const meta = {
745
+ pid: child.pid,
746
+ cwd: process.cwd(),
747
+ runtime: config2.runtime,
748
+ agentName: config2.agent_name,
749
+ connectedAt: (/* @__PURE__ */ new Date()).toISOString(),
750
+ claudePath: options.claudePath
709
751
  };
710
- const runtimeDisplay = runtimeNames[config2.runtime] || config2.runtime;
752
+ writeFileSync(getMetaFile(agentName), JSON.stringify(meta, null, 2));
753
+ const runtimeDisplay = RUNTIME_NAMES[config2.runtime] || config2.runtime;
711
754
  const cwd = process.cwd();
712
755
  const home = homedir();
713
756
  const shortLog = logFile.startsWith(home) ? "~" + logFile.slice(home.length) : logFile;
@@ -738,6 +781,126 @@ function registerConnectCommand(program2) {
738
781
  console.error(`Failed to stop process ${pid}.`);
739
782
  }
740
783
  });
784
+ program2.command("status").description("Show status of all locally connected agents.").action(() => {
785
+ const stateDir = getStateDir();
786
+ const metaFiles = readdirSync(stateDir).filter((f) => f.endsWith(".meta.json"));
787
+ if (metaFiles.length === 0) {
788
+ console.log("No agents found. Use `bx connect <agent-name>` to connect one.");
789
+ return;
790
+ }
791
+ const home = homedir();
792
+ const shortenPath = (p) => p.startsWith(home) ? "~" + p.slice(home.length) : p;
793
+ const rows = [];
794
+ for (const file of metaFiles) {
795
+ const agentName = file.replace(".meta.json", "");
796
+ const meta = readMeta(agentName);
797
+ if (!meta) continue;
798
+ const pid = readPid(agentName);
799
+ const isOnline = pid !== null;
800
+ rows.push({
801
+ agent: meta.agentName || agentName,
802
+ status: isOnline ? "online" : "offline",
803
+ runtime: RUNTIME_NAMES[meta.runtime] || meta.runtime,
804
+ directory: shortenPath(meta.cwd),
805
+ pid: isOnline ? String(pid) : "-"
806
+ });
807
+ }
808
+ const headers = { agent: "Agent", status: "Status", runtime: "Runtime", directory: "Directory", pid: "PID" };
809
+ const cols = Object.keys(headers);
810
+ const widths = {};
811
+ for (const col of cols) {
812
+ widths[col] = Math.max(headers[col].length, ...rows.map((r) => r[col].length));
813
+ }
814
+ const pad = (s, w) => s + " ".repeat(w - s.length);
815
+ const headerLine = cols.map((c) => pad(headers[c], widths[c])).join(" ");
816
+ console.log("");
817
+ console.log(` ${headerLine}`);
818
+ for (const row of rows) {
819
+ const line = cols.map((c) => pad(row[c], widths[c])).join(" ");
820
+ console.log(` ${line}`);
821
+ }
822
+ console.log("");
823
+ });
824
+ program2.command("reconnect [agent-name]").description("Reconnect a previously connected agent.").option("--all", "Reconnect all agents").option("--claude-path <path>", "Path to claude CLI binary").action(async (agentName, options) => {
825
+ if (options.all) {
826
+ const stateDir = getStateDir();
827
+ const metaFiles = readdirSync(stateDir).filter((f) => f.endsWith(".meta.json"));
828
+ if (metaFiles.length === 0) {
829
+ console.log("No agents found. Use `bx connect <agent-name>` to connect one.");
830
+ return;
831
+ }
832
+ for (const file of metaFiles) {
833
+ const name = file.replace(".meta.json", "");
834
+ await reconnectAgent(name, options.claudePath);
835
+ }
836
+ return;
837
+ }
838
+ if (!agentName) {
839
+ console.error("Please specify an agent name or use --all.");
840
+ process.exit(1);
841
+ }
842
+ await reconnectAgent(agentName, options.claudePath);
843
+ });
844
+ async function reconnectAgent(agentName, claudePathOverride) {
845
+ const meta = readMeta(agentName);
846
+ if (!meta) {
847
+ console.error(`No previous connection found for "${agentName}". Use \`bx connect ${agentName}\` instead.`);
848
+ return;
849
+ }
850
+ const existingPid = readPid(agentName);
851
+ if (existingPid) {
852
+ try {
853
+ process.kill(existingPid, "SIGTERM");
854
+ } catch {
855
+ }
856
+ try {
857
+ unlinkSync(getPidFile(agentName));
858
+ } catch {
859
+ }
860
+ }
861
+ const config2 = await apiFetchJson(
862
+ `/agents/${encodeURIComponent(agentName)}/connection-config`
863
+ );
864
+ const claudePath = claudePathOverride || meta.claudePath || "claude";
865
+ const logFile = getLogFile(agentName);
866
+ const logFd = openSync(logFile, "a");
867
+ const daemonEnv = {
868
+ ...process.env,
869
+ _BX_WS_URL: config2.websocket_url,
870
+ _BX_CLAUDE_PATH: claudePath,
871
+ _BX_AGENT_NAME: config2.agent_name,
872
+ _BX_RUNTIME: config2.runtime
873
+ };
874
+ delete daemonEnv.CLAUDECODE;
875
+ delete daemonEnv.CLAUDE_CODE_ENTRYPOINT;
876
+ const child = spawn(process.argv[0], [process.argv[1], "_daemon", agentName], {
877
+ detached: true,
878
+ stdio: ["ignore", logFd, logFd],
879
+ cwd: meta.cwd,
880
+ env: daemonEnv
881
+ });
882
+ child.unref();
883
+ const newMeta = {
884
+ pid: child.pid,
885
+ cwd: meta.cwd,
886
+ runtime: config2.runtime,
887
+ agentName: config2.agent_name,
888
+ connectedAt: (/* @__PURE__ */ new Date()).toISOString(),
889
+ claudePath
890
+ };
891
+ writeFileSync(getMetaFile(agentName), JSON.stringify(newMeta, null, 2));
892
+ const runtimeDisplay = RUNTIME_NAMES[config2.runtime] || config2.runtime;
893
+ const home = homedir();
894
+ const shortCwd = meta.cwd.startsWith(home) ? "~" + meta.cwd.slice(home.length) : meta.cwd;
895
+ const shortLog = logFile.startsWith(home) ? "~" + logFile.slice(home.length) : logFile;
896
+ console.log("");
897
+ console.log(` Agent "${config2.agent_name}" reconnected.`);
898
+ console.log("");
899
+ console.log(` Runtime: ${runtimeDisplay}`);
900
+ console.log(` Directory: ${shortCwd}`);
901
+ console.log(` Logs: ${shortLog}`);
902
+ console.log("");
903
+ }
741
904
  }
742
905
 
743
906
  // src/index.ts
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@boxcrew/cli",
3
- "version": "0.1.12",
3
+ "version": "0.1.14",
4
4
  "description": "BoxCrew CLI — manage your agents from the terminal",
5
5
  "type": "module",
6
6
  "bin": {