@madarco/agentbox 0.4.1 → 0.6.0

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 (40) hide show
  1. package/dist/{chunk-J35IH7W5.js → chunk-BBZMA2K6.js} +61 -23
  2. package/dist/chunk-BBZMA2K6.js.map +1 -0
  3. package/dist/{chunk-SOMIKEN2.js → chunk-HHMWQNLF.js} +272 -214
  4. package/dist/chunk-HHMWQNLF.js.map +1 -0
  5. package/dist/{chunk-IDR4HVIC.js → chunk-HPZMD5DE.js} +2 -2
  6. package/dist/chunk-HPZMD5DE.js.map +1 -0
  7. package/dist/{chunk-NSIECUCS.js → chunk-HTTKML3C.js} +705 -289
  8. package/dist/chunk-HTTKML3C.js.map +1 -0
  9. package/dist/{chunk-WR5FFGE5.js → chunk-KJNZP6I3.js} +218 -128
  10. package/dist/chunk-KJNZP6I3.js.map +1 -0
  11. package/dist/{chunk-FQD6ZWYW.js → chunk-M7I247BK.js} +68 -65
  12. package/dist/chunk-M7I247BK.js.map +1 -0
  13. package/dist/create-6PWXI6HO-OWAMHBAK.js +15 -0
  14. package/dist/index.js +2394 -1283
  15. package/dist/index.js.map +1 -1
  16. package/dist/{lifecycle-LURNDNYO-UWQYPNPX.js → lifecycle-EMXR46DI-DUVBXNTV.js} +5 -5
  17. package/dist/{state-ZSP3ORXW-WI6KOIG3.js → state-KD7M46ZP-KHFTHFUS.js} +2 -2
  18. package/dist/stats-SZXOJE3D-N7OODCHW.js +19 -0
  19. package/package.json +3 -2
  20. package/runtime/docker/Dockerfile.box +65 -25
  21. package/runtime/docker/apps/cli/share/agentbox-setup/SKILL.md +52 -55
  22. package/runtime/docker/packages/ctl/dist/bin.cjs +272 -160
  23. package/runtime/docker/packages/sandbox-docker/scripts/agentbox-checkpoint-cleanup +52 -0
  24. package/runtime/docker/packages/sandbox-docker/scripts/agentbox-dockerd-start +87 -7
  25. package/runtime/docker/packages/sandbox-docker/scripts/agentbox-open +28 -0
  26. package/runtime/docker/packages/sandbox-docker/scripts/custom-system-CLAUDE.md +21 -15
  27. package/runtime/relay/bin.cjs +407 -12
  28. package/share/agentbox-setup/SKILL.md +52 -55
  29. package/dist/chunk-FQD6ZWYW.js.map +0 -1
  30. package/dist/chunk-IDR4HVIC.js.map +0 -1
  31. package/dist/chunk-J35IH7W5.js.map +0 -1
  32. package/dist/chunk-NSIECUCS.js.map +0 -1
  33. package/dist/chunk-SOMIKEN2.js.map +0 -1
  34. package/dist/chunk-WR5FFGE5.js.map +0 -1
  35. package/dist/create-4BQY2UYU-CGSW3RGE.js +0 -15
  36. package/dist/stats-GZFLPYTU-DBJ2DVBJ.js +0 -19
  37. /package/dist/{create-4BQY2UYU-CGSW3RGE.js.map → create-6PWXI6HO-OWAMHBAK.js.map} +0 -0
  38. /package/dist/{lifecycle-LURNDNYO-UWQYPNPX.js.map → lifecycle-EMXR46DI-DUVBXNTV.js.map} +0 -0
  39. /package/dist/{state-ZSP3ORXW-WI6KOIG3.js.map → state-KD7M46ZP-KHFTHFUS.js.map} +0 -0
  40. /package/dist/{stats-GZFLPYTU-DBJ2DVBJ.js.map → stats-SZXOJE3D-N7OODCHW.js.map} +0 -0
@@ -10371,8 +10371,27 @@ var {
10371
10371
  } = import_index.default;
10372
10372
 
10373
10373
  // src/client.ts
10374
+ var import_node_child_process = require("child_process");
10375
+ var import_node_fs = require("fs");
10374
10376
  var import_node_net = require("net");
