@boxcrew/cli 0.1.13 → 0.1.15

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 +189 -39
  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
- import { homedir } from "os";
364
+ import { homedir, platform, release, hostname } 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,6 +526,14 @@ function runDaemon(agentName) {
509
526
  let sendToServer = null;
510
527
  let reconnectAttempt = 0;
511
528
  let shouldReconnect = true;
529
+ const eventBuffer = [];
530
+ const enqueueMessage = (msg) => {
531
+ if (sendToServer) {
532
+ sendToServer(msg);
533
+ } else {
534
+ eventBuffer.push(msg);
535
+ }
536
+ };
512
537
  const buildChildEnv = () => {
513
538
  const childEnv = { ...process.env };
514
539
  delete childEnv.CLAUDECODE;
@@ -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,9 +600,9 @@ function runDaemon(agentName) {
577
600
  });
578
601
  rl.on("line", (line) => {
579
602
  const event = parseLine(line);
580
- if (event && sendToServer) {
603
+ if (event) {
581
604
  producedOutput = true;
582
- sendToServer({ type: "event", messageId, event });
605
+ enqueueMessage({ type: "event", messageId, event });
583
606
  }
584
607
  });
585
608
  child.on("exit", (code) => {
@@ -589,19 +612,17 @@ function runDaemon(agentName) {
589
612
  spawnRuntime(messageId, message, void 0, true);
590
613
  return;
591
614
  }
592
- if (code && code !== 0 && sendToServer) {
615
+ if (code && code !== 0) {
593
616
  const errorMsg = stderrChunks.trim() || `${runtime} exited with code ${code}`;
594
617
  console.error(`${runtime} exited with code ${code}: ${stderrChunks.trim()}`);
595
- sendToServer({ type: "event", messageId, event: { kind: "text", text: `Error: ${errorMsg}` } });
596
- }
597
- if (sendToServer) {
598
- sendToServer({ type: "event", messageId, event: { kind: "done" } });
618
+ enqueueMessage({ type: "event", messageId, event: { kind: "text", text: `Error: ${errorMsg}` } });
599
619
  }
620
+ enqueueMessage({ type: "event", messageId, event: { kind: "done" } });
600
621
  });
601
622
  }
602
623
  child.on("error", (err) => {
603
624
  console.error(`Failed to spawn ${runtime}: ${err.message}`);
604
- sendToServer?.({ type: "error", messageId, error: `Failed to spawn: ${err.message}` });
625
+ enqueueMessage({ type: "error", messageId, error: `Failed to spawn: ${err.message}` });
605
626
  });
606
627
  };
607
628
  const handleChat = (msg) => {
@@ -628,6 +649,16 @@ function runDaemon(agentName) {
628
649
  reconnectAttempt = 0;
629
650
  sendToServer = send;
630
651
  console.log(`[${(/* @__PURE__ */ new Date()).toISOString()}] Connected`);
652
+ send({
653
+ type: "hello",
654
+ os: `${platform()} ${release()}`,
655
+ cwd: process.cwd(),
656
+ hostname: hostname()
657
+ });
658
+ while (eventBuffer.length > 0) {
659
+ const msg = eventBuffer.shift();
660
+ send(msg);
661
+ }
631
662
  });
632
663
  ws.on("message", (data) => {
633
664
  let msg;
@@ -650,18 +681,13 @@ function runDaemon(agentName) {
650
681
  });
651
682
  ws.on("close", (code, reason) => {
652
683
  sendToServer = null;
653
- if (activeProcess) {
654
- activeProcess.kill("SIGTERM");
655
- activeProcess = null;
656
- }
657
- if (code === 4004) {
658
- console.log(`[${(/* @__PURE__ */ new Date()).toISOString()}] Replaced by new connection. Shutting down.`);
659
- cleanup();
660
- process.exit(0);
661
- return;
662
- }
663
- if (code === 4006) {
664
- console.log(`[${(/* @__PURE__ */ new Date()).toISOString()}] Agent was deleted. Shutting down.`);
684
+ if (code === 4004 || code === 4006) {
685
+ if (activeProcess) {
686
+ activeProcess.kill("SIGTERM");
687
+ activeProcess = null;
688
+ }
689
+ const msg = code === 4004 ? "Replaced by new connection. Shutting down." : "Agent was deleted. Shutting down.";
690
+ console.log(`[${(/* @__PURE__ */ new Date()).toISOString()}] ${msg}`);
665
691
  cleanup();
666
692
  process.exit(0);
667
693
  return;
@@ -672,7 +698,7 @@ function runDaemon(agentName) {
672
698
  RECONNECT_MAX_MS
673
699
  );
674
700
  reconnectAttempt++;
675
- console.log(`[${(/* @__PURE__ */ new Date()).toISOString()}] Disconnected. Reconnecting in ${delay / 1e3}s...`);
701
+ console.log(`[${(/* @__PURE__ */ new Date()).toISOString()}] Disconnected (code ${code}). Active process kept alive. Reconnecting in ${delay / 1e3}s...`);
676
702
  setTimeout(connect, delay);
677
703
  }
678
704
  });
@@ -721,12 +747,16 @@ function registerConnectCommand(program2) {
721
747
  env: daemonEnv
722
748
  });
723
749
  child.unref();
724
- const runtimeNames = {
725
- "claude-code": "Claude Code",
726
- "opencode": "OpenCode",
727
- "openclaw": "OpenClaw"
750
+ const meta = {
751
+ pid: child.pid,
752
+ cwd: process.cwd(),
753
+ runtime: config2.runtime,
754
+ agentName: config2.agent_name,
755
+ connectedAt: (/* @__PURE__ */ new Date()).toISOString(),
756
+ claudePath: options.claudePath
728
757
  };
729
- const runtimeDisplay = runtimeNames[config2.runtime] || config2.runtime;
758
+ writeFileSync(getMetaFile(agentName), JSON.stringify(meta, null, 2));
759
+ const runtimeDisplay = RUNTIME_NAMES[config2.runtime] || config2.runtime;
730
760
  const cwd = process.cwd();
731
761
  const home = homedir();
732
762
  const shortLog = logFile.startsWith(home) ? "~" + logFile.slice(home.length) : logFile;
@@ -757,6 +787,126 @@ function registerConnectCommand(program2) {
757
787
  console.error(`Failed to stop process ${pid}.`);
758
788
  }
759
789
  });
790
+ program2.command("status").description("Show status of all locally connected agents.").action(() => {
791
+ const stateDir = getStateDir();
792
+ const metaFiles = readdirSync(stateDir).filter((f) => f.endsWith(".meta.json"));
793
+ if (metaFiles.length === 0) {
794
+ console.log("No agents found. Use `bx connect <agent-name>` to connect one.");
795
+ return;
796
+ }
797
+ const home = homedir();
798
+ const shortenPath = (p) => p.startsWith(home) ? "~" + p.slice(home.length) : p;
799
+ const rows = [];
800
+ for (const file of metaFiles) {
801
+ const agentName = file.replace(".meta.json", "");
802
+ const meta = readMeta(agentName);
803
+ if (!meta) continue;
804
+ const pid = readPid(agentName);
805
+ const isOnline = pid !== null;
806
+ rows.push({
807
+ agent: meta.agentName || agentName,
808
+ status: isOnline ? "online" : "offline",
809
+ runtime: RUNTIME_NAMES[meta.runtime] || meta.runtime,
810
+ directory: shortenPath(meta.cwd),
811
+ pid: isOnline ? String(pid) : "-"
812
+ });
813
+ }
814
+ const headers = { agent: "Agent", status: "Status", runtime: "Runtime", directory: "Directory", pid: "PID" };
815
+ const cols = Object.keys(headers);
816
+ const widths = {};
817
+ for (const col of cols) {
818
+ widths[col] = Math.max(headers[col].length, ...rows.map((r) => r[col].length));
819
+ }
820
+ const pad = (s, w) => s + " ".repeat(w - s.length);
821
+ const headerLine = cols.map((c) => pad(headers[c], widths[c])).join(" ");
822
+ console.log("");
823
+ console.log(` ${headerLine}`);
824
+ for (const row of rows) {
825
+ const line = cols.map((c) => pad(row[c], widths[c])).join(" ");
826
+ console.log(` ${line}`);
827
+ }
828
+ console.log("");
829
+ });
830
+ 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) => {
831
+ if (options.all) {
832
+ const stateDir = getStateDir();
833
+ const metaFiles = readdirSync(stateDir).filter((f) => f.endsWith(".meta.json"));
834
+ if (metaFiles.length === 0) {
835
+ console.log("No agents found. Use `bx connect <agent-name>` to connect one.");
836
+ return;
837
+ }
838
+ for (const file of metaFiles) {
839
+ const name = file.replace(".meta.json", "");
840
+ await reconnectAgent(name, options.claudePath);
841
+ }
842
+ return;
843
+ }
844
+ if (!agentName) {
845
+ console.error("Please specify an agent name or use --all.");
846
+ process.exit(1);
847
+ }
848
+ await reconnectAgent(agentName, options.claudePath);
849
+ });
850
+ async function reconnectAgent(agentName, claudePathOverride) {
851
+ const meta = readMeta(agentName);
852
+ if (!meta) {
853
+ console.error(`No previous connection found for "${agentName}". Use \`bx connect ${agentName}\` instead.`);
854
+ return;
855
+ }
856
+ const existingPid = readPid(agentName);
857
+ if (existingPid) {
858
+ try {
859
+ process.kill(existingPid, "SIGTERM");
860
+ } catch {
861
+ }
862
+ try {
863
+ unlinkSync(getPidFile(agentName));
864
+ } catch {
865
+ }
866
+ }
867
+ const config2 = await apiFetchJson(
868
+ `/agents/${encodeURIComponent(agentName)}/connection-config`
869
+ );
870
+ const claudePath = claudePathOverride || meta.claudePath || "claude";
871
+ const logFile = getLogFile(agentName);
872
+ const logFd = openSync(logFile, "a");
873
+ const daemonEnv = {
874
+ ...process.env,
875
+ _BX_WS_URL: config2.websocket_url,
876
+ _BX_CLAUDE_PATH: claudePath,
877
+ _BX_AGENT_NAME: config2.agent_name,
878
+ _BX_RUNTIME: config2.runtime
879
+ };
880
+ delete daemonEnv.CLAUDECODE;
881
+ delete daemonEnv.CLAUDE_CODE_ENTRYPOINT;
882
+ const child = spawn(process.argv[0], [process.argv[1], "_daemon", agentName], {
883
+ detached: true,
884
+ stdio: ["ignore", logFd, logFd],
885
+ cwd: meta.cwd,
886
+ env: daemonEnv
887
+ });
888
+ child.unref();
889
+ const newMeta = {
890
+ pid: child.pid,
891
+ cwd: meta.cwd,
892
+ runtime: config2.runtime,
893
+ agentName: config2.agent_name,
894
+ connectedAt: (/* @__PURE__ */ new Date()).toISOString(),
895
+ claudePath
896
+ };
897
+ writeFileSync(getMetaFile(agentName), JSON.stringify(newMeta, null, 2));
898
+ const runtimeDisplay = RUNTIME_NAMES[config2.runtime] || config2.runtime;
899
+ const home = homedir();
900
+ const shortCwd = meta.cwd.startsWith(home) ? "~" + meta.cwd.slice(home.length) : meta.cwd;
901
+ const shortLog = logFile.startsWith(home) ? "~" + logFile.slice(home.length) : logFile;
902
+ console.log("");
903
+ console.log(` Agent "${config2.agent_name}" reconnected.`);
904
+ console.log("");
905
+ console.log(` Runtime: ${runtimeDisplay}`);
906
+ console.log(` Directory: ${shortCwd}`);
907
+ console.log(` Logs: ${shortLog}`);
908
+ console.log("");
909
+ }
760
910
  }
761
911
 
762
912
  // src/index.ts
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@boxcrew/cli",
3
- "version": "0.1.13",
3
+ "version": "0.1.15",
4
4
  "description": "BoxCrew CLI — manage your agents from the terminal",
5
5
  "type": "module",
6
6
  "bin": {