@boxcrew/cli 0.1.15 → 0.1.17

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 +176 -118
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -1,6 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  // src/index.ts
4
+ import { createRequire } from "module";
4
5
  import { Command } from "commander";
5
6
 
6
7
  // src/commands/login.ts
@@ -103,6 +104,10 @@ function registerLogoutCommand(program2) {
103
104
  });
104
105
  }
105
106
 
107
+ // src/commands/agents.ts
108
+ import { readdirSync as readdirSync2 } from "fs";
109
+ import { homedir as homedir2 } from "os";
110
+
106
111
  // src/client.ts
107
112
  function getBaseUrl() {
108
113
  return process.env.BOXCREW_API_URL || config.get("apiUrl");
@@ -141,6 +146,64 @@ async function apiFetchJson(path, options = {}) {
141
146
  return response.json();
142
147
  }
143
148
 
149
+ // src/lib/state.ts
150
+ import { existsSync, mkdirSync, readFileSync, readdirSync, unlinkSync } from "fs";
151
+ import { join } from "path";
152
+ import { homedir } from "os";
153
+ var UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
154
+ var RUNTIME_NAMES = {
155
+ "claude-code": "Claude Code",
156
+ "opencode": "OpenCode",
157
+ "openclaw": "OpenClaw"
158
+ };
159
+ function getStateDir() {
160
+ const dir = join(homedir(), ".boxcrew");
161
+ if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
162
+ return dir;
163
+ }
164
+ function getPidFile(key) {
165
+ return join(getStateDir(), `${key}.pid`);
166
+ }
167
+ function getMetaFile(key) {
168
+ return join(getStateDir(), `${key}.meta.json`);
169
+ }
170
+ function getLogFile(key) {
171
+ return join(getStateDir(), `${key}.log`);
172
+ }
173
+ function readMeta(key) {
174
+ const metaFile = getMetaFile(key);
175
+ if (!existsSync(metaFile)) return null;
176
+ try {
177
+ return JSON.parse(readFileSync(metaFile, "utf-8"));
178
+ } catch {
179
+ return null;
180
+ }
181
+ }
182
+ function readPid(key) {
183
+ const pidFile = getPidFile(key);
184
+ if (!existsSync(pidFile)) return null;
185
+ const pid = parseInt(readFileSync(pidFile, "utf-8").trim(), 10);
186
+ if (isNaN(pid)) return null;
187
+ try {
188
+ process.kill(pid, 0);
189
+ return pid;
190
+ } catch {
191
+ unlinkSync(pidFile);
192
+ return null;
193
+ }
194
+ }
195
+ function resolveAgentKey(nameOrId) {
196
+ if (UUID_RE.test(nameOrId)) return nameOrId;
197
+ const stateDir = getStateDir();
198
+ const metaFiles = readdirSync(stateDir).filter((f) => f.endsWith(".meta.json"));
199
+ for (const file of metaFiles) {
200
+ const key = file.replace(".meta.json", "");
201
+ const meta = readMeta(key);
202
+ if (meta && meta.agentName === nameOrId) return key;
203
+ }
204
+ return null;
205
+ }
206
+
144
207
  // src/commands/agents.ts
145
208
  var TRANSIENT_CODES = [
146
209
  "ETIMEDOUT",
@@ -164,10 +227,55 @@ function registerAgentsCommands(program2) {
164
227
  console.log("No agents found.");
165
228
  return;
166
229
  }
167
- console.log("");
230
+ const home = homedir2();
231
+ const shortenPath = (p) => p.startsWith(home) ? "~" + p.slice(home.length) : p;
232
+ const localMeta = /* @__PURE__ */ new Map();
233
+ try {
234
+ const stateDir = getStateDir();
235
+ const metaFiles = readdirSync2(stateDir).filter((f) => f.endsWith(".meta.json"));
236
+ for (const file of metaFiles) {
237
+ const key = file.replace(".meta.json", "");
238
+ const meta = readMeta(key);
239
+ if (meta) {
240
+ localMeta.set(meta.agentId, { pid: readPid(key), cwd: meta.cwd });
241
+ }
242
+ }
243
+ } catch {
244
+ }
245
+ const rows = [];
168
246
  for (const agent of data) {
169
- const model = agent.model ? ` (${agent.model})` : "";
170
- console.log(` ${agent.name} ${agent.runtime}${model}`);
247
+ const shortId = agent.id.slice(0, 8);
248
+ const typeDisplay = agent.agent_type === "remote" ? "BYOA" : "hosted";
249
+ const runtimeDisplay = RUNTIME_NAMES[agent.runtime] || agent.runtime;
250
+ let status;
251
+ let directory = "-";
252
+ if (agent.agent_type !== "remote") {
253
+ status = "-";
254
+ } else {
255
+ const local = localMeta.get(agent.id);
256
+ if (local?.pid) {
257
+ status = "online (here)";
258
+ directory = shortenPath(local.cwd);
259
+ } else if (agent.is_online) {
260
+ status = "online";
261
+ } else {
262
+ status = "offline";
263
+ if (local?.cwd) directory = shortenPath(local.cwd);
264
+ }
265
+ }
266
+ rows.push({ name: agent.name, id: shortId, type: typeDisplay, runtime: runtimeDisplay, status, directory });
267
+ }
268
+ const headers = { name: "Name", id: "ID", type: "Type", runtime: "Runtime", status: "Status", directory: "Directory" };
269
+ const cols = Object.keys(headers);
270
+ const widths = {};
271
+ for (const col of cols) {
272
+ widths[col] = Math.max(headers[col].length, ...rows.map((r) => r[col].length));
273
+ }
274
+ const pad = (s, w) => s + " ".repeat(w - s.length);
275
+ console.log("");
276
+ console.log(` ${cols.map((c) => pad(headers[c], widths[c])).join(" ")}`);
277
+ for (const row of rows) {
278
+ console.log(` ${cols.map((c) => pad(row[c], widths[c])).join(" ")}`);
171
279
  }
172
280
  console.log("");
173
281
  });