10375
- async function connect(opts) {
10377
+ async function tryReviveDaemon(socketPath) {
10378
+ if (process.env.AGENTBOX !== "1") return false;
10379
+ try {
10380
+ const child = (0, import_node_child_process.spawn)("agentbox-ctl", ["daemon"], {
10381
+ detached: true,
10382
+ stdio: "ignore"
10383
+ });
10384
+ child.unref();
10385
+ } catch {
10386
+ return false;
10387
+ }
10388
+ for (let i = 0; i < 50; i++) {
10389
+ await new Promise((r) => setTimeout(r, 100));
10390
+ if ((0, import_node_fs.existsSync)(socketPath)) return true;
10391
+ }
10392
+ return false;
10393
+ }
10394
+ async function connectOnce(opts) {
10376
10395
  const sock = (0, import_node_net.createConnection)(opts.socketPath);
10377
10396
  await new Promise((resolve, reject) => {
10378
10397
  const timer = setTimeout(() => {
@@ -10390,6 +10409,17 @@ async function connect(opts) {
10390
10409
  });
10391
10410
  return sock;
10392
10411
  }
10412
+ async function connect(opts) {
10413
+ try {
10414
+ return await connectOnce(opts);
10415
+ } catch (err) {
10416
+ const code = err.code;
10417
+ if (code !== "ECONNREFUSED" && code !== "ENOENT") throw err;
10418
+ const revived = await tryReviveDaemon(opts.socketPath);
10419
+ if (!revived) throw err;
10420
+ return await connectOnce(opts);
10421
+ }
10422
+ }
10393
10423
  async function sendOneShot(opts, req) {
10394
10424
  const sock = await connect(opts);
10395
10425
  sock.write(`${JSON.stringify(req)}
@@ -10545,6 +10575,108 @@ var claudeStateCommand = new Command("claude-state").description("Report Claude
10545
10575
  process.exit(0);
10546
10576
  });
10547
10577
 
10578
+ // src/relay-rpc.ts
10579
+ var import_node_http = require("http");
10580
+ var import_node_https = require("https");
10581
+ function postRpc(method, params, opts = {}) {
10582
+ const prefix = opts.errorPrefix ?? "agentbox-ctl rpc";
10583
+ const urlStr = process.env.AGENTBOX_RELAY_URL;
10584
+ const token = process.env.AGENTBOX_RELAY_TOKEN;
10585
+ if (!urlStr || !token) {
10586
+ process.stderr.write(
10587
+ `${prefix}: AGENTBOX_RELAY_URL / AGENTBOX_RELAY_TOKEN not set; no relay configured for this box.
10588
+ `
10589
+ );
10590
+ return Promise.resolve({ status: 0, parsed: null, raw: "", internalExitCode: 65 });
10591
+ }
10592
+ let url;
10593
+ try {
10594
+ url = new URL(urlStr);
10595
+ } catch {
10596
+ process.stderr.write(`${prefix}: invalid AGENTBOX_RELAY_URL: ${urlStr}
10597
+ `);
10598
+ return Promise.resolve({ status: 0, parsed: null, raw: "", internalExitCode: 65 });
10599
+ }
10600
+ const body = JSON.stringify({ method, params });
10601
+ const isHttps = url.protocol === "https:";
10602
+ const transport = isHttps ? import_node_https.request : import_node_http.request;
10603
+ const port = url.port.length > 0 ? Number.parseInt(url.port, 10) : isHttps ? 443 : 80;
10604
+ return new Promise((resolve) => {
10605
+ const req = transport(
10606
+ {
10607
+ host: url.hostname,
10608
+ port,
10609
+ method: "POST",
10610
+ path: `${url.pathname.replace(/\/$/, "")}/rpc`,
10611
+ headers: {
10612
+ "Content-Type": "application/json",
10613
+ "Content-Length": Buffer.byteLength(body).toString(),
10614
+ Authorization: `Bearer ${token}`
10615
+ }
10616
+ },
10617
+ (res) => {
10618
+ const chunks = [];
10619
+ res.on("data", (c) => chunks.push(c));
10620
+ res.on("end", () => {
10621
+ const status2 = res.statusCode ?? 0;
10622
+ const text = Buffer.concat(chunks).toString("utf8");
10623
+ let parsed = null;
10624
+ try {
10625
+ const v = JSON.parse(text);
10626
+ if (v && typeof v === "object" && typeof v.exitCode === "number") {
10627
+ parsed = v;
10628
+ }
10629
+ } catch {
10630
+ parsed = null;
10631
+ }
10632
+ resolve({ status: status2, parsed, raw: text, internalExitCode: null });
10633
+ });
10634
+ }
10635
+ );
10636
+ req.on("error", (err) => {
10637
+ process.stderr.write(`${prefix}: ${String(err.message ?? err)}
10638
+ `);
10639
+ resolve({ status: 0, parsed: null, raw: "", internalExitCode: 126 });
10640
+ });
10641
+ req.write(body);
10642
+ req.end();
10643
+ });
10644
+ }
10645
+ async function postRpcAndExit(method, params, opts = {}) {
10646
+ const prefix = opts.errorPrefix ?? "agentbox-ctl rpc";
10647
+ const out = await postRpc(method, params, opts);
10648
+ if (out.internalExitCode !== null) return out.internalExitCode;
10649
+ if (out.parsed) {
10650
+ if (out.parsed.stdout) process.stdout.write(out.parsed.stdout);
10651
+ if (out.parsed.stderr) process.stderr.write(out.parsed.stderr);
10652
+ return out.parsed.exitCode;
10653
+ }
10654
+ process.stderr.write(`${prefix}: relay returned ${String(out.status)}: ${out.raw}
10655
+ `);
10656
+ return out.status >= 200 && out.status < 300 ? 0 : 1;
10657
+ }
10658
+
10659
+ // src/commands/cp.ts
10660
+ var cpCommand = new Command("cp").description("Copy a file/dir between this box and the host (gated by user prompt on the host wrapper)").addCommand(
10661
+ new Command("toHost").description("Copy box:<boxPath> -> host:<hostPath>").argument("<boxPath>", "source path inside the container").argument("<hostPath>", "destination path on the host").option("--no-recursive", "reserved; current implementation is always recursive (docker cp -a)").action(async (boxPath, hostPath, opts) => {
10662
+ const params = { boxPath, hostPath };
10663
+ if (opts.recursive === false) params.recursive = false;
10664
+ const code = await postRpcAndExit("cp.toHost", params, {
10665
+ errorPrefix: "agentbox-ctl cp"
10666
+ });
10667
+ process.exit(code);
10668
+ })
10669
+ ).addCommand(
10670
+ new Command("fromHost").description("Copy host:<hostPath> -> box:<boxPath>").argument("<hostPath>", "source path on the host").argument("<boxPath>", "destination path inside the container").option("--no-recursive", "reserved; current implementation is always recursive (docker cp -a)").action(async (hostPath, boxPath, opts) => {
10671
+ const params = { boxPath, hostPath };
10672
+ if (opts.recursive === false) params.recursive = false;
10673
+ const code = await postRpcAndExit("cp.fromHost", params, {
10674
+ errorPrefix: "agentbox-ctl cp"
10675
+ });
10676
+ process.exit(code);
10677
+ })
10678
+ );
10679
+
10548
10680
  // src/config.ts
10549
10681
  var import_promises = require("fs/promises");
10550
10682
  var import_yaml = __toESM(require_dist(), 1);
@@ -10975,9 +11107,9 @@ function describeCommand(cmd) {
10975
11107
  }
10976
11108
 
10977
11109
  // src/supervisor.ts
10978
- var import_node_child_process = require("child_process");
11110
+ var import_node_child_process2 = require("child_process");
10979
11111
  var import_node_events = require("events");
10980
- var import_node_fs = require("fs");
11112
+ var import_node_fs2 = require("fs");
10981
11113
  var import_promises3 = require("fs/promises");
10982
11114
  var import_node_path = require("path");
10983
11115
 
@@ -11088,8 +11220,8 @@ function startProbe(probe, ctx) {
11088
11220
  }
11089
11221
 
11090
11222
  // src/relay-client.ts
11091
- var import_node_http = require("http");
11092
- var import_node_https = require("https");
11223
+ var import_node_http2 = require("http");
11224
+ var import_node_https2 = require("https");
11093
11225
  var RelayClient = class {
11094
11226
  url;
11095
11227
  token;
@@ -11115,7 +11247,7 @@ var RelayClient = class {
11115
11247
  const url = this.url;
11116
11248
  const body = JSON.stringify({ type, ts: (/* @__PURE__ */ new Date()).toISOString(), payload });
11117
11249
  const isHttps = url.protocol === "https:";
11118
- const transport = isHttps ? import_node_https.request : import_node_http.request;
11250
+ const transport = isHttps ? import_node_https2.request : import_node_http2.request;
11119
11251
  const port = url.port.length > 0 ? Number.parseInt(url.port, 10) : isHttps ? 443 : 80;
11120
11252
  const req = transport(
11121
11253
  {
@@ -11237,12 +11369,26 @@ function spawnArgs(cmd) {
11237
11369
  if (typeof cmd === "string") return { bin: "bash", args: ["-c", cmd] };
11238
11370
  return { bin: cmd[0], args: cmd.slice(1) };
11239
11371
  }
11372
+ var cachedLoginPath;
11373
+ function loginShellPath() {
11374
+ if (cachedLoginPath !== void 0) return cachedLoginPath;
11375
+ try {
11376
+ const out = (0, import_node_child_process2.execFileSync)("bash", ["-lc", 'printf %s "$PATH"'], {
11377
+ encoding: "utf8",
11378
+ timeout: 5e3
11379
+ }).trim();
11380
+ cachedLoginPath = out || (process.env.PATH ?? "");
11381
+ } catch {
11382
+ cachedLoginPath = process.env.PATH ?? "";
11383
+ }
11384
+ return cachedLoginPath;
11385
+ }
11240
11386
  var ServiceRunner = class extends import_node_events.EventEmitter {
11241
11387
  constructor(spec, opts) {
11242
11388
  super();
11243
11389
  this.spec = spec;
11244
11390
  this.opts = opts;
11245
- this.spawnFn = opts.spawn ?? import_node_child_process.spawn;
11391
+ this.spawnFn = opts.spawn ?? import_node_child_process2.spawn;
11246
11392
  this.setTimer = opts.setTimer ?? ((fn, ms) => setTimeout(fn, ms));
11247
11393
  this.clearTimer = opts.clearTimer ?? ((h) => {
11248
11394
  clearTimeout(h);
@@ -11339,7 +11485,7 @@ var ServiceRunner = class extends import_node_events.EventEmitter {
11339
11485
  const spec = this.spec;
11340
11486
  const cwd = resolveCwd(spec.cwd, this.opts.cwd);
11341
11487
  if (!this.logStream) {
11342
- this.logStream = (0, import_node_fs.createWriteStream)((0, import_node_path.join)(this.opts.logDir, `${spec.name}.log`), {
11488
+ this.logStream = (0, import_node_fs2.createWriteStream)((0, import_node_path.join)(this.opts.logDir, `${spec.name}.log`), {
11343
11489
  flags: "a"
11344
11490
  });
11345
11491
  this.logStream.on("error", (err) => {
@@ -11351,7 +11497,7 @@ var ServiceRunner = class extends import_node_events.EventEmitter {
11351
11497
  try {
11352
11498
  child = this.spawnFn(bin, args, {
11353
11499
  cwd,
11354
- env: { ...process.env, ...spec.env ?? {} },
11500
+ env: { ...process.env, PATH: loginShellPath(), ...spec.env ?? {} },
11355
11501
  stdio: ["ignore", "pipe", "pipe"]
11356
11502
  });
11357
11503
  } catch (err) {
@@ -11473,7 +11619,7 @@ var TaskRunner = class extends import_node_events.EventEmitter {
11473
11619
  super();
11474
11620
  this.spec = spec;
11475
11621
  this.opts = opts;
11476
- this.spawnFn = opts.spawn ?? import_node_child_process.spawn;
11622
+ this.spawnFn = opts.spawn ?? import_node_child_process2.spawn;
11477
11623
  }
11478
11624
  spec;
11479
11625
  opts;
@@ -11537,7 +11683,7 @@ var TaskRunner = class extends import_node_events.EventEmitter {
11537
11683
  const spec = this.spec;
11538
11684
  const cwd = resolveCwd(spec.cwd, this.opts.cwd);
11539
11685
  if (!this.logStream) {
11540
- this.logStream = (0, import_node_fs.createWriteStream)((0, import_node_path.join)(this.opts.logDir, `${spec.name}.log`), {
11686
+ this.logStream = (0, import_node_fs2.createWriteStream)((0, import_node_path.join)(this.opts.logDir, `${spec.name}.log`), {
11541
11687
  flags: "a"
11542
11688
  });
11543
11689
  this.logStream.on("error", (err) => {
@@ -11549,7 +11695,7 @@ var TaskRunner = class extends import_node_events.EventEmitter {
11549
11695
  try {
11550
11696
  child = this.spawnFn(bin, args, {
11551
11697
  cwd,
11552
- env: { ...process.env, ...spec.env ?? {} },
11698
+ env: { ...process.env, PATH: loginShellPath(), ...spec.env ?? {} },
11553
11699
  stdio: ["ignore", "pipe", "pipe"]
11554
11700
  });
11555
11701
  } catch (err) {
@@ -11853,6 +11999,14 @@ var Supervisor = class extends import_node_events.EventEmitter {
11853
11999
  changed.push(spec.name);
11854
12000
  }
11855
12001
  }
12002
+ for (const [name, unit] of this.units) {
12003
+ if (unit.kind !== "task") continue;
12004
+ const task = unit;
12005
+ if (task.getState() === "skipped") {
12006
+ this.failed.delete(name);
12007
+ task.resetForRerun();
12008
+ }
12009
+ }
11856
12010
  this.applyWebProxy();
11857
12011
  this.schedule();
11858
12012
  return { added, removed, changed };
@@ -12013,10 +12167,10 @@ var import_promises4 = require("fs/promises");
12013
12167
  var import_node_path2 = require("path");
12014
12168
 
12015
12169
  // src/status-reporter.ts
12016
- var import_node_child_process3 = require("child_process");
12170
+ var import_node_child_process4 = require("child_process");
12017
12171
 
12018
12172
  // src/tmux.ts
12019
- var import_node_child_process2 = require("child_process");
12173
+ var import_node_child_process3 = require("child_process");
12020
12174
  var import_node_os = require("os");
12021
12175
  var MAX_TITLE_LEN = 120;
12022
12176
  function sanitizePaneTitle(raw, ctx) {
@@ -12030,7 +12184,7 @@ function sanitizePaneTitle(raw, ctx) {
12030
12184
  }
12031
12185
  function runTool(cmd, args) {
12032
12186
  return new Promise((resolve) => {
12033
- const child = (0, import_node_child_process2.spawn)(cmd, args, { stdio: ["ignore", "pipe", "pipe"] });
12187
+ const child = (0, import_node_child_process3.spawn)(cmd, args, { stdio: ["ignore", "pipe", "pipe"] });
12034
12188
  let stdout = "";
12035
12189
  let stderr = "";
12036
12190
  child.stdout.on("data", (b) => stdout += b.toString("utf8"));
@@ -12174,7 +12328,7 @@ async function collectPorts(supervisor) {
12174
12328
  }
12175
12329
  function run(cmd, args) {
12176
12330
  return new Promise((resolve) => {
12177
- const child = (0, import_node_child_process3.spawn)(cmd, args, { stdio: ["ignore", "pipe", "ignore"] });
12331
+ const child = (0, import_node_child_process4.spawn)(cmd, args, { stdio: ["ignore", "pipe", "ignore"] });
12178
12332
  let stdout = "";
12179
12333
  child.stdout.on("data", (b) => stdout += b.toString("utf8"));
12180
12334
  child.on("error", () => resolve({ exitCode: 127, stdout }));
@@ -12221,7 +12375,8 @@ async function startServer(opts) {
12221
12375
  resolve();
12222
12376
  });
12223
12377
  });
12224
- await (0, import_promises4.chmod)(opts.socketPath, 432);
12378
+ await (0, import_promises4.chmod)(opts.socketPath, 432).catch(() => {
12379
+ });
12225
12380
  return server;
12226
12381
  }
12227
12382
  async function handleConnection(sock, opts) {
@@ -12427,174 +12582,127 @@ var daemonCommand = new Command("daemon").description("Run the agentbox-ctl supe
12427
12582
  process.on("SIGINT", () => void shutdown("SIGINT"));
12428
12583
  });
12429
12584
 
12430
- // src/commands/checkpoint.ts
12431
- var import_node_http2 = require("http");
12432
- var import_node_https2 = require("https");
12433
- async function rpc(params) {
12434
- const urlStr = process.env.AGENTBOX_RELAY_URL;
12435
- const token = process.env.AGENTBOX_RELAY_TOKEN;
12436
- if (!urlStr || !token) {
12585
+ // src/commands/download.ts
12586
+ var KINDS = ["workspace", "env", "config", "claude"];
12587
+ function isKind(v) {
12588
+ return KINDS.includes(v);
12589
+ }
12590
+ var downloadCommand = new Command("download").description(
12591
+ "Download box contents to the host (gated by user prompt). Kinds: workspace (default), env, config, claude"
12592
+ ).argument("[kind]", `one of: ${KINDS.join(", ")}`, "workspace").action(async (kindArg) => {
12593
+ if (!isKind(kindArg)) {
12437
12594
  process.stderr.write(
12438
- "agentbox-ctl checkpoint: AGENTBOX_RELAY_URL / AGENTBOX_RELAY_TOKEN not set; no relay configured for this box.\n"
12439
- );
12440
- return 65;
12441
- }
12442
- let url;
12443
- try {
12444
- url = new URL(urlStr);
12445
- } catch {
12446
- process.stderr.write(`agentbox-ctl checkpoint: invalid AGENTBOX_RELAY_URL: ${urlStr}
12447
- `);
12448
- return 65;
12449
- }
12450
- const body = JSON.stringify({ method: "checkpoint.create", params });
12451
- const isHttps = url.protocol === "https:";
12452
- const transport = isHttps ? import_node_https2.request : import_node_http2.request;
12453
- const port = url.port.length > 0 ? Number.parseInt(url.port, 10) : isHttps ? 443 : 80;
12454
- return new Promise((resolve) => {
12455
- const req = transport(
12456
- {
12457
- host: url.hostname,
12458
- port,
12459
- method: "POST",
12460
- path: `${url.pathname.replace(/\/$/, "")}/rpc`,
12461
- headers: {
12462
- "Content-Type": "application/json",
12463
- "Content-Length": Buffer.byteLength(body).toString(),
12464
- Authorization: `Bearer ${token}`
12465
- }
12466
- },
12467
- (res) => {
12468
- const chunks = [];
12469
- res.on("data", (c) => chunks.push(c));
12470
- res.on("end", () => {
12471
- const status2 = res.statusCode ?? 0;
12472
- const text = Buffer.concat(chunks).toString("utf8");
12473
- let parsed = null;
12474
- try {
12475
- parsed = JSON.parse(text);
12476
- } catch {
12477
- parsed = null;
12478
- }
12479
- if (parsed && typeof parsed.exitCode === "number") {
12480
- if (parsed.stdout) process.stdout.write(parsed.stdout);
12481
- if (parsed.stderr) process.stderr.write(parsed.stderr);
12482
- resolve(parsed.exitCode);
12483
- return;
12484
- }
12485
- process.stderr.write(
12486
- `agentbox-ctl checkpoint: relay returned ${String(status2)}: ${text}
12595
+ `agentbox-ctl download: unknown kind "${kindArg}"; expected one of: ${KINDS.join(", ")}
12487
12596
  `
12488
- );
12489
- resolve(status2 >= 200 && status2 < 300 ? 0 : 1);
12490
- });
12491
- }
12492
12597
  );
12493
- req.on("error", (err) => {
12494
- process.stderr.write(`agentbox-ctl checkpoint: ${String(err.message ?? err)}
12495
- `);
12496
- resolve(126);
12497
- });
12498
- req.write(body);
12499
- req.end();
12598
+ process.exit(64);
12599
+ }
12600
+ const params = { kind: kindArg };
12601
+ const code = await postRpcAndExit(`download.${kindArg}`, params, {
12602
+ errorPrefix: "agentbox-ctl download"
12500
12603
  });
