@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.
Files changed (2) hide show
  1. package/dist/index.js +238 -147
  2. 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 open from "open";
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 (!token) {
27
- console.error(
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 authenticate...");
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 createInterface2 } from "readline";
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 registerConnectCommand(program2) {
410
- 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").option(
411
- "--frontend-url <url>",
412
- "BoxCrew frontend URL",
413
- process.env.BOXCREW_FRONTEND_URL || "https://boxcrew.ai"
414
- ).action(async (agentName, options) => {
415
- if (!getAuthToken()) {
416
- await ensureLoggedIn(options.frontendUrl);
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 config2 = await apiFetchJson(
419
- `/agents/${encodeURIComponent(agentName)}/connection-config`
420
- );
421
- const wsUrl = config2.websocket_url;
422
- const claudePath = options.claudePath;
423
- let activeProcess = null;
424
- let sendToServer = null;
425
- let reconnectAttempt = 0;
426
- let shouldReconnect = true;
427
- const handleChat = (msg) => {
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
- const { messageId, message, sessionId } = msg;
433
- const args = ["-p", message, "--output-format", "stream-json", "--verbose"];
434
- if (sessionId) args.push("--resume", sessionId);
435
- const childEnv = { ...process.env };
436
- delete childEnv.CLAUDECODE;
437
- const child = spawn(claudePath, args, {
438
- stdio: ["ignore", "pipe", "pipe"],
439
- env: childEnv
440
- });
441
- activeProcess = child;
442
- const rl = createInterface2({ input: child.stdout });
443
- rl.on("line", (line) => {
444
- const event = parseStreamJsonLine(line);
445
- if (event && sendToServer) {
446
- sendToServer({ type: "event", messageId, event });
447
- }
448
- });
449
- child.on("exit", (code) => {
450
- if (activeProcess === child) activeProcess = null;
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
- connect();
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
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@boxcrew/cli",
3
- "version": "0.1.4",
3
+ "version": "0.1.6",
4
4
  "description": "BoxCrew CLI — manage your agents from the terminal",
5
5
  "type": "module",
6
6
  "bin": {