@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.
- package/dist/index.js +189 -39
- 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 (
|
|
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,9 +600,9 @@ function runDaemon(agentName) {
|
|
|
577
600
|
});
|
|
578
601
|
rl.on("line", (line) => {
|
|
579
602
|
const event = parseLine(line);
|
|
580
|
-
if (event
|
|
603
|
+
if (event) {
|
|
581
604
|
producedOutput = true;
|
|
582
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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 (
|
|
654
|
-
activeProcess
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
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
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
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
|
-
|
|
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
|