12501
- }
12502
- var checkpointCommand = new Command("checkpoint").description("Capture this box as a project checkpoint (host-side, via the agentbox relay)").option("--name <name>", "checkpoint name (default: <box-name>-<next>)").option("--merged", "flatten lower+upper into one tree instead of a layered delta").option("--set-default", "mark this checkpoint as the project default for new boxes").action(async (opts) => {
12503
- const params = {};
12504
- if (opts.name) params.name = opts.name;
12505
- if (opts.merged === true) params.merged = true;
12506
- if (opts.setDefault === true) params.setDefault = true;
12507
- const code = await rpc(params);
12508
12604
  process.exit(code);
12509
12605
  });
12510
12606
 
12511
- // src/commands/git.ts
12512
- var import_node_http3 = require("http");
12513
- var import_node_https3 = require("https");
12514
- async function rpc2(method, opts, extra) {
12515
- const urlStr = process.env.AGENTBOX_RELAY_URL;
12516
- const token = process.env.AGENTBOX_RELAY_TOKEN;
12517
- if (!urlStr || !token) {
12518
- process.stderr.write(
12519
- "agentbox-ctl git: AGENTBOX_RELAY_URL / AGENTBOX_RELAY_TOKEN not set; no relay configured for this box.\n"
12520
- );
12521
- return 65;
12522
- }
12523
- let url;
12524
- try {
12525
- url = new URL(urlStr);
12526
- } catch {
12527
- process.stderr.write(`agentbox-ctl git: invalid AGENTBOX_RELAY_URL: ${urlStr}
12528
- `);
12529
- return 65;
12607
+ // src/commands/checkpoint.ts
12608
+ var checkpointCommand = new Command("checkpoint").description("Capture this box as a project checkpoint (host-side, via the agentbox relay)").option("--name <name>", "checkpoint name (default: <box-name>-<next>)").option("--merged", "flatten lower+upper into one tree instead of a layered delta").option("--set-default", "mark this checkpoint as the project default for new boxes").option(
12609
+ "--replace",
12610
+ "if a checkpoint with the same name exists, rm it first (idempotent recapture; safe to retry when the previous run's stdout was lost)"
12611
+ ).action(
12612
+ async (opts) => {
12613
+ const params = {};
12614
+ if (opts.name) params.name = opts.name;
12615
+ if (opts.merged === true) params.merged = true;
12616
+ if (opts.setDefault === true) params.setDefault = true;
12617
+ if (opts.replace === true) params.replace = true;
12618
+ const code = await postRpcAndExit("checkpoint.create", params, {
12619
+ errorPrefix: "agentbox-ctl checkpoint"
12620
+ });
12621
+ process.exit(code);
12530
12622
  }
12531
- const params = {
12532
- path: opts.cwd ?? process.cwd()
12533
- };
12623
+ );
12624
+
12625
+ // src/commands/git.ts
12626
+ var import_node_child_process5 = require("child_process");
12627
+ function buildParams(opts, extra) {
12628
+ const params = { path: opts.cwd ?? process.cwd() };
12534
12629
  if (opts.remote) params.remote = opts.remote;
12535
12630
  if (extra.length > 0) params.args = extra;
12536
- const body = JSON.stringify({ method, params });
12537
- const isHttps = url.protocol === "https:";
12538
- const transport = isHttps ? import_node_https3.request : import_node_http3.request;
12539
- const port = url.port.length > 0 ? Number.parseInt(url.port, 10) : isHttps ? 443 : 80;
12631
+ return params;
12632
+ }
12633
+ function runLocalGit(args, cwd) {
12540
12634
  return new Promise((resolve) => {
12541
- const req = transport(
12542
- {
12543
- host: url.hostname,
12544
- port,
12545
- method: "POST",
12546
- path: `${url.pathname.replace(/\/$/, "")}/rpc`,
12547
- headers: {
12548
- "Content-Type": "application/json",
12549
- "Content-Length": Buffer.byteLength(body).toString(),
12550
- Authorization: `Bearer ${token}`
12551
- }
12552
- },
12553
- (res) => {
12554
- const chunks = [];
12555
- res.on("data", (c) => chunks.push(c));
12556
- res.on("end", () => {
12557
- const status2 = res.statusCode ?? 0;
12558
- const text = Buffer.concat(chunks).toString("utf8");
12559
- let parsed = null;
12560
- try {
12561
- parsed = JSON.parse(text);
12562
- } catch {
12563
- parsed = null;
12564
- }
12565
- if (parsed && typeof parsed.exitCode === "number") {
12566
- if (parsed.stdout) process.stdout.write(parsed.stdout);
12567
- if (parsed.stderr) process.stderr.write(parsed.stderr);
12568
- resolve(parsed.exitCode);
12569
- return;
12570
- }
12571
- process.stderr.write(`agentbox-ctl git: relay returned ${String(status2)}: ${text}
12572
- `);
12573
- resolve(status2 >= 200 && status2 < 300 ? 0 : 1);
12574
- });
12575
- }
12576
- );
12577
- req.on("error", (err) => {
12635
+ const child = (0, import_node_child_process5.spawn)("git", args, { cwd, stdio: "inherit" });
12636
+ child.on("close", (code) => resolve(code ?? 1));
12637
+ child.on("error", (err) => {
12578
12638
  process.stderr.write(`agentbox-ctl git: ${String(err.message ?? err)}
12579
12639
  `);
12580
12640
  resolve(126);
12581
12641
  });
12582
- req.write(body);
12583
- req.end();
12584
12642
  });
12585
12643
  }
12586
12644
  var gitCommand = new Command("git").description("Git operations that need host credentials (routed through the agentbox relay)").addCommand(
12587
- new Command("pull").description("Run `git pull` on the host worktree for this box").option("--remote <name>", "remote name (default: origin)").option("--cwd <path>", "path inside the container identifying which worktree to use").allowExcessArguments(true).allowUnknownOption(true).argument("[args...]", "additional args forwarded to git pull").action(async (args, opts) => {
12588
- const code = await rpc2("git.pull", opts, args);
12645
+ new Command("push").description("Run `git push` on the host main repo against this box's branch (user is prompted on the host wrapper to confirm)").option("--remote <name>", "remote name (default: origin)").option("--cwd <path>", "container path identifying which registered worktree to use").allowExcessArguments(true).allowUnknownOption(true).argument("[args...]", "additional args forwarded to git push").action(async (args, opts) => {
12646
+ const code = await postRpcAndExit("git.push", buildParams(opts, args), {
12647
+ errorPrefix: "agentbox-ctl git"
12648
+ });
12589
12649
  process.exit(code);
12590
12650
  })
12591
12651
  ).addCommand(
12592
- new Command("push").description("Run `git push` on the host worktree for this box").option("--remote <name>", "remote name (default: origin)").option("--cwd <path>", "path inside the container identifying which worktree to use").allowExcessArguments(true).allowUnknownOption(true).argument("[args...]", "additional args forwarded to git push").action(async (args, opts) => {
12593
- const code = await rpc2("git.push", opts, args);
12652
+ new Command("fetch").description("Run `git fetch` on the host main repo (refs land in the shared .git)").option("--remote <name>", "remote name (default: origin)").option("--cwd <path>", "container path identifying which registered worktree to use").allowExcessArguments(true).allowUnknownOption(true).argument("[args...]", "additional args forwarded to git fetch").action(async (args, opts) => {
12653
+ const code = await postRpcAndExit("git.fetch", buildParams(opts, args), {
12654
+ errorPrefix: "agentbox-ctl git"
12655
+ });
12594
12656
  process.exit(code);
12595
12657
  })
12658
+ ).addCommand(
12659
+ new Command("pull").description(
12660
+ "Fetch via the relay (host creds), then merge into the in-container working tree locally"
12661
+ ).option("--remote <name>", "remote name (default: origin)").option("--cwd <path>", "container path identifying which registered worktree to use").option("--ff-only", "pass --ff-only to the local merge").allowExcessArguments(true).allowUnknownOption(true).argument("[args...]", "additional args forwarded to git fetch").action(
12662
+ async (args, opts) => {
12663
+ const fetchCode = await postRpcAndExit("git.fetch", buildParams(opts, args), {
12664
+ errorPrefix: "agentbox-ctl git"
12665
+ });
12666
+ if (fetchCode !== 0) process.exit(fetchCode);
12667
+ const remote = opts.remote ?? "origin";
12668
+ const cwd = opts.cwd ?? process.cwd();
12669
+ const mergeArgs = ["merge"];
12670
+ if (opts.ffOnly) mergeArgs.push("--ff-only");
12671
+ mergeArgs.push(`${remote}/HEAD`);
12672
+ const mergeCode = await runLocalGit(mergeArgs, cwd);
12673
+ process.exit(mergeCode);
12674
+ }
12675
+ )
12676
+ );
12677
+
12678
+ // src/commands/notify.ts
12679
+ async function reportState(opts, state) {
12680
+ try {
12681
+ await claudeState({ socketPath: opts.socket, timeoutMs: 1500 }, state);
12682
+ } catch {
12683
+ }
12684
+ }
12685
+ var notifyCommand = new Command("notify").description(
12686
+ "Signal that the in-box agent is waiting for user input (highlights the box in the dashboard)"
12687
+ ).option("--socket <path>", "unix socket path", DEFAULT_SOCKET_PATH).option("--message <text>", "reserved for future use; accepted but ignored in v1").action(async (opts) => {
12688
+ await reportState(opts, "waiting");
12689
+ process.exit(0);
12690
+ }).addCommand(
12691
+ new Command("clear").description("Clear the waiting state (alias for `claude-state idle`)").option("--socket <path>", "unix socket path", DEFAULT_SOCKET_PATH).action(async (opts) => {
12692
+ await reportState(opts, "idle");
12693
+ process.exit(0);
12694
+ })
12596
12695
  );
12597
12696
 
12697
+ // src/commands/open.ts
12698
+ var openCommand = new Command("open").description("Open a URL in the host's default browser (via the agentbox relay)").argument("<url>", "http(s) URL to open on the host").action(async (url) => {
12699
+ const params = { url };
12700
+ const code = await postRpcAndExit("browser.open", params, {
12701
+ errorPrefix: "agentbox-ctl open"
12702
+ });
12703
+ process.exit(code);
12704
+ });
12705
+
12598
12706
  // src/render.ts
12599
12707
  function renderStatusTable(rows) {
12600
12708
  if (rows.length === 0) return "(no services configured)";
@@ -12783,6 +12891,10 @@ program2.addCommand(waitReadyCommand);
12783
12891
  program2.addCommand(runTaskCommand);
12784
12892
  program2.addCommand(gitCommand);
12785
12893
  program2.addCommand(checkpointCommand);
12894
+ program2.addCommand(cpCommand);
12895
+ program2.addCommand(downloadCommand);
12896
+ program2.addCommand(notifyCommand);
12897
+ program2.addCommand(openCommand);
12786
12898
  program2.parseAsync(process.argv).catch((err) => {
12787
12899
  const msg = err instanceof Error ? err.message : String(err);
12788
12900
  process.stderr.write(`agentbox-ctl: ${msg}
@@ -0,0 +1,52 @@
1
+ #!/usr/bin/env bash
2
+ # Pre-`docker commit` cleanup: strip ephemeral / disposable state so the
3
+ # captured checkpoint image is closer to "warm project state, nothing else".
4
+ #
5
+ # Invoked by the host via `docker exec --user root <container>
6
+ # /usr/local/bin/agentbox-checkpoint-cleanup` right before
7
+ # `docker commit`. Best-effort: every step is allowed to fail (a checkpoint
8
+ # capture should never block on cleanup hiccups).
9
+ #
10
+ # What we DELIBERATELY keep:
11
+ # - /workspace the actual point of the checkpoint
12
+ # - /home/vscode/.npm warm npm cache (next install is fast)
13
+ # - /home/vscode/.cache pnpm/yarn/Cargo/etc. caches
14
+ # - /var/lib/docker in-box dockerd's data root
15
+ # - /home/vscode/.claude the named volume is bind-mounted; image
16
+ # layer never sees it anyway
17
+ set +e
18
+
19
+ # apt: drop downloaded .deb cache and the package index. The index is ~50MB
20
+ # and gets refreshed on the next `apt-get update`; the .deb cache is reusable
21
+ # only if we don't change versions, which we usually do.
22
+ apt-get clean 2>/dev/null
23
+ rm -rf /var/lib/apt/lists/* 2>/dev/null
24
+
25
+ # Throwaway scratch dirs. Preserve /tmp/claude-* — that is the live in-box
26
+ # Claude Code session's working tree (its per-task stdout/stderr files). The
27
+ # agent that triggered this checkpoint *is* that session; deleting its task
28
+ # output mid-run makes its harness see ENOENT, treat the command as failed,
29
+ # and retry the checkpoint (observed: 5 duplicate auto-named checkpoints).
30
+ # Stale claude-* dirs baked into the image are tiny and Claude Code prunes
31
+ # them itself on the next session start.
32
+ find /tmp /var/tmp -mindepth 1 -maxdepth 1 ! -name 'claude-*' -exec rm -rf {} + 2>/dev/null
33
+
34
+ # Logs: truncate (don't delete) so the original file modes / ownerships stay
35
+ # intact for the next run. Targets common rotated archives too.
36
+ find /var/log -type f \( -name '*.log' -o -name '*.gz' -o -name '*.1' \) \
37
+ -exec truncate -s0 {} + 2>/dev/null
38
+ find /var/log/agentbox -type f -exec truncate -s0 {} + 2>/dev/null
39
+
40
+ # Bash history (root + vscode). Re-assert vscode ownership: `: >` run as root
41
+ # (re)creates the file root-owned 0644 when it didn't exist, which the uid-1000
42
+ # vscode user cannot append to, silently dropping all shell history.
43
+ : > /root/.bash_history 2>/dev/null
44
+ : > /home/vscode/.bash_history 2>/dev/null
45
+ chown vscode:vscode /home/vscode/.bash_history 2>/dev/null
46
+ chmod 600 /home/vscode/.bash_history 2>/dev/null
47
+
48
+ # Anthropic's installer writes a transient marker; redundant once the binary
49
+ # is in place. Safe to wipe.
50
+ rm -rf /home/vscode/.claude-installer 2>/dev/null
51
+
52
+ exit 0