@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.
- package/dist/index.js +200 -37
- 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
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
529
|
+
const eventBuffer = [];
|
|
530
|
+
const enqueueMessage = (msg) => {
|
|
531
|
+
if (sendToServer) {
|
|
532
|
+
sendToServer(msg);
|
|
533
|
+
} else {
|
|
534
|
+
eventBuffer.push(msg);
|
|
516
535
|
}
|
|
517
|
-
|
|
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 (
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
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
|
|
581
|
-
|
|
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 &&
|
|
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
|
-
|
|
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
|
-
|
|
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 (
|
|
641
|
-
activeProcess
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
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
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
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
|
-
|
|
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
|