@boxcrew/cli 0.1.4 → 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 +238 -147
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -4,8 +4,8 @@
|
|
|
4
4
|
import { Command } from "commander";
|
|
5
5
|
|
|
6
6
|
// src/commands/login.ts
|
|
7
|
-
import { createInterface } from "readline";
|
|
8
|
-
import
|
|
7
|
+
import { createInterface as createInterface2 } from "readline";
|
|
8
|
+
import open2 from "open";
|
|
9
9
|
|
|
10
10
|
// src/config.ts
|
|
11
11
|
import Conf from "conf";
|
|
@@ -17,44 +17,19 @@ var config = new Conf({
|
|
|
17
17
|
});
|
|
18
18
|
|
|
19
19
|
// src/auth.ts
|
|
20
|
+
import { createInterface } from "readline";
|
|
21
|
+
import open from "open";
|
|
22
|
+
var DEFAULT_FRONTEND_URL = "https://boxcrew.ai";
|
|
20
23
|
function getAuthToken() {
|
|
21
24
|
if (process.env.BOXCREW_API_KEY) return process.env.BOXCREW_API_KEY;
|
|
22
25
|
return config.get("apiKey") ?? null;
|
|
23
26
|
}
|
|
24
|
-
function requireAuth() {
|
|
27
|
+
async function requireAuth() {
|
|
25
28
|
const token = getAuthToken();
|
|
26
|
-
if (
|
|
27
|
-
|
|
28
|
-
"Not authenticated. Run `bx login` or set the BOXCREW_API_KEY environment variable."
|
|
29
|
-
);
|
|
30
|
-
process.exit(1);
|
|
31
|
-
}
|
|
32
|
-
return token;
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
// src/commands/login.ts
|
|
36
|
-
var DEFAULT_FRONTEND_URL = "https://boxcrew.ai";
|
|
37
|
-
function registerLoginCommand(program2) {
|
|
38
|
-
program2.command("login").description("Authenticate with BoxCrew").option("--api-url <url>", "BoxCrew API URL").option(
|
|
39
|
-
"--frontend-url <url>",
|
|
40
|
-
"BoxCrew frontend URL",
|
|
41
|
-
process.env.BOXCREW_FRONTEND_URL || DEFAULT_FRONTEND_URL
|
|
42
|
-
).action(
|
|
43
|
-
async (options) => {
|
|
44
|
-
if (options.apiUrl) {
|
|
45
|
-
config.set("apiUrl", options.apiUrl);
|
|
46
|
-
}
|
|
47
|
-
await ensureLoggedIn(options.frontendUrl);
|
|
48
|
-
}
|
|
49
|
-
);
|
|
50
|
-
}
|
|
51
|
-
async function ensureLoggedIn(frontendUrl = DEFAULT_FRONTEND_URL) {
|
|
52
|
-
if (getAuthToken()) {
|
|
53
|
-
console.log("Already authenticated.");
|
|
54
|
-
return;
|
|
55
|
-
}
|
|
29
|
+
if (token) return token;
|
|
30
|
+
const frontendUrl = process.env.BOXCREW_FRONTEND_URL || DEFAULT_FRONTEND_URL;
|
|
56
31
|
const authUrl = `${frontendUrl}/cli-auth`;
|
|
57
|
-
console.log("Opening browser to
|
|
32
|
+
console.log("Not authenticated. Opening browser to log in...");
|
|
58
33
|
console.log(`If the browser doesn't open, visit: ${authUrl}
|
|
59
34
|
`);
|
|
60
35
|
open(authUrl).catch(() => {
|
|
@@ -75,6 +50,49 @@ async function ensureLoggedIn(frontendUrl = DEFAULT_FRONTEND_URL) {
|
|
|
75
50
|
}
|
|
76
51
|
config.set("apiKey", apiKey);
|
|
77
52
|
console.log("Authenticated successfully!\n");
|
|
53
|
+
return apiKey;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// src/commands/login.ts
|
|
57
|
+
var DEFAULT_FRONTEND_URL2 = "https://boxcrew.ai";
|
|
58
|
+
function registerLoginCommand(program2) {
|
|
59
|
+
program2.command("login").description("Authenticate with BoxCrew (or re-authenticate)").option("--api-url <url>", "BoxCrew API URL").option(
|
|
60
|
+
"--frontend-url <url>",
|
|
61
|
+
"BoxCrew frontend URL",
|
|
62
|
+
process.env.BOXCREW_FRONTEND_URL || DEFAULT_FRONTEND_URL2
|
|
63
|
+
).action(
|
|
64
|
+
async (options) => {
|
|
65
|
+
if (options.apiUrl) {
|
|
66
|
+
config.set("apiUrl", options.apiUrl);
|
|
67
|
+
}
|
|
68
|
+
if (getAuthToken()) {
|
|
69
|
+
console.log("Already authenticated. To re-authenticate, continue below.\n");
|
|
70
|
+
}
|
|
71
|
+
const authUrl = `${options.frontendUrl}/cli-auth`;
|
|
72
|
+
console.log("Opening browser to authenticate...");
|
|
73
|
+
console.log(`If the browser doesn't open, visit: ${authUrl}
|
|
74
|
+
`);
|
|
75
|
+
open2(authUrl).catch(() => {
|
|
76
|
+
});
|
|
77
|
+
const rl = createInterface2({
|
|
78
|
+
input: process.stdin,
|
|
79
|
+
output: process.stdout
|
|
80
|
+
});
|
|
81
|
+
const apiKey = await new Promise((resolve) => {
|
|
82
|
+
rl.question("Paste your API key: ", (answer) => {
|
|
83
|
+
rl.close();
|
|
84
|
+
resolve(answer.trim());
|
|
85
|
+
});
|
|
86
|
+
});
|
|
87
|
+
if (!apiKey.startsWith("bxk_")) {
|
|
88
|
+
console.error('Invalid API key. Keys should start with "bxk_".');
|
|
89
|
+
process.exit(1);
|
|
90
|
+
}
|
|
91
|
+
config.set("apiKey", apiKey);
|
|
92
|
+
console.log("\nAuthenticated successfully!");
|
|
93
|
+
console.log(`API URL: ${config.get("apiUrl")}`);
|
|
94
|
+
}
|
|
95
|
+
);
|
|
78
96
|
}
|
|
79
97
|
|
|
80
98
|
// src/commands/logout.ts
|
|
@@ -90,7 +108,7 @@ function getBaseUrl() {
|
|
|
90
108
|
return process.env.BOXCREW_API_URL || config.get("apiUrl");
|
|
91
109
|
}
|
|
92
110
|
async function apiFetch(path, options = {}) {
|
|
93
|
-
const token = requireAuth();
|
|
111
|
+
const token = await requireAuth();
|
|
94
112
|
const baseUrl = getBaseUrl();
|
|
95
113
|
const url = `${baseUrl}${path}`;
|
|
96
114
|
const headers = {
|
|
@@ -340,10 +358,37 @@ function registerApiCommand(program2) {
|
|
|
340
358
|
|
|
341
359
|
// src/commands/connect.ts
|
|
342
360
|
import { spawn } from "child_process";
|
|
343
|
-
import { createInterface as
|
|
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";
|
|
344
365
|
import WebSocket from "ws";
|
|
345
366
|
var RECONNECT_BASE_MS = 1e3;
|
|
346
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
|
+
}
|
|
347
392
|
function parseStreamJsonLine(line) {
|
|
348
393
|
let obj;
|
|
349
394
|
try {
|
|
@@ -406,123 +451,169 @@ function parseStreamJsonLine(line) {
|
|
|
406
451
|
}
|
|
407
452
|
return null;
|
|
408
453
|
}
|
|
409
|
-
function
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
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;
|
|
417
471
|
}
|
|
418
|
-
const
|
|
419
|
-
|
|
420
|
-
);
|
|
421
|
-
const
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
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;
|
|
428
543
|
if (activeProcess) {
|
|
429
544
|
activeProcess.kill("SIGTERM");
|
|
430
545
|
activeProcess = null;
|
|
431
546
|
}
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
}
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
if (sendToServer) {
|
|
452
|
-
sendToServer({ type: "event", messageId, event: { kind: "done" } });
|
|
453
|
-
}
|
|
454
|
-
if (code && code !== 0) {
|
|
455
|
-
console.error(`Claude Code exited with code ${code}`);
|
|
456
|
-
sendToServer?.({ type: "error", messageId, error: `Claude Code exited with code ${code}` });
|
|
457
|
-
}
|
|
458
|
-
});
|
|
459
|
-
child.on("error", (err) => {
|
|
460
|
-
console.error(`Failed to spawn Claude Code: ${err.message}`);
|
|
461
|
-
sendToServer?.({ type: "error", messageId, error: `Failed to spawn: ${err.message}` });
|
|
462
|
-
});
|
|
463
|
-
};
|
|
464
|
-
const connect = () => {
|
|
465
|
-
const ws = new WebSocket(wsUrl);
|
|
466
|
-
const send = (msg) => {
|
|
467
|
-
if (ws.readyState === WebSocket.OPEN) {
|
|
468
|
-
ws.send(JSON.stringify(msg));
|
|
469
|
-
}
|
|
470
|
-
};
|
|
471
|
-
ws.on("open", () => {
|
|
472
|
-
reconnectAttempt = 0;
|
|
473
|
-
sendToServer = send;
|
|
474
|
-
console.log(`Agent "${config2.agent_name}" is online. Press Ctrl+C to disconnect.`);
|
|
475
|
-
});
|
|
476
|
-
ws.on("message", (data) => {
|
|
477
|
-
let msg;
|
|
478
|
-
try {
|
|
479
|
-
msg = JSON.parse(data.toString());
|
|
480
|
-
} catch {
|
|
481
|
-
return;
|
|
482
|
-
}
|
|
483
|
-
if (msg.type === "ping") {
|
|
484
|
-
send({ type: "pong" });
|
|
485
|
-
return;
|
|
486
|
-
}
|
|
487
|
-
if (msg.type === "stop" && activeProcess) {
|
|
488
|
-
activeProcess.kill("SIGINT");
|
|
489
|
-
return;
|
|
490
|
-
}
|
|
491
|
-
if (msg.type === "chat") {
|
|
492
|
-
handleChat(msg);
|
|
493
|
-
}
|
|
494
|
-
});
|
|
495
|
-
ws.on("close", () => {
|
|
496
|
-
sendToServer = null;
|
|
497
|
-
if (activeProcess) {
|
|
498
|
-
activeProcess.kill("SIGTERM");
|
|
499
|
-
activeProcess = null;
|
|
500
|
-
}
|
|
501
|
-
if (shouldReconnect) {
|
|
502
|
-
const delay = Math.min(
|
|
503
|
-
RECONNECT_BASE_MS * Math.pow(2, reconnectAttempt),
|
|
504
|
-
RECONNECT_MAX_MS
|
|
505
|
-
);
|
|
506
|
-
reconnectAttempt++;
|
|
507
|
-
console.log(`Disconnected. Reconnecting in ${delay / 1e3}s...`);
|
|
508
|
-
setTimeout(connect, delay);
|
|
509
|
-
}
|
|
510
|
-
});
|
|
511
|
-
ws.on("error", (err) => {
|
|
512
|
-
console.error("Connection error:", err.message);
|
|
513
|
-
});
|
|
514
|
-
const shutdown = () => {
|
|
515
|
-
shouldReconnect = false;
|
|
516
|
-
if (activeProcess) activeProcess.kill("SIGTERM");
|
|
517
|
-
ws.close();
|
|
518
|
-
process.exit(0);
|
|
519
|
-
};
|
|
520
|
-
process.removeAllListeners("SIGINT");
|
|
521
|
-
process.removeAllListeners("SIGTERM");
|
|
522
|
-
process.on("SIGINT", shutdown);
|
|
523
|
-
process.on("SIGTERM", shutdown);
|
|
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);
|
|
524
566
|
};
|
|
525
|
-
|
|
567
|
+
process.on("SIGINT", shutdown);
|
|
568
|
+
process.on("SIGTERM", shutdown);
|
|
569
|
+
};
|
|
570
|
+
connect();
|
|
571
|
+
}
|
|
572
|
+
function registerConnectCommand(program2) {
|
|
573
|
+
program2.command("_daemon <agent-name>", { hidden: true }).action((agentName) => {
|
|
574
|
+
runDaemon(agentName);
|
|
575
|
+
});
|
|
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
|
+
}
|
|
582
|
+
const config2 = await apiFetchJson(
|
|
583
|
+
`/agents/${encodeURIComponent(agentName)}/connection-config`
|
|
584
|
+
);
|
|
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
|
|
595
|
+
}
|
|
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
|
+
}
|
|
526
617
|
});
|
|
527
618
|
}
|
|
528
619
|
|