@@ -359,53 +467,12 @@ function registerApiCommand(program2) {
359
467
  // src/commands/connect.ts
360
468
  import { spawn } from "child_process";
361
469
  import { createInterface as createInterface3 } from "readline";
362
- import { openSync, mkdirSync, existsSync, writeFileSync, readFileSync, unlinkSync, readdirSync } from "fs";
363
- import { join } from "path";
364
- import { homedir, platform, release, hostname } from "os";
470
+ import { openSync, existsSync as existsSync2, writeFileSync, unlinkSync as unlinkSync2, readdirSync as readdirSync3, renameSync } from "fs";
471
+ import { join as join2 } from "path";
472
+ import { homedir as homedir3, platform, release, hostname } from "os";
365
473
  import WebSocket from "ws";
366
474
  var RECONNECT_BASE_MS = 1e3;
367
475
  var RECONNECT_MAX_MS = 3e4;
368
- var RUNTIME_NAMES = {
369
- "claude-code": "Claude Code",
370
- "opencode": "OpenCode",
371
- "openclaw": "OpenClaw"
372
- };
373
- function getStateDir() {
374
- const dir = join(homedir(), ".boxcrew");
375
- if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
376
- return dir;
377
- }
378
- function getPidFile(agentName) {
379
- return join(getStateDir(), `${agentName}.pid`);
380
- }
381
- function getMetaFile(agentName) {
382
- return join(getStateDir(), `${agentName}.meta.json`);
383
- }
384
- function readMeta(agentName) {
385
- const metaFile = getMetaFile(agentName);
386
- if (!existsSync(metaFile)) return null;
387
- try {
388
- return JSON.parse(readFileSync(metaFile, "utf-8"));
389
- } catch {
390
- return null;
391
- }
392
- }
393
- function getLogFile(agentName) {
394
- return join(getStateDir(), `${agentName}.log`);
395
- }
396
- function readPid(agentName) {
397
- const pidFile = getPidFile(agentName);
398
- if (!existsSync(pidFile)) return null;
399
- const pid = parseInt(readFileSync(pidFile, "utf-8").trim(), 10);
400
- if (isNaN(pid)) return null;
401
- try {
402
- process.kill(pid, 0);
403
- return pid;
404
- } catch {
405
- unlinkSync(pidFile);
406
- return null;
407
- }
408
- }
409
476
  function parseStreamJsonLine(line) {
410
477
  let obj;
411
478
  try {
@@ -512,16 +579,16 @@ function parseOpenClawOutput(stdout) {
512
579
  return [{ kind: "text", text: stdout.trim() || "No response" }];
513
580
  }
514
581
  }
515
- function runDaemon(agentName) {
582
+ function runDaemon(agentId) {
516
583
  const wsUrl = process.env._BX_WS_URL;
517
584
  const claudePath = process.env._BX_CLAUDE_PATH || "claude";
518
- const agentDisplayName = process.env._BX_AGENT_NAME || agentName;
585
+ const agentDisplayName = process.env._BX_AGENT_NAME || agentId;
519
586
  const runtime = process.env._BX_RUNTIME || "claude-code";
520
587
  if (!wsUrl) {
521
588
  console.error("Missing _BX_WS_URL");
522
589
  process.exit(1);
523
590
  }
524
- writeFileSync(getPidFile(agentName), String(process.pid));
591
+ writeFileSync(getPidFile(agentId), String(process.pid));
525
592
  let activeProcess = null;
526
593
  let sendToServer = null;
527
594
  let reconnectAttempt = 0;
@@ -634,7 +701,7 @@ function runDaemon(agentName) {
634
701
  };
635
702
  const cleanup = () => {
636
703
  try {
637
- unlinkSync(getPidFile(agentName));
704
+ unlinkSync2(getPidFile(agentId));
638
705
  } catch {
639
706
  }
640
707
  };
@@ -678,6 +745,14 @@ function runDaemon(agentName) {
678
745
  if (msg.type === "chat") {
679
746
  handleChat(msg);
680
747
  }
748
+ if (msg.type === "rename") {
749
+ const meta = readMeta(agentId);
750
+ if (meta) {
751
+ meta.agentName = msg.newName;
752
+ writeFileSync(getMetaFile(agentId), JSON.stringify(meta, null, 2));
753
+ }
754
+ console.log(`[${(/* @__PURE__ */ new Date()).toISOString()}] Agent renamed to "${msg.newName}"`);
755
+ }
681
756
  });
682
757
  ws.on("close", (code, reason) => {
683
758
  sendToServer = null;
@@ -718,19 +793,27 @@ function runDaemon(agentName) {
718
793
  connect();
719
794
  }
720
795
  function registerConnectCommand(program2) {
721
- program2.command("_daemon <agent-name>", { hidden: true }).action((agentName) => {
722
- runDaemon(agentName);
796
+ program2.command("_daemon <agent-id>", { hidden: true }).action((agentId) => {
797
+ runDaemon(agentId);
723
798
  });
724
799
  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) => {
725
- const existingPid = readPid(agentName);
800
+ const config2 = await apiFetchJson(
801
+ `/agents/${encodeURIComponent(agentName)}/connection-config`
802
+ );
803
+ const agentId = config2.agent_id;
804
+ if (!existsSync2(getMetaFile(agentId)) && existsSync2(getMetaFile(agentName))) {
805
+ for (const ext of [".meta.json", ".pid", ".log"]) {
806
+ const oldPath = join2(getStateDir(), `${agentName}${ext}`);
807
+ const newPath = join2(getStateDir(), `${agentId}${ext}`);
808
+ if (existsSync2(oldPath)) renameSync(oldPath, newPath);
809
+ }
810
+ }
811
+ const existingPid = readPid(agentId);
726
812
  if (existingPid) {
727
813
  console.log(`Agent "${agentName}" is already connected (PID ${existingPid}).`);
728
814
  return;
729
815
  }
730
- const config2 = await apiFetchJson(
731
- `/agents/${encodeURIComponent(agentName)}/connection-config`
732
- );
733
- const logFile = getLogFile(agentName);
816
+ const logFile = getLogFile(agentId);
734
817
  const logFd = openSync(logFile, "a");
735
818
  const daemonEnv = {
736
819
  ...process.env,
@@ -741,7 +824,7 @@ function registerConnectCommand(program2) {
741
824
  };
742
825
  delete daemonEnv.CLAUDECODE;
743
826
  delete daemonEnv.CLAUDE_CODE_ENTRYPOINT;
744
- const child = spawn(process.argv[0], [process.argv[1], "_daemon", agentName], {
827
+ const child = spawn(process.argv[0], [process.argv[1], "_daemon", agentId], {
745
828
  detached: true,
746
829
  stdio: ["ignore", logFd, logFd],
747
830
  env: daemonEnv
@@ -751,14 +834,15 @@ function registerConnectCommand(program2) {
751
834
  pid: child.pid,
752
835
  cwd: process.cwd(),
753
836
  runtime: config2.runtime,
837
+ agentId,
754
838
  agentName: config2.agent_name,
755
839
  connectedAt: (/* @__PURE__ */ new Date()).toISOString(),
756
840
  claudePath: options.claudePath
757
841
  };
758
- writeFileSync(getMetaFile(agentName), JSON.stringify(meta, null, 2));
842
+ writeFileSync(getMetaFile(agentId), JSON.stringify(meta, null, 2));
759
843
  const runtimeDisplay = RUNTIME_NAMES[config2.runtime] || config2.runtime;
760
844
  const cwd = process.cwd();
761
- const home = homedir();
845
+ const home = homedir3();
762
846
  const shortLog = logFile.startsWith(home) ? "~" + logFile.slice(home.length) : logFile;
763
847
  console.log("");
764
848
  console.log(` Agent "${config2.agent_name}" is online.`);
@@ -771,7 +855,12 @@ function registerConnectCommand(program2) {
771
855
  console.log("");
772
856
  });
773
857
  program2.command("disconnect <agent-name>").description("Disconnect a local agent.").action((agentName) => {
774
- const pid = readPid(agentName);
858
+ const key = resolveAgentKey(agentName);
859
+ if (!key) {
860
+ console.log(`Agent "${agentName}" is not connected.`);
861
+ return;
862
+ }
863
+ const pid = readPid(key);
775
864
  if (!pid) {
776
865
  console.log(`Agent "${agentName}" is not connected.`);
777
866
  return;
@@ -779,7 +868,7 @@ function registerConnectCommand(program2) {
779
868
  try {
780
869
  process.kill(pid, "SIGTERM");
781
870
  try {
782
- unlinkSync(getPidFile(agentName));
871
+ unlinkSync2(getPidFile(key));
783
872
  } catch {
784
873
  }
785
874
  console.log(`Agent "${agentName}" disconnected.`);
@@ -787,57 +876,17 @@ function registerConnectCommand(program2) {
787
876
  console.error(`Failed to stop process ${pid}.`);
788
877
  }
789
878
  });
790
- program2.command("status").description("Show status of all locally connected agents.").action(() => {
791
- const stateDir = getStateDir();
792
- const metaFiles = readdirSync(stateDir).filter((f) => f.endsWith(".meta.json"));
793
- if (metaFiles.length === 0) {
794
- console.log("No agents found. Use `bx connect <agent-name>` to connect one.");
795
- return;
796
- }
797
- const home = homedir();
798
- const shortenPath = (p) => p.startsWith(home) ? "~" + p.slice(home.length) : p;
799
- const rows = [];
800
- for (const file of metaFiles) {
801
- const agentName = file.replace(".meta.json", "");
802
- const meta = readMeta(agentName);
803
- if (!meta) continue;
804
- const pid = readPid(agentName);
805
- const isOnline = pid !== null;
806
- rows.push({
807
- agent: meta.agentName || agentName,
808
- status: isOnline ? "online" : "offline",
809
- runtime: RUNTIME_NAMES[meta.runtime] || meta.runtime,
810
- directory: shortenPath(meta.cwd),
811
- pid: isOnline ? String(pid) : "-"
812
- });
813
- }
814
- const headers = { agent: "Agent", status: "Status", runtime: "Runtime", directory: "Directory", pid: "PID" };
815
- const cols = Object.keys(headers);
816
- const widths = {};
817
- for (const col of cols) {
818
- widths[col] = Math.max(headers[col].length, ...rows.map((r) => r[col].length));
819
- }
820
- const pad = (s, w) => s + " ".repeat(w - s.length);
821
- const headerLine = cols.map((c) => pad(headers[c], widths[c])).join(" ");
822
- console.log("");
823
- console.log(` ${headerLine}`);
824
- for (const row of rows) {
825
- const line = cols.map((c) => pad(row[c], widths[c])).join(" ");
826
- console.log(` ${line}`);
827
- }
828
- console.log("");
829
- });
830
879
  program2.command("reconnect [agent-name]").description("Reconnect a previously connected agent.").option("--all", "Reconnect all agents").option("--claude-path <path>", "Path to claude CLI binary").action(async (agentName, options) => {
831
880
  if (options.all) {
832
881
  const stateDir = getStateDir();
833
- const metaFiles = readdirSync(stateDir).filter((f) => f.endsWith(".meta.json"));
882
+ const metaFiles = readdirSync3(stateDir).filter((f) => f.endsWith(".meta.json"));
834
883
  if (metaFiles.length === 0) {
835
884
  console.log("No agents found. Use `bx connect <agent-name>` to connect one.");
836
885
  return;
837
886
  }
838
887
  for (const file of metaFiles) {
839
- const name = file.replace(".meta.json", "");
840
- await reconnectAgent(name, options.claudePath);
888
+ const key2 = file.replace(".meta.json", "");
889
+ await reconnectAgent(key2, options.claudePath);
841
890
  }
842
891
  return;
843
892
  }
@@ -845,30 +894,36 @@ function registerConnectCommand(program2) {
845
894
  console.error("Please specify an agent name or use --all.");
846
895
  process.exit(1);
847
896
  }
848
- await reconnectAgent(agentName, options.claudePath);
897
+ const key = resolveAgentKey(agentName);
898
+ if (!key) {
899
+ console.error(`No previous connection found for "${agentName}". Use \`bx connect ${agentName}\` instead.`);
900
+ process.exit(1);
901
+ }
902
+ await reconnectAgent(key, options.claudePath);
849
903
  });
850
- async function reconnectAgent(agentName, claudePathOverride) {
851
- const meta = readMeta(agentName);
904
+ async function reconnectAgent(key, claudePathOverride) {
905
+ const meta = readMeta(key);
852
906
  if (!meta) {
853
- console.error(`No previous connection found for "${agentName}". Use \`bx connect ${agentName}\` instead.`);
907
+ console.error(`No previous connection found for "${key}". Use \`bx connect <agent-name>\` instead.`);
854
908
  return;
855
909
  }
856
- const existingPid = readPid(agentName);
910
+ const existingPid = readPid(key);
857
911
  if (existingPid) {
858
912
  try {
859
913
  process.kill(existingPid, "SIGTERM");
860
914
  } catch {
861
915
  }
862
916
  try {
863
- unlinkSync(getPidFile(agentName));
917
+ unlinkSync2(getPidFile(key));
864
918
  } catch {
865
919
  }
866
920
  }
921
+ const agentId = meta.agentId || key;
867
922
  const config2 = await apiFetchJson(
868
- `/agents/${encodeURIComponent(agentName)}/connection-config`
923
+ `/agents/${encodeURIComponent(agentId)}/connection-config`
869
924
  );
870
925
  const claudePath = claudePathOverride || meta.claudePath || "claude";
871
- const logFile = getLogFile(agentName);
926
+ const logFile = getLogFile(key);
872
927
  const logFd = openSync(logFile, "a");
873
928
  const daemonEnv = {
874
929
  ...process.env,
@@ -879,7 +934,7 @@ function registerConnectCommand(program2) {
879
934
  };
880
935
  delete daemonEnv.CLAUDECODE;
881
936
  delete daemonEnv.CLAUDE_CODE_ENTRYPOINT;
882
- const child = spawn(process.argv[0], [process.argv[1], "_daemon", agentName], {
937
+ const child = spawn(process.argv[0], [process.argv[1], "_daemon", key], {
883
938
  detached: true,
884
939
  stdio: ["ignore", logFd, logFd],
885
940
  cwd: meta.cwd,
@@ -890,13 +945,14 @@ function registerConnectCommand(program2) {
890
945
  pid: child.pid,
891
946
  cwd: meta.cwd,
892
947
  runtime: config2.runtime,
948
+ agentId: config2.agent_id,
893
949
  agentName: config2.agent_name,
894
950
  connectedAt: (/* @__PURE__ */ new Date()).toISOString(),
895
951
  claudePath
896
952
  };
897
- writeFileSync(getMetaFile(agentName), JSON.stringify(newMeta, null, 2));
953
+ writeFileSync(getMetaFile(key), JSON.stringify(newMeta, null, 2));
898
954
  const runtimeDisplay = RUNTIME_NAMES[config2.runtime] || config2.runtime;
899
- const home = homedir();
955
+ const home = homedir3();
900
956
  const shortCwd = meta.cwd.startsWith(home) ? "~" + meta.cwd.slice(home.length) : meta.cwd;
901
957
  const shortLog = logFile.startsWith(home) ? "~" + logFile.slice(home.length) : logFile;
902
958
  console.log("");
@@ -910,7 +966,9 @@ function registerConnectCommand(program2) {
910
966
  }
911
967
 
912
968
  // src/index.ts
913
- var program = new Command().name("bx").description("BoxCrew CLI \u2014 manage your agents from the terminal").version("0.1.0");
969
+ var require2 = createRequire(import.meta.url);
970
+ var { version } = require2("../package.json");
971
+ var program = new Command().name("bx").description("BoxCrew CLI \u2014 manage your agents from the terminal").version(version);
914
972
  registerLoginCommand(program);
915
973
  registerLogoutCommand(program);
916
974
  registerAgentsCommands(program);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@boxcrew/cli",
3
- "version": "0.1.15",
3
+ "version": "0.1.17",
4
4
  "description": "BoxCrew CLI — manage your agents from the terminal",
5
5
  "type": "module",
6
6
  "bin": {