@boxcrew/cli 0.1.5 → 0.1.7
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 +270 -91
- 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,35 +451,118 @@ function parseStreamJsonLine(line) {
|
|
|
424
451
|
}
|
|
425
452
|
return null;
|
|
426
453
|
}
|
|
427
|
-
function
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
454
|
+
function parseOpenCodeLine(line) {
|
|
455
|
+
let event;
|
|
456
|
+
try {
|
|
457
|
+
event = JSON.parse(line);
|
|
458
|
+
} catch {
|
|
459
|
+
return null;
|
|
460
|
+
}
|
|
461
|
+
if (event.type === "text" && event.part?.type === "text" && event.part.text) {
|
|
462
|
+
return { kind: "text", text: event.part.text };
|
|
463
|
+
}
|
|
464
|
+
if (event.type) {
|
|
465
|
+
return { kind: "raw", raw: event, rawType: event.type };
|
|
466
|
+
}
|
|
467
|
+
return null;
|
|
468
|
+
}
|
|
469
|
+
function parseOpenClawOutput(stdout) {
|
|
470
|
+
try {
|
|
471
|
+
const firstBrace = stdout.indexOf("{");
|
|
472
|
+
if (firstBrace === -1) {
|
|
473
|
+
return [{ kind: "text", text: stdout.trim() || "No response" }];
|
|
474
|
+
}
|
|
475
|
+
const response = JSON.parse(stdout.slice(firstBrace));
|
|
476
|
+
const events = [];
|
|
477
|
+
events.push({ kind: "raw", raw: response, rawType: "openclaw_response" });
|
|
478
|
+
const sessionId = response.meta?.agentMeta?.sessionId;
|
|
479
|
+
if (sessionId) {
|
|
480
|
+
events.push({ kind: "session_id", sessionId });
|
|
481
|
+
}
|
|
482
|
+
const payloads = response.payloads;
|
|
483
|
+
if (Array.isArray(payloads)) {
|
|
484
|
+
const texts = payloads.map((p) => p.text).filter((t) => typeof t === "string" && t.length > 0);
|
|
485
|
+
if (texts.length > 0) {
|
|
486
|
+
events.push({ kind: "text", text: texts.join("\n\n") });
|
|
487
|
+
} else {
|
|
488
|
+
events.push({ kind: "text", text: "No response" });
|
|
442
489
|
}
|
|
443
|
-
|
|
444
|
-
|
|
490
|
+
} else {
|
|
491
|
+
events.push({ kind: "text", text: "No response" });
|
|
492
|
+
}
|
|
493
|
+
return events;
|
|
494
|
+
} catch {
|
|
495
|
+
return [{ kind: "text", text: stdout.trim() || "No response" }];
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
function runDaemon(agentName) {
|
|
499
|
+
const wsUrl = process.env._BX_WS_URL;
|
|
500
|
+
const claudePath = process.env._BX_CLAUDE_PATH || "claude";
|
|
501
|
+
const agentDisplayName = process.env._BX_AGENT_NAME || agentName;
|
|
502
|
+
const runtime = process.env._BX_RUNTIME || "claude-code";
|
|
503
|
+
if (!wsUrl) {
|
|
504
|
+
console.error("Missing _BX_WS_URL");
|
|
505
|
+
process.exit(1);
|
|
506
|
+
}
|
|
507
|
+
writeFileSync(getPidFile(agentName), String(process.pid));
|
|
508
|
+
let activeProcess = null;
|
|
509
|
+
let sendToServer = null;
|
|
510
|
+
let reconnectAttempt = 0;
|
|
511
|
+
let shouldReconnect = true;
|
|
512
|
+
const handleChat = (msg) => {
|
|
513
|
+
if (activeProcess) {
|
|
514
|
+
activeProcess.kill("SIGTERM");
|
|
515
|
+
activeProcess = null;
|
|
516
|
+
}
|
|
517
|
+
const { messageId, message, sessionId } = msg;
|
|
518
|
+
const childEnv = { ...process.env };
|
|
519
|
+
delete childEnv.CLAUDECODE;
|
|
520
|
+
let cmd;
|
|
521
|
+
let args;
|
|
522
|
+
if (runtime === "opencode") {
|
|
523
|
+
cmd = "opencode";
|
|
524
|
+
args = ["run", "--format", "json"];
|
|
525
|
+
} else if (runtime === "openclaw") {
|
|
526
|
+
cmd = "openclaw";
|
|
527
|
+
args = ["agent", "--message", message, "--json", "--agent", "main", "--timeout", "300"];
|
|
528
|
+
} else {
|
|
529
|
+
cmd = claudePath;
|
|
530
|
+
args = ["-p", message, "--output-format", "stream-json", "--verbose"];
|
|
445
531
|
if (sessionId) args.push("--resume", sessionId);
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
532
|
+
}
|
|
533
|
+
const child = spawn(cmd, args, {
|
|
534
|
+
stdio: [runtime === "opencode" ? "pipe" : "ignore", "pipe", "pipe"],
|
|
535
|
+
env: childEnv
|
|
536
|
+
});
|
|
537
|
+
activeProcess = child;
|
|
538
|
+
if (runtime === "opencode" && child.stdin) {
|
|
539
|
+
child.stdin.write(message);
|
|
540
|
+
child.stdin.end();
|
|
541
|
+
}
|
|
542
|
+
if (runtime === "openclaw") {
|
|
543
|
+
let stdout = "";
|
|
544
|
+
child.stdout.on("data", (chunk) => {
|
|
545
|
+
stdout += chunk.toString();
|
|
451
546
|
});
|
|
452
|
-
|
|
547
|
+
child.on("exit", (code) => {
|
|
548
|
+
if (activeProcess === child) activeProcess = null;
|
|
549
|
+
if (sendToServer) {
|
|
550
|
+
const events = parseOpenClawOutput(stdout);
|
|
551
|
+
for (const event of events) {
|
|
552
|
+
sendToServer({ type: "event", messageId, event });
|
|
553
|
+
}
|
|
554
|
+
sendToServer({ type: "event", messageId, event: { kind: "done" } });
|
|
555
|
+
}
|
|
556
|
+
if (code && code !== 0) {
|
|
557
|
+
console.error(`OpenClaw exited with code ${code}`);
|
|
558
|
+
sendToServer?.({ type: "error", messageId, error: `OpenClaw exited with code ${code}` });
|
|
559
|
+
}
|
|
560
|
+
});
|
|
561
|
+
} else {
|
|
562
|
+
const parseLine = runtime === "opencode" ? parseOpenCodeLine : parseStreamJsonLine;
|
|
453
563
|
const rl = createInterface3({ input: child.stdout });
|
|
454
564
|
rl.on("line", (line) => {
|
|
455
|
-
const event =
|
|
565
|
+
const event = parseLine(line);
|
|
456
566
|
if (event && sendToServer) {
|
|
457
567
|
sendToServer({ type: "event", messageId, event });
|
|
458
568
|
}
|
|
@@ -463,77 +573,146 @@ function registerConnectCommand(program2) {
|
|
|
463
573
|
sendToServer({ type: "event", messageId, event: { kind: "done" } });
|
|
464
574
|
}
|
|
465
575
|
if (code && code !== 0) {
|
|
466
|
-
console.error(
|
|
467
|
-
sendToServer?.({ type: "error", messageId, error:
|
|
576
|
+
console.error(`${runtime} exited with code ${code}`);
|
|
577
|
+
sendToServer?.({ type: "error", messageId, error: `${runtime} exited with code ${code}` });
|
|
468
578
|
}
|
|
469
579
|
});
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
});
|
|
580
|
+
}
|
|
581
|
+
child.on("error", (err) => {
|
|
582
|
+
console.error(`Failed to spawn ${runtime}: ${err.message}`);
|
|
583
|
+
sendToServer?.({ type: "error", messageId, error: `Failed to spawn: ${err.message}` });
|
|
584
|
+
});
|
|
585
|
+
};
|
|
586
|
+
const cleanup = () => {
|
|
587
|
+
try {
|
|
588
|
+
unlinkSync(getPidFile(agentName));
|
|
589
|
+
} catch {
|
|
590
|
+
}
|
|
591
|
+
};
|
|
592
|
+
const connect = () => {
|
|
593
|
+
const ws = new WebSocket(wsUrl);
|
|
594
|
+
const send = (msg) => {
|
|
595
|
+
if (ws.readyState === WebSocket.OPEN) {
|
|
596
|
+
ws.send(JSON.stringify(msg));
|
|
597
|
+
}
|
|
474
598
|
};
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
}
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
}
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
599
|
+
ws.on("open", () => {
|
|
600
|
+
reconnectAttempt = 0;
|
|
601
|
+
sendToServer = send;
|
|
602
|
+
console.log(`[${(/* @__PURE__ */ new Date()).toISOString()}] Connected`);
|
|
603
|
+
});
|
|
604
|
+
ws.on("message", (data) => {
|
|
605
|
+
let msg;
|
|
606
|
+
try {
|
|
607
|
+
msg = JSON.parse(data.toString());
|
|
608
|
+
} catch {
|
|
609
|
+
return;
|
|
610
|
+
}
|
|
611
|
+
if (msg.type === "ping") {
|
|
612
|
+
send({ type: "pong" });
|
|
613
|
+
return;
|
|
614
|
+
}
|
|
615
|
+
if (msg.type === "stop" && activeProcess) {
|
|
616
|
+
activeProcess.kill("SIGINT");
|
|
617
|
+
return;
|
|
618
|
+
}
|
|
619
|
+
if (msg.type === "chat") {
|
|
620
|
+
handleChat(msg);
|
|
621
|
+
}
|
|
622
|
+
});
|
|
623
|
+
ws.on("close", () => {
|
|
624
|
+
sendToServer = null;
|
|
625
|
+
if (activeProcess) {
|
|
626
|
+
activeProcess.kill("SIGTERM");
|
|
627
|
+
activeProcess = null;
|
|
628
|
+
}
|
|
629
|
+
if (shouldReconnect) {
|
|
630
|
+
const delay = Math.min(
|
|
631
|
+
RECONNECT_BASE_MS * Math.pow(2, reconnectAttempt),
|
|
632
|
+
RECONNECT_MAX_MS
|
|
633
|
+
);
|
|
634
|
+
reconnectAttempt++;
|
|
635
|
+
console.log(`[${(/* @__PURE__ */ new Date()).toISOString()}] Disconnected. Reconnecting in ${delay / 1e3}s...`);
|
|
636
|
+
setTimeout(connect, delay);
|
|
637
|
+
}
|
|
638
|
+
});
|
|
639
|
+
ws.on("error", (err) => {
|
|
640
|
+
console.error(`[${(/* @__PURE__ */ new Date()).toISOString()}] Error: ${err.message}`);
|
|
641
|
+
});
|
|
642
|
+
const shutdown = () => {
|
|
643
|
+
shouldReconnect = false;
|
|
644
|
+
if (activeProcess) activeProcess.kill("SIGTERM");
|
|
645
|
+
ws.close();
|
|
646
|
+
cleanup();
|
|
647
|
+
process.exit(0);
|
|
648
|
+
};
|
|
649
|
+
process.on("SIGINT", shutdown);
|
|
650
|
+
process.on("SIGTERM", shutdown);
|
|
651
|
+
};
|
|
652
|
+
connect();
|
|
653
|
+
}
|
|
654
|
+
function registerConnectCommand(program2) {
|
|
655
|
+
program2.command("_daemon <agent-name>", { hidden: true }).action((agentName) => {
|
|
656
|
+
runDaemon(agentName);
|
|
657
|
+
});
|
|
658
|
+
program2.command("connect <agent-name>").description("Connect a local agent to BoxCrew.").option("--claude-path <path>", "Path to claude CLI binary", "claude").action(async (agentName, options) => {
|
|
659
|
+
const existingPid = readPid(agentName);
|
|
660
|
+
if (existingPid) {
|
|
661
|
+
console.log(`Agent "${agentName}" is already connected (PID ${existingPid}).`);
|
|
662
|
+
return;
|
|
663
|
+
}
|
|
664
|
+
const config2 = await apiFetchJson(
|
|
665
|
+
`/agents/${encodeURIComponent(agentName)}/connection-config`
|
|
666
|
+
);
|
|
667
|
+
const logFile = getLogFile(agentName);
|
|
668
|
+
const logFd = openSync(logFile, "a");
|
|
669
|
+
const child = spawn(process.argv[0], [process.argv[1], "_daemon", agentName], {
|
|
670
|
+
detached: true,
|
|
671
|
+
stdio: ["ignore", logFd, logFd],
|
|
672
|
+
env: {
|
|
673
|
+
...process.env,
|
|
674
|
+
_BX_WS_URL: config2.websocket_url,
|
|
675
|
+
_BX_CLAUDE_PATH: options.claudePath,
|
|
676
|
+
_BX_AGENT_NAME: config2.agent_name,
|
|
677
|
+
_BX_RUNTIME: config2.runtime
|
|
678
|
+
}
|
|
679
|
+
});
|
|
680
|
+
child.unref();
|
|
681
|
+
const runtimeNames = {
|
|
682
|
+
"claude-code": "Claude Code",
|
|
683
|
+
"opencode": "OpenCode",
|
|
684
|
+
"openclaw": "OpenClaw"
|
|
535
685
|
};
|
|
536
|
-
|
|
686
|
+
const runtimeDisplay = runtimeNames[config2.runtime] || config2.runtime;
|
|
687
|
+
const cwd = process.cwd();
|
|
688
|
+
const home = homedir();
|
|
689
|
+
const shortLog = logFile.startsWith(home) ? "~" + logFile.slice(home.length) : logFile;
|
|
690
|
+
console.log("");
|
|
691
|
+
console.log(` Agent "${config2.agent_name}" is online.`);
|
|
692
|
+
console.log("");
|
|
693
|
+
console.log(` Runtime: ${runtimeDisplay}`);
|
|
694
|
+
console.log(` Directory: ${cwd}`);
|
|
695
|
+
console.log(` Logs: ${shortLog}`);
|
|
696
|
+
console.log("");
|
|
697
|
+
console.log(` To disconnect: npx @boxcrew/cli disconnect ${agentName}`);
|
|
698
|
+
console.log("");
|
|
699
|
+
});
|
|
700
|
+
program2.command("disconnect <agent-name>").description("Disconnect a local agent.").action((agentName) => {
|
|
701
|
+
const pid = readPid(agentName);
|
|
702
|
+
if (!pid) {
|
|
703
|
+
console.log(`Agent "${agentName}" is not connected.`);
|
|
704
|
+
return;
|
|
705
|
+
}
|
|
706
|
+
try {
|
|
707
|
+
process.kill(pid, "SIGTERM");
|
|
708
|
+
try {
|
|
709
|
+
unlinkSync(getPidFile(agentName));
|
|
710
|
+
} catch {
|
|
711
|
+
}
|
|
712
|
+
console.log(`Agent "${agentName}" disconnected.`);
|
|
713
|
+
} catch {
|
|
714
|
+
console.error(`Failed to stop process ${pid}.`);
|
|
715
|
+
}
|
|
537
716
|
});
|
|
538
717
|
}
|
|
539
718
|
|