@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.
Files changed (2) hide show
  1. package/dist/index.js +270 -91
  2. 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 registerConnectCommand(program2) {
428
- program2.command("connect <agent-name>").description("Connect a local Claude Code instance to a BoxCrew agent.").option("--claude-path <path>", "Path to claude CLI binary", "claude").action(async (agentName, options) => {
429
- const config2 = await apiFetchJson(
430
- `/agents/${encodeURIComponent(agentName)}/connection-config`
431
- );
432
- const wsUrl = config2.websocket_url;
433
- const claudePath = options.claudePath;
434
- let activeProcess = null;
435
- let sendToServer = null;
436
- let reconnectAttempt = 0;
437
- let shouldReconnect = true;
438
- const handleChat = (msg) => {
439
- if (activeProcess) {
440
- activeProcess.kill("SIGTERM");
441
- activeProcess = null;
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
- const { messageId, message, sessionId } = msg;
444
- const args = ["-p", message, "--output-format", "stream-json", "--verbose"];
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
- const childEnv = { ...process.env };
447
- delete childEnv.CLAUDECODE;
448
- const child = spawn(claudePath, args, {
449
- stdio: ["ignore", "pipe", "pipe"],
450
- env: childEnv
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
- activeProcess = child;
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 = parseStreamJsonLine(line);
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(`Claude Code exited with code ${code}`);
467
- sendToServer?.({ type: "error", messageId, error: `Claude Code exited with code ${code}` });
576
+ console.error(`${runtime} exited with code ${code}`);
577
+ sendToServer?.({ type: "error", messageId, error: `${runtime} exited with code ${code}` });
468
578
  }
469
579
  });
470
- child.on("error", (err) => {
471
- console.error(`Failed to spawn Claude Code: ${err.message}`);
472
- sendToServer?.({ type: "error", messageId, error: `Failed to spawn: ${err.message}` });
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
- const connect = () => {
476
- const ws = new WebSocket(wsUrl);
477
- const send = (msg) => {
478
- if (ws.readyState === WebSocket.OPEN) {
479
- ws.send(JSON.stringify(msg));
480
- }
481
- };
482
- ws.on("open", () => {
483
- reconnectAttempt = 0;
484
- sendToServer = send;
485
- console.log(`Agent "${config2.agent_name}" is online. Press Ctrl+C to disconnect.`);
486
- });
487
- ws.on("message", (data) => {
488
- let msg;
489
- try {
490
- msg = JSON.parse(data.toString());
491
- } catch {
492
- return;
493
- }
494
- if (msg.type === "ping") {
495
- send({ type: "pong" });
496
- return;
497
- }
498
- if (msg.type === "stop" && activeProcess) {
499
- activeProcess.kill("SIGINT");
500
- return;
501
- }
502
- if (msg.type === "chat") {
503
- handleChat(msg);
504
- }
505
- });
506
- ws.on("close", () => {
507
- sendToServer = null;
508
- if (activeProcess) {
509
- activeProcess.kill("SIGTERM");
510
- activeProcess = null;
511
- }
512
- if (shouldReconnect) {
513
- const delay = Math.min(
514
- RECONNECT_BASE_MS * Math.pow(2, reconnectAttempt),
515
- RECONNECT_MAX_MS
516
- );
517
- reconnectAttempt++;
518
- console.log(`Disconnected. Reconnecting in ${delay / 1e3}s...`);
519
- setTimeout(connect, delay);
520
- }
521
- });
522
- ws.on("error", (err) => {
523
- console.error("Connection error:", err.message);
524
- });
525
- const shutdown = () => {
526
- shouldReconnect = false;
527
- if (activeProcess) activeProcess.kill("SIGTERM");
528
- ws.close();
529
- process.exit(0);
530
- };
531
- process.removeAllListeners("SIGINT");
532
- process.removeAllListeners("SIGTERM");
533
- process.on("SIGINT", shutdown);
534
- process.on("SIGTERM", shutdown);
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
- connect();
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
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@boxcrew/cli",
3
- "version": "0.1.5",
3
+ "version": "0.1.7",
4
4
  "description": "BoxCrew CLI — manage your agents from the terminal",
5
5
  "type": "module",
6
6
  "bin": {