@boxcrew/cli 0.1.5 → 0.1.6
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 +184 -104
- 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,116 +451,169 @@ function parseStreamJsonLine(line) {
|
|
|
424
451
|
}
|
|
425
452
|
return null;
|
|
426
453
|
}
|
|
454
|
+
function runDaemon(agentName) {
|
|
455
|
+
const wsUrl = process.env._BX_WS_URL;
|
|
456
|
+
const claudePath = process.env._BX_CLAUDE_PATH || "claude";
|
|
457
|
+
const agentDisplayName = process.env._BX_AGENT_NAME || agentName;
|
|
458
|
+
if (!wsUrl) {
|
|
459
|
+
console.error("Missing _BX_WS_URL");
|
|
460
|
+
process.exit(1);
|
|
461
|
+
}
|
|
462
|
+
writeFileSync(getPidFile(agentName), String(process.pid));
|
|
463
|
+
let activeProcess = null;
|
|
464
|
+
let sendToServer = null;
|
|
465
|
+
let reconnectAttempt = 0;
|
|
466
|
+
let shouldReconnect = true;
|
|
467
|
+
const handleChat = (msg) => {
|
|
468
|
+
if (activeProcess) {
|
|
469
|
+
activeProcess.kill("SIGTERM");
|
|
470
|
+
activeProcess = null;
|
|
471
|
+
}
|
|
472
|
+
const { messageId, message, sessionId } = msg;
|
|
473
|
+
const args = ["-p", message, "--output-format", "stream-json", "--verbose"];
|
|
474
|
+
if (sessionId) args.push("--resume", sessionId);
|
|
475
|
+
const childEnv = { ...process.env };
|
|
476
|
+
delete childEnv.CLAUDECODE;
|
|
477
|
+
const child = spawn(claudePath, args, {
|
|
478
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
479
|
+
env: childEnv
|
|
480
|
+
});
|
|
481
|
+
activeProcess = child;
|
|
482
|
+
const rl = createInterface3({ input: child.stdout });
|
|
483
|
+
rl.on("line", (line) => {
|
|
484
|
+
const event = parseStreamJsonLine(line);
|
|
485
|
+
if (event && sendToServer) {
|
|
486
|
+
sendToServer({ type: "event", messageId, event });
|
|
487
|
+
}
|
|
488
|
+
});
|
|
489
|
+
child.on("exit", (code) => {
|
|
490
|
+
if (activeProcess === child) activeProcess = null;
|
|
491
|
+
if (sendToServer) {
|
|
492
|
+
sendToServer({ type: "event", messageId, event: { kind: "done" } });
|
|
493
|
+
}
|
|
494
|
+
if (code && code !== 0) {
|
|
495
|
+
console.error(`Claude Code exited with code ${code}`);
|
|
496
|
+
sendToServer?.({ type: "error", messageId, error: `Claude Code exited with code ${code}` });
|
|
497
|
+
}
|
|
498
|
+
});
|
|
499
|
+
child.on("error", (err) => {
|
|
500
|
+
console.error(`Failed to spawn Claude Code: ${err.message}`);
|
|
501
|
+
sendToServer?.({ type: "error", messageId, error: `Failed to spawn: ${err.message}` });
|
|
502
|
+
});
|
|
503
|
+
};
|
|
504
|
+
const cleanup = () => {
|
|
505
|
+
try {
|
|
506
|
+
unlinkSync(getPidFile(agentName));
|
|
507
|
+
} catch {
|
|
508
|
+
}
|
|
509
|
+
};
|
|
510
|
+
const connect = () => {
|
|
511
|
+
const ws = new WebSocket(wsUrl);
|
|
512
|
+
const send = (msg) => {
|
|
513
|
+
if (ws.readyState === WebSocket.OPEN) {
|
|
514
|
+
ws.send(JSON.stringify(msg));
|
|
515
|
+
}
|
|
516
|
+
};
|
|
517
|
+
ws.on("open", () => {
|
|
518
|
+
reconnectAttempt = 0;
|
|
519
|
+
sendToServer = send;
|
|
520
|
+
console.log(`[${(/* @__PURE__ */ new Date()).toISOString()}] Connected`);
|
|
521
|
+
});
|
|
522
|
+
ws.on("message", (data) => {
|
|
523
|
+
let msg;
|
|
524
|
+
try {
|
|
525
|
+
msg = JSON.parse(data.toString());
|
|
526
|
+
} catch {
|
|
527
|
+
return;
|
|
528
|
+
}
|
|
529
|
+
if (msg.type === "ping") {
|
|
530
|
+
send({ type: "pong" });
|
|
531
|
+
return;
|
|
532
|
+
}
|
|
533
|
+
if (msg.type === "stop" && activeProcess) {
|
|
534
|
+
activeProcess.kill("SIGINT");
|
|
535
|
+
return;
|
|
536
|
+
}
|
|
537
|
+
if (msg.type === "chat") {
|
|
538
|
+
handleChat(msg);
|
|
539
|
+
}
|
|
540
|
+
});
|
|
541
|
+
ws.on("close", () => {
|
|
542
|
+
sendToServer = null;
|
|
543
|
+
if (activeProcess) {
|
|
544
|
+
activeProcess.kill("SIGTERM");
|
|
545
|
+
activeProcess = null;
|
|
546
|
+
}
|
|
547
|
+
if (shouldReconnect) {
|
|
548
|
+
const delay = Math.min(
|
|
549
|
+
RECONNECT_BASE_MS * Math.pow(2, reconnectAttempt),
|
|
550
|
+
RECONNECT_MAX_MS
|
|
551
|
+
);
|
|
552
|
+
reconnectAttempt++;
|
|
553
|
+
console.log(`[${(/* @__PURE__ */ new Date()).toISOString()}] Disconnected. Reconnecting in ${delay / 1e3}s...`);
|
|
554
|
+
setTimeout(connect, delay);
|
|
555
|
+
}
|
|
556
|
+
});
|
|
557
|
+
ws.on("error", (err) => {
|
|
558
|
+
console.error(`[${(/* @__PURE__ */ new Date()).toISOString()}] Error: ${err.message}`);
|
|
559
|
+
});
|
|
560
|
+
const shutdown = () => {
|
|
561
|
+
shouldReconnect = false;
|
|
562
|
+
if (activeProcess) activeProcess.kill("SIGTERM");
|
|
563
|
+
ws.close();
|
|
564
|
+
cleanup();
|
|
565
|
+
process.exit(0);
|
|
566
|
+
};
|
|
567
|
+
process.on("SIGINT", shutdown);
|
|
568
|
+
process.on("SIGTERM", shutdown);
|
|
569
|
+
};
|
|
570
|
+
connect();
|
|
571
|
+
}
|
|
427
572
|
function registerConnectCommand(program2) {
|
|
573
|
+
program2.command("_daemon <agent-name>", { hidden: true }).action((agentName) => {
|
|
574
|
+
runDaemon(agentName);
|
|
575
|
+
});
|
|
428
576
|
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) => {
|
|
577
|
+
const existingPid = readPid(agentName);
|
|
578
|
+
if (existingPid) {
|
|
579
|
+
console.log(`Agent "${agentName}" is already connected (PID ${existingPid}).`);
|
|
580
|
+
return;
|
|
581
|
+
}
|
|
429
582
|
const config2 = await apiFetchJson(
|
|
430
583
|
`/agents/${encodeURIComponent(agentName)}/connection-config`
|
|
431
584
|
);
|
|
432
|
-
const
|
|
433
|
-
const
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
585
|
+
const logFile = getLogFile(agentName);
|
|
586
|
+
const logFd = openSync(logFile, "a");
|
|
587
|
+
const child = spawn(process.argv[0], [process.argv[1], "_daemon", agentName], {
|
|
588
|
+
detached: true,
|
|
589
|
+
stdio: ["ignore", logFd, logFd],
|
|
590
|
+
env: {
|
|
591
|
+
...process.env,
|
|
592
|
+
_BX_WS_URL: config2.websocket_url,
|
|
593
|
+
_BX_CLAUDE_PATH: options.claudePath,
|
|
594
|
+
_BX_AGENT_NAME: config2.agent_name
|
|
442
595
|
}
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
});
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
}
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
}
|
|
465
|
-
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}` });
|
|
468
|
-
}
|
|
469
|
-
});
|
|
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
|
-
});
|
|
474
|
-
};
|
|
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);
|
|
535
|
-
};
|
|
536
|
-
connect();
|
|
596
|
+
});
|
|
597
|
+
child.unref();
|
|
598
|
+
console.log(`Agent "${config2.agent_name}" is online.`);
|
|
599
|
+
console.log(`Logs: ${logFile}`);
|
|
600
|
+
});
|
|
601
|
+
program2.command("disconnect <agent-name>").description("Disconnect a local agent.").action((agentName) => {
|
|
602
|
+
const pid = readPid(agentName);
|
|
603
|
+
if (!pid) {
|
|
604
|
+
console.log(`Agent "${agentName}" is not connected.`);
|
|
605
|
+
return;
|
|
606
|
+
}
|
|
607
|
+
try {
|
|
608
|
+
process.kill(pid, "SIGTERM");
|
|
609
|
+
try {
|
|
610
|
+
unlinkSync(getPidFile(agentName));
|
|
611
|
+
} catch {
|
|
612
|
+
}
|
|
613
|
+
console.log(`Agent "${agentName}" disconnected.`);
|
|
614
|
+
} catch {
|
|
615
|
+
console.error(`Failed to stop process ${pid}.`);
|
|
616
|
+
}
|
|
537
617
|
});
|
|
538
618
|
}
|
|
539
619
|
|