@madarco/agentbox 0.14.0 → 0.15.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/CHANGELOG.md +60 -0
  2. package/dist/{_cloud-attach-GUBB5RH2.js → _cloud-attach-R6TRWG5L.js} +3 -3
  3. package/dist/{chunk-BYCLD6D6.js → chunk-43Q5GWP6.js} +98 -54
  4. package/dist/chunk-43Q5GWP6.js.map +1 -0
  5. package/dist/{chunk-VATTS2MR.js → chunk-72CJTXN6.js} +2 -2
  6. package/dist/{chunk-TBSIJVSN.js → chunk-E7CHS7ZR.js} +21 -13
  7. package/dist/chunk-E7CHS7ZR.js.map +1 -0
  8. package/dist/{chunk-LDMYHWUS.js → chunk-MCOU6CZS.js} +2 -2
  9. package/dist/{chunk-TCS5HXJX.js → chunk-MLMFNN4T.js} +396 -324
  10. package/dist/chunk-MLMFNN4T.js.map +1 -0
  11. package/dist/{dist-J2IHD5T7.js → dist-AGTIA7AD.js} +3 -3
  12. package/dist/{dist-3IMQNTTV.js → dist-FIFEFKJ7.js} +3 -3
  13. package/dist/{dist-34RKQ74M.js → dist-JZ3XO6EB.js} +4 -4
  14. package/dist/{dist-4DPOL5A7.js → dist-OGJGZETZ.js} +2 -2
  15. package/dist/{dist-57M6ZA7H.js → dist-S4XR4ACV.js} +4 -4
  16. package/dist/index.js +868 -300
  17. package/dist/index.js.map +1 -1
  18. package/package.json +5 -5
  19. package/runtime/docker/apps/cli/share/agentbox-setup/SKILL.md +30 -0
  20. package/runtime/docker/packages/ctl/dist/bin.cjs +321 -27
  21. package/runtime/e2b/agentbox-setup-skill.md +30 -0
  22. package/runtime/e2b/ctl.cjs +321 -27
  23. package/runtime/hetzner/agentbox-setup-skill.md +30 -0
  24. package/runtime/hetzner/ctl.cjs +321 -27
  25. package/runtime/relay/bin.cjs +83 -5
  26. package/runtime/vercel/agentbox-setup-skill.md +30 -0
  27. package/runtime/vercel/ctl.cjs +321 -27
  28. package/share/agentbox-setup/SKILL.md +30 -0
  29. package/share/host-skills/agentbox-info/SKILL.md +21 -1
  30. package/dist/chunk-BYCLD6D6.js.map +0 -1
  31. package/dist/chunk-TBSIJVSN.js.map +0 -1
  32. package/dist/chunk-TCS5HXJX.js.map +0 -1
  33. /package/dist/{_cloud-attach-GUBB5RH2.js.map → _cloud-attach-R6TRWG5L.js.map} +0 -0
  34. /package/dist/{chunk-VATTS2MR.js.map → chunk-72CJTXN6.js.map} +0 -0
  35. /package/dist/{chunk-LDMYHWUS.js.map → chunk-MCOU6CZS.js.map} +0 -0
  36. /package/dist/{dist-J2IHD5T7.js.map → dist-AGTIA7AD.js.map} +0 -0
  37. /package/dist/{dist-3IMQNTTV.js.map → dist-FIFEFKJ7.js.map} +0 -0
  38. /package/dist/{dist-34RKQ74M.js.map → dist-JZ3XO6EB.js.map} +0 -0
  39. /package/dist/{dist-4DPOL5A7.js.map → dist-OGJGZETZ.js.map} +0 -0
  40. /package/dist/{dist-57M6ZA7H.js.map → dist-S4XR4ACV.js.map} +0 -0
@@ -3801,7 +3801,7 @@ var require_cross_spawn = __commonJS({
3801
3801
  var cp = require("child_process");
3802
3802
  var parse = require_parse();
3803
3803
  var enoent = require_enoent();
3804
- function spawn10(command, args, options) {
3804
+ function spawn11(command, args, options) {
3805
3805
  const parsed = parse(command, args, options);
3806
3806
  const spawned = cp.spawn(parsed.command, parsed.args, parsed.options);
3807
3807
  enoent.hookChildProcess(spawned, parsed);
@@ -3813,8 +3813,8 @@ var require_cross_spawn = __commonJS({
3813
3813
  result.error = result.error || enoent.verifyENOENTSync(result.status, parsed);
3814
3814
  return result;
3815
3815
  }
3816
- module2.exports = spawn10;
3817
- module2.exports.spawn = spawn10;
3816
+ module2.exports = spawn11;
3817
+ module2.exports.spawn = spawn11;
3818
3818
  module2.exports.sync = spawnSync2;
3819
3819
  module2.exports._parse = parse;
3820
3820
  module2.exports._enoent = enoent;
@@ -11566,20 +11566,28 @@ async function postRpcAndExit(method, params, opts = {}) {
11566
11566
  }
11567
11567
 
11568
11568
  // src/commands/cp.ts
11569
+ function collectExclude(val, acc) {
11570
+ acc.push(val);
11571
+ return acc;
11572
+ }
11573
+ function buildCpParams(boxPath, hostPath, opts) {
11574
+ const params = { boxPath, hostPath };
11575
+ if (opts.recursive === false) params.recursive = false;
11576
+ if (opts.exclude.length > 0) params.exclude = opts.exclude;
11577
+ if (opts.defaultExcludes === false) params.defaultExcludes = false;
11578
+ if (opts.yes) params.yes = true;
11579
+ return params;
11580
+ }
11569
11581
  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(
11570
- 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) => {
11571
- const params = { boxPath, hostPath };
11572
- if (opts.recursive === false) params.recursive = false;
11573
- const code = await postRpcAndExit("cp.toHost", params, {
11582
+ 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)").option("--exclude <pattern>", "exclude paths matching <pattern> (repeatable)", collectExclude, []).option("--no-default-excludes", "keep heavy dirs the host drops by default (.git, node_modules, ...)").option("-y, --yes", "copy even if the source is over the host size limit").action(async (boxPath, hostPath, opts) => {
11583
+ const code = await postRpcAndExit("cp.toHost", buildCpParams(boxPath, hostPath, opts), {
11574
11584
  errorPrefix: "agentbox-ctl cp"
11575
11585
  });
11576
11586
  process.exit(code);
11577
11587
  })
11578
11588
  ).addCommand(
11579
- 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) => {
11580
- const params = { boxPath, hostPath };
11581
- if (opts.recursive === false) params.recursive = false;
11582
- const code = await postRpcAndExit("cp.fromHost", params, {
11589
+ 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)").option("--exclude <pattern>", "exclude paths matching <pattern> (repeatable)", collectExclude, []).option("--no-default-excludes", "keep heavy dirs the host drops by default (.git, node_modules, ...)").option("-y, --yes", "copy even if the source is over the host size limit").action(async (hostPath, boxPath, opts) => {
11590
+ const code = await postRpcAndExit("cp.fromHost", buildCpParams(boxPath, hostPath, opts), {
11583
11591
  errorPrefix: "agentbox-ctl cp"
11584
11592
  });
11585
11593
  process.exit(code);
@@ -18468,6 +18476,115 @@ var import_path2 = require("path");
18468
18476
  var import_yaml2 = __toESM(require_dist(), 1);
18469
18477
  var import_path3 = require("path");
18470
18478
  var import_yaml3 = __toESM(require_dist(), 1);
18479
+ var BUILT_IN_DEFAULTS = {
18480
+ box: {
18481
+ provider: "docker",
18482
+ hostSnapshot: void 0,
18483
+ defaultCheckpoint: "",
18484
+ defaultCheckpointDocker: "",
18485
+ defaultCheckpointDaytona: "",
18486
+ defaultCheckpointHetzner: "",
18487
+ defaultCheckpointVercel: "",
18488
+ defaultCheckpointE2b: "",
18489
+ size: "",
18490
+ sizeDocker: "",
18491
+ sizeDaytona: "",
18492
+ sizeHetzner: "",
18493
+ sizeVercel: "",
18494
+ sizeE2b: "",
18495
+ withPlaywright: false,
18496
+ withEnv: false,
18497
+ resyncOnStart: true,
18498
+ vnc: true,
18499
+ autoApproveHostActions: false,
18500
+ isolateClaudeConfig: false,
18501
+ isolateCodexConfig: false,
18502
+ isolateOpencodeConfig: false,
18503
+ image: "agentbox/box:dev",
18504
+ imageDocker: "",
18505
+ imageDaytona: "",
18506
+ imageHetzner: "",
18507
+ imageVercel: "",
18508
+ imageE2b: "",
18509
+ // Mirrors BOX_IMAGE_REGISTRY in @agentbox/sandbox-docker. Empty disables the
18510
+ // registry pull (always build the docker base image locally).
18511
+ imageRegistry: "ghcr.io/madarco/agentbox/box",
18512
+ dockerCacheShared: false,
18513
+ memory: 0,
18514
+ cpus: 0,
18515
+ pidsLimit: 0,
18516
+ disk: "",
18517
+ bundleDepth: void 0,
18518
+ vercelVcpus: 2,
18519
+ vercelTimeoutMs: 27e5,
18520
+ vercelNetworkPolicy: "",
18521
+ cpMaxBytes: 100 * 1024 * 1024
18522
+ },
18523
+ checkpoint: {
18524
+ maxLayers: 3
18525
+ },
18526
+ claude: {
18527
+ sessionName: "claude",
18528
+ dangerouslySkipPermissions: true
18529
+ },
18530
+ codex: {
18531
+ sessionName: "codex",
18532
+ dangerouslySkipPermissions: true
18533
+ },
18534
+ opencode: {
18535
+ sessionName: "opencode"
18536
+ },
18537
+ attach: {
18538
+ openIn: "split",
18539
+ cmuxStatus: true
18540
+ },
18541
+ code: {
18542
+ ide: "auto",
18543
+ wait: true,
18544
+ timeoutMs: 12e4,
18545
+ autoTerminals: true
18546
+ },
18547
+ shell: {
18548
+ user: "vscode",
18549
+ login: true,
18550
+ tmux: true
18551
+ },
18552
+ engine: {
18553
+ kind: "auto"
18554
+ },
18555
+ browser: {
18556
+ default: "agent-browser"
18557
+ },
18558
+ relay: {
18559
+ port: 8787
18560
+ },
18561
+ vnc: {
18562
+ containerPort: 6080
18563
+ },
18564
+ portless: {
18565
+ enabled: void 0,
18566
+ stateDir: ""
18567
+ },
18568
+ autopause: {
18569
+ enabled: true,
18570
+ maxRunningBoxes: 5,
18571
+ idleMinutes: 5
18572
+ },
18573
+ queue: {
18574
+ enabled: true,
18575
+ maxConcurrent: 5,
18576
+ maxWorking: 0,
18577
+ idleGraceSeconds: 15,
18578
+ openIn: "none"
18579
+ },
18580
+ cloud: {
18581
+ useCurrentBranch: false
18582
+ },
18583
+ maintenance: {
18584
+ pruneProjectConfigs: true,
18585
+ pruneProjectConfigsEvery: 50
18586
+ }
18587
+ };
18471
18588
  var KEY_REGISTRY = [
18472
18589
  {
18473
18590
  key: "box.provider",
@@ -18576,6 +18693,11 @@ var KEY_REGISTRY = [
18576
18693
  type: "bool",
18577
18694
  description: "Run the per-box Xvnc + noVNC stack."
18578
18695
  },
18696
+ {
18697
+ key: "box.autoApproveHostActions",
18698
+ type: "bool",
18699
+ description: "Auto-approve host-action confirmations (git push, cp host<->box, gh PR writes, checkpoint) for this box without an interactive prompt. Off by default; intended for unattended orchestration of trusted boxes. Each auto-approval is recorded as a relay event (visible in `agentbox agent` / the dashboard)."
18700
+ },
18579
18701
  {
18580
18702
  key: "box.isolateClaudeConfig",
18581
18703
  type: "bool",
@@ -18674,6 +18796,12 @@ var KEY_REGISTRY = [
18674
18796
  type: "int",
18675
18797
  description: "Max session length (ms) for new --provider vercel boxes before the VM auto-snapshots; persistent mode auto-resumes on the next call. Default 2700000 (45 min, the Hobby ceiling). Vercel-only."
18676
18798
  },
18799
+ {
18800
+ key: "box.cpMaxBytes",
18801
+ type: "int",
18802
+ description: "Max bytes a single host\u2192box copy may transfer after excludes, shared by `agentbox cp` (blocked with a size breakdown unless --yes) and each `carry:` entry (rejected at resolve time). Default 104857600 (100 MiB).",
18803
+ advanced: true
18804
+ },
18677
18805
  {
18678
18806
  key: "box.vercelNetworkPolicy",
18679
18807
  type: "string",
@@ -18821,6 +18949,12 @@ var KEY_REGISTRY = [
18821
18949
  type: "int",
18822
18950
  description: "Seconds an agent must stay non-working before it frees its working slot (debounce against brief idle flaps between turns). Only used when queue.maxWorking > 0."
18823
18951
  },
18952
+ {
18953
+ key: "queue.openIn",
18954
+ type: "enum",
18955
+ enumValues: ["none", "split", "window", "tab"],
18956
+ description: "When a background `-i` job finishes creating its box, where the host relay opens an attached terminal onto it: `none` (default \u2014 open nothing, just queue), `split`, `window`, or `tab`. Honored only when the submitting shell runs inside tmux, cmux, or iTerm2 (the targeting is captured at submit time). Under cmux, `split` splits the pane you submitted from (falling back to the parent workspace, then a new workspace), `tab` adds a tab in the parent workspace, and `window` opens a separate workspace; iTerm2 opens relative to the frontmost window. Unlike `attach.openIn` there is no `same` mode \u2014 the box is created asynchronously, so it is always a fresh terminal."
18957
+ },
18824
18958
  {
18825
18959
  key: "cloud.useCurrentBranch",
18826
18960
  type: "bool",
@@ -19008,6 +19142,21 @@ var EventBuffer = class {
19008
19142
  };
19009
19143
  var PendingPrompts = class {
19010
19144
  entries = /* @__PURE__ */ new Map();
19145
+ autoApprove = null;
19146
+ /** Install the per-box auto-approve policy (relay server, once at startup). */
19147
+ setAutoApprovePolicy(policy) {
19148
+ this.autoApprove = policy;
19149
+ }
19150
+ /**
19151
+ * True when this box opted into `box.autoApproveHostActions`. Records the
19152
+ * bypass to the audit sink as a side effect so the caller short-circuits
19153
+ * with a trail. Returns false when no policy is installed.
19154
+ */
19155
+ consumeAutoApprove(boxId, params) {
19156
+ if (!this.autoApprove || !this.autoApprove.shouldAutoApprove(boxId)) return false;
19157
+ this.autoApprove.audit(boxId, params);
19158
+ return true;
19159
+ }
19011
19160
  add(boxId, ev) {
19012
19161
  return new Promise((resolve2) => {
19013
19162
  this.entries.set(ev.id, {
@@ -19094,6 +19243,9 @@ async function askPrompt(prompts, subscribers, boxId, params, opts) {
19094
19243
  if (process.env.AGENTBOX_PROMPT === "off") {
19095
19244
  return { answer: "y" };
19096
19245
  }
19246
+ if (prompts.consumeAutoApprove(boxId, params)) {
19247
+ return { answer: "y" };
19248
+ }
19097
19249
  const ev = { id: (0, import_crypto2.randomUUID)(), ...params };
19098
19250
  const promise = prompts.add(boxId, ev);
19099
19251
  subscribers.broadcast(boxId, "prompt-ask", ev);
@@ -20240,6 +20392,21 @@ function createRelayServer(opts) {
20240
20392
  const prompts = new PendingPrompts();
20241
20393
  const subscribers = new PromptSubscribers();
20242
20394
  const notices = new BoxNotices(subscribers);
20395
+ prompts.setAutoApprovePolicy({
20396
+ shouldAutoApprove: (boxId) => registry.get(boxId)?.autoApproveHostActions === true,
20397
+ audit: (boxId, params) => {
20398
+ events.append({
20399
+ boxId,
20400
+ type: "host-action-auto-approved",
20401
+ payload: {
20402
+ command: params.context?.command,
20403
+ argv: params.context?.argv,
20404
+ message: params.message
20405
+ }
20406
+ });
20407
+ log(`auto-approved host action for ${boxId}: ${params.context?.command ?? params.message}`);
20408
+ }
20409
+ });
20243
20410
  const hostInitiatedTokens = new HostInitiatedTokens();
20244
20411
  let queuePoke = null;
20245
20412
  const host = opts.host ?? "0.0.0.0";
@@ -20427,11 +20594,22 @@ function createRelayServer(opts) {
20427
20594
  send(res, 400, { error: "cp.* requires {boxPath, hostPath} strings" });
20428
20595
  return;
20429
20596
  }
20597
+ if (params.exclude !== void 0 && (!Array.isArray(params.exclude) || params.exclude.some((p) => typeof p !== "string"))) {
20598
+ send(res, 400, { error: "cp.* exclude must be an array of strings" });
20599
+ return;
20600
+ }
20430
20601
  const direction = body.method === "cp.toHost" ? "box -> host" : "host -> box";
20602
+ const pathDetail = body.method === "cp.toHost" ? `${params.boxPath} -> ${params.hostPath}` : `${params.hostPath} -> ${params.boxPath}`;
20603
+ const detailParts = [pathDetail];
20604
+ if (params.exclude && params.exclude.length > 0) {
20605
+ detailParts.push(`exclude: ${params.exclude.join(", ")}`);
20606
+ }
20607
+ if (params.defaultExcludes === false) detailParts.push("(default excludes off)");
20608
+ if (params.yes) detailParts.push("(over size limit \u2014 confirmed)");
20431
20609
  const verdict = await askPrompt(prompts, subscribers, reg.boxId, {
20432
20610
  kind: "confirm",
20433
20611
  message: `Allow cp (${direction}) on ${reg.name}?`,
20434
- detail: body.method === "cp.toHost" ? `${params.boxPath} -> ${params.hostPath}` : `${params.hostPath} -> ${params.boxPath}`,
20612
+ detail: detailParts.join("\n"),
20435
20613
  defaultAnswer: "n",
20436
20614
  context: {
20437
20615
  command: body.method,
@@ -20597,7 +20775,8 @@ function createRelayServer(opts) {
20597
20775
  worktrees,
20598
20776
  previewUrl: typeof body.previewUrl === "string" && body.previewUrl.length > 0 ? body.previewUrl : void 0,
20599
20777
  previewToken: typeof body.previewToken === "string" && body.previewToken.length > 0 ? body.previewToken : void 0,
20600
- bridgeToken: typeof body.bridgeToken === "string" && body.bridgeToken.length > 0 ? body.bridgeToken : void 0
20778
+ bridgeToken: typeof body.bridgeToken === "string" && body.bridgeToken.length > 0 ? body.bridgeToken : void 0,
20779
+ autoApproveHostActions: body.autoApproveHostActions === true
20601
20780
  };
20602
20781
  registry.register(reg);
20603
20782
  log(
@@ -20705,6 +20884,15 @@ function createRelayServer(opts) {
20705
20884
  send(res, 200, { boxes: redacted });
20706
20885
  return;
20707
20886
  }
20887
+ if (route === "GET /admin/prompts") {
20888
+ const boxId = url.searchParams.get("boxId") ?? "";
20889
+ if (boxId.length === 0) {
20890
+ send(res, 400, { error: "missing boxId query param" });
20891
+ return;
20892
+ }
20893
+ send(res, 200, { prompts: prompts.forBox(boxId) });
20894
+ return;
20895
+ }
20708
20896
  if (route === "GET /admin/prompts/stream") {
20709
20897
  const boxId = url.searchParams.get("boxId") ?? "";
20710
20898
  if (boxId.length === 0) {
@@ -21022,7 +21210,11 @@ async function handleCpRpc(reg, method, params) {
21022
21210
  };
21023
21211
  }
21024
21212
  const boxRef = `${reg.name}:${params.boxPath}`;
21025
- const argv = method === "cp.toHost" ? [process.execPath, entry, "cp", boxRef, params.hostPath] : [process.execPath, entry, "cp", params.hostPath, boxRef];
21213
+ const flags = [];
21214
+ for (const pat of params.exclude ?? []) flags.push("--exclude", pat);
21215
+ if (params.defaultExcludes === false) flags.push("--no-default-excludes");
21216
+ if (params.yes) flags.push("--yes");
21217
+ const argv = method === "cp.toHost" ? [process.execPath, entry, "cp", boxRef, params.hostPath, ...flags] : [process.execPath, entry, "cp", params.hostPath, boxRef, ...flags];
21026
21218
  return runHostCommand(argv, CP_RPC_TIMEOUT_MS);
21027
21219
  }
21028
21220
  async function handleDownloadRpc(reg, kind) {
@@ -21639,8 +21831,68 @@ function defaultCapturePane(sessionName) {
21639
21831
  });
21640
21832
  }
21641
21833
 
21642
- // src/supervisor.ts
21834
+ // src/claude-scraper.ts
21643
21835
  var import_node_child_process8 = require("child_process");
21836
+ var DEFAULT_INTERVAL_MS2 = 1500;
21837
+ var DEFAULT_SESSION2 = "claude";
21838
+ var BOTTOM_LINES = 25;
21839
+ var WORKING_GUARD = /\besc to interrupt\b|ctrl-?c to (stop|interrupt)/i;
21840
+ var WAITING_PATTERNS = [
21841
+ /❯\s*\d+\.\s/,
21842
+ // the selector arrow on a numbered option — the Claude select menu
21843
+ /Do you want to proceed\?/i,
21844
+ /Would you like to proceed\?/i,
21845
+ /\b\d+\.\s+Yes\b/,
21846
+ // a "N. Yes" option line
21847
+ /\b(wants to (use|run|access)|Allow .* to (use|run|access))/i
21848
+ // MCP / tool trust dialog
21849
+ ];
21850
+ function startClaudeScraper(opts) {
21851
+ const sessionName = opts.sessionName ?? DEFAULT_SESSION2;
21852
+ const intervalMs = opts.intervalMs ?? DEFAULT_INTERVAL_MS2;
21853
+ const capture = opts.capturePane ?? defaultCapturePane2;
21854
+ let stopped = false;
21855
+ const tick = async () => {
21856
+ if (stopped) return;
21857
+ try {
21858
+ const pane = await capture(sessionName);
21859
+ if (pane === null) return;
21860
+ if (matchWaiting(pane)) opts.reporter.markScreenWaiting();
21861
+ } catch {
21862
+ }
21863
+ };
21864
+ const timer = setInterval(() => void tick(), intervalMs);
21865
+ timer.unref();
21866
+ void tick();
21867
+ return {
21868
+ stop() {
21869
+ stopped = true;
21870
+ clearInterval(timer);
21871
+ }
21872
+ };
21873
+ }
21874
+ function matchWaiting(pane) {
21875
+ const region = pane.split("\n").slice(-BOTTOM_LINES).join("\n");
21876
+ if (WORKING_GUARD.test(region)) return false;
21877
+ return WAITING_PATTERNS.some((re) => re.test(region));
21878
+ }
21879
+ function defaultCapturePane2(sessionName) {
21880
+ return new Promise((resolve2) => {
21881
+ const child = (0, import_node_child_process8.spawn)("tmux", ["capture-pane", "-p", "-t", sessionName], {
21882
+ stdio: ["ignore", "pipe", "ignore"]
21883
+ });
21884
+ let stdout = "";
21885
+ child.stdout.on("data", (b) => stdout += b.toString("utf8"));
21886
+ child.on("error", () => resolve2(null));
21887
+ child.on("close", (code) => {
21888
+ if (code === 0) resolve2(stdout);
21889
+ else resolve2(null);
21890
+ });
21891
+ });
21892
+ }
21893
+
21894
+ // src/supervisor.ts
21895
+ var import_node_child_process9 = require("child_process");
21644
21896
  var import_node_events15 = require("events");
21645
21897
  var import_node_fs7 = require("fs");
21646
21898
  var import_promises18 = require("fs/promises");
@@ -21906,7 +22158,7 @@ var cachedLoginPath;
21906
22158
  function loginShellPath() {
21907
22159
  if (cachedLoginPath !== void 0) return cachedLoginPath;
21908
22160
  try {
21909
- const out = (0, import_node_child_process8.execFileSync)("bash", ["-lc", 'printf %s "$PATH"'], {
22161
+ const out = (0, import_node_child_process9.execFileSync)("bash", ["-lc", 'printf %s "$PATH"'], {
21910
22162
  encoding: "utf8",
21911
22163
  timeout: 5e3
21912
22164
  }).trim();
@@ -21921,7 +22173,7 @@ var ServiceRunner = class extends import_node_events15.EventEmitter {
21921
22173
  super();
21922
22174
  this.spec = spec;
21923
22175
  this.opts = opts;
21924
- this.spawnFn = opts.spawn ?? import_node_child_process8.spawn;
22176
+ this.spawnFn = opts.spawn ?? import_node_child_process9.spawn;
21925
22177
  this.setTimer = opts.setTimer ?? ((fn, ms) => setTimeout(fn, ms));
21926
22178
  this.clearTimer = opts.clearTimer ?? ((h2) => {
21927
22179
  clearTimeout(h2);
@@ -22152,7 +22404,7 @@ var TaskRunner = class extends import_node_events15.EventEmitter {
22152
22404
  super();
22153
22405
  this.spec = spec;
22154
22406
  this.opts = opts;
22155
- this.spawnFn = opts.spawn ?? import_node_child_process8.spawn;
22407
+ this.spawnFn = opts.spawn ?? import_node_child_process9.spawn;
22156
22408
  }
22157
22409
  spec;
22158
22410
  opts;
@@ -22325,6 +22577,20 @@ var Supervisor = class extends import_node_events15.EventEmitter {
22325
22577
  }
22326
22578
  return out;
22327
22579
  }
22580
+ /**
22581
+ * Set of service names that declare a `ready_when` probe of any kind (port or
22582
+ * log_match). A probed service is only "up" once it reaches `ready`; an
22583
+ * unprobed one is up at `running`. The status reporter surfaces this so the
22584
+ * host can tell a warming-up `running` service from a settled one.
22585
+ */
22586
+ probedServices() {
22587
+ const out = /* @__PURE__ */ new Set();
22588
+ for (const u2 of this.units.values()) {
22589
+ if (u2.kind !== "service") continue;
22590
+ if (u2.spec.readyWhen) out.add(u2.name);
22591
+ }
22592
+ return out;
22593
+ }
22328
22594
  /**
22329
22595
  * Map of service name -> `expose:` mapping, for the (at most one) service
22330
22596
  * that declares it. The status reporter surfaces this so the host knows the
@@ -22701,10 +22967,10 @@ var import_promises19 = require("fs/promises");
22701
22967
  var import_node_path7 = require("path");
22702
22968
 
22703
22969
  // src/status-reporter.ts
22704
- var import_node_child_process10 = require("child_process");
22970
+ var import_node_child_process11 = require("child_process");
22705
22971
 
22706
22972
  // src/tmux.ts
22707
- var import_node_child_process9 = require("child_process");
22973
+ var import_node_child_process10 = require("child_process");
22708
22974
  var import_node_os4 = require("os");
22709
22975
  var MAX_TITLE_LEN = 120;
22710
22976
  function sanitizePaneTitle(raw, ctx) {
@@ -22718,7 +22984,7 @@ function sanitizePaneTitle(raw, ctx) {
22718
22984
  }
22719
22985
  function runTool(cmd, args) {
22720
22986
  return new Promise((resolve2) => {
22721
- const child = (0, import_node_child_process9.spawn)(cmd, args, { stdio: ["ignore", "pipe", "pipe"] });
22987
+ const child = (0, import_node_child_process10.spawn)(cmd, args, { stdio: ["ignore", "pipe", "pipe"] });
22722
22988
  let stdout = "";
22723
22989
  let stderr = "";
22724
22990
  child.stdout.on("data", (b) => stdout += b.toString("utf8"));
@@ -22815,6 +23081,23 @@ var StatusReporter = class {
22815
23081
  }
22816
23082
  this.schedulePush();
22817
23083
  }
23084
+ /**
23085
+ * Screen-scraper safety net: promote a *stuck* `working` to `waiting` when the
23086
+ * Claude tmux pane shows a prompt the hooks missed (MCP tool dialogs have no
23087
+ * hook; the `Notification:permission_prompt` hook can fire late or drop).
23088
+ * Deliberately promote-ONLY — it acts solely when the current state is
23089
+ * `working`, so it never clobbers the richer hook-driven `end-plan`/`question`
23090
+ * (sticky) or `idle`/`compacting`/`error`. The next real hook
23091
+ * (`UserPromptSubmit`/`PreToolUse`) overwrites `waiting`→`working` when the
23092
+ * agent resumes, so no demote path is needed. Returns true if it promoted.
23093
+ */
23094
+ markScreenWaiting() {
23095
+ if (this.claudeState !== "working") return false;
23096
+ this.claudeState = "waiting";
23097
+ this.claudeUpdatedAt = (/* @__PURE__ */ new Date()).toISOString();
23098
+ this.schedulePush();
23099
+ return true;
23100
+ }
22818
23101
  setCodexState(state) {
22819
23102
  this.codexState = state;
22820
23103
  this.codexUpdatedAt = (/* @__PURE__ */ new Date()).toISOString();
@@ -22851,11 +23134,13 @@ var StatusReporter = class {
22851
23134
  }
22852
23135
  async snapshot() {
22853
23136
  const probePorts = this.supervisor.serviceProbePorts();
23137
+ const probed = this.supervisor.probedServices();
22854
23138
  const exposes = this.supervisor.serviceExposes();
22855
23139
  const services = this.supervisor.list().map((s) => ({
22856
23140
  name: s.name,
22857
23141
  state: s.state,
22858
23142
  port: probePorts.get(s.name) ?? null,
23143
+ ...probed.has(s.name) ? { probed: true } : {},
22859
23144
  ...exposes.has(s.name) ? { expose: exposes.get(s.name) } : {}
22860
23145
  }));
22861
23146
  const tasks = this.supervisor.listTasks().map((t) => ({ name: t.name, state: t.state }));
@@ -22911,7 +23196,7 @@ async function collectPorts(supervisor) {
22911
23196
  }
22912
23197
  function run(cmd, args) {
22913
23198
  return new Promise((resolve2) => {
22914
- const child = (0, import_node_child_process10.spawn)(cmd, args, { stdio: ["ignore", "pipe", "ignore"] });
23199
+ const child = (0, import_node_child_process11.spawn)(cmd, args, { stdio: ["ignore", "pipe", "ignore"] });
22915
23200
  let stdout = "";
22916
23201
  child.stdout.on("data", (b) => stdout += b.toString("utf8"));
22917
23202
  child.on("error", () => resolve2({ exitCode: 127, stdout }));
@@ -23248,6 +23533,14 @@ var daemonCommand = new Command("daemon").description("Run the agentbox-ctl supe
23248
23533
  } catch (err) {
23249
23534
  const msg = err instanceof Error ? err.message : String(err);
23250
23535
  process.stderr.write(`agentbox-ctl: codex scraper failed to start: ${msg}
23536
+ `);
23537
+ }
23538
+ let claudeScraper = null;
23539
+ try {
23540
+ claudeScraper = startClaudeScraper({ reporter });
23541
+ } catch (err) {
23542
+ const msg = err instanceof Error ? err.message : String(err);
23543
+ process.stderr.write(`agentbox-ctl: claude scraper failed to start: ${msg}
23251
23544
  `);
23252
23545
  }
23253
23546
  const server = await startServer({
@@ -23325,6 +23618,7 @@ var daemonCommand = new Command("daemon").description("Run the agentbox-ctl supe
23325
23618
  process.stdout.write(`agentbox-ctl: ${signal} \u2014 shutting down
23326
23619
  `);
23327
23620
  if (codexScraper) codexScraper.stop();
23621
+ if (claudeScraper) claudeScraper.stop();
23328
23622
  reporter.stop();
23329
23623
  reporter.flush();
23330
23624
  server.close();
@@ -23486,7 +23780,7 @@ var apiCommand = new Command("api").description(
23486
23780
  var ghCommand = new Command("gh").description("GitHub CLI operations routed through the relay (host `gh` runs with host creds; box never sees a token)").addCommand(buildPrCommand("agentbox-ctl gh pr")).addCommand(buildRunCommand("agentbox-ctl gh run")).addCommand(apiCommand).addCommand(repoCommand);
23487
23781
 
23488
23782
  // src/commands/git.ts
23489
- var import_node_child_process11 = require("child_process");
23783
+ var import_node_child_process12 = require("child_process");
23490
23784
  function hostInitiatedOption() {
23491
23785
  return new Option(
23492
23786
  "--host-initiated-token <token>",
@@ -23502,7 +23796,7 @@ function buildParams(opts, extra) {
23502
23796
  }
23503
23797
  function runLocalGit(args, cwd) {
23504
23798
  return new Promise((resolve2) => {
23505
- const child = (0, import_node_child_process11.spawn)("git", args, { cwd, stdio: "inherit" });
23799
+ const child = (0, import_node_child_process12.spawn)("git", args, { cwd, stdio: "inherit" });
23506
23800
  child.on("close", (code) => resolve2(code ?? 1));
23507
23801
  child.on("error", (err) => {
23508
23802
  process.stderr.write(`agentbox-ctl git: ${String(err.message ?? err)}
@@ -23513,7 +23807,7 @@ function runLocalGit(args, cwd) {
23513
23807
  }
23514
23808
  function hasGitIdentity(cwd) {
23515
23809
  return new Promise((resolve2) => {
23516
- const child = (0, import_node_child_process11.spawn)("git", ["config", "user.email"], {
23810
+ const child = (0, import_node_child_process12.spawn)("git", ["config", "user.email"], {
23517
23811
  cwd,
23518
23812
  stdio: ["ignore", "pipe", "ignore"]
23519
23813
  });
@@ -23615,7 +23909,7 @@ var notifyCommand = new Command("notify").description(
23615
23909
  );
23616
23910
 
23617
23911
  // src/commands/open.ts
23618
- var import_node_child_process12 = require("child_process");
23912
+ var import_node_child_process13 = require("child_process");
23619
23913
  var OPEN_TIMEOUT_MS = 3e4;
23620
23914
  function isHttpUrl(value) {
23621
23915
  try {
@@ -23627,7 +23921,7 @@ function isHttpUrl(value) {
23627
23921
  }
23628
23922
  function openInBoxBrowser(url) {
23629
23923
  return new Promise((resolve2) => {
23630
- const child = (0, import_node_child_process12.spawn)("agent-browser", ["open", "--headed", url], { stdio: "inherit" });
23924
+ const child = (0, import_node_child_process13.spawn)("agent-browser", ["open", "--headed", url], { stdio: "inherit" });
23631
23925
  const timer = setTimeout(() => {
23632
23926
  child.kill("SIGTERM");
23633
23927
  process.stderr.write(
@@ -192,6 +192,36 @@ services:
192
192
  factor: 2
193
193
  ```
194
194
 
195
+ ## 6b. Bringing extra host files/folders into the box
196
+
197
+ Two ways to copy host files in (both COPY — never a live mount, so the box can't
198
+ write back to the host):
199
+
200
+ - **`carry:` block** (declarative, in `agentbox.yaml`) — for files/dirs every box
201
+ should get at create time. Each entry is `{ src, dest }` with optional `mode`,
202
+ `user`, `optional`, and `exclude:` (a list of tar globs / bare dir names to drop
203
+ when copying a directory). Heavy regenerable dirs (`.git`, `node_modules`, `bin`,
204
+ `obj`, `packages`, `dist`, `.next`, `target`) are dropped by default; `exclude:`
205
+ is additive. Each carry entry is capped at `box.cpMaxBytes` (default 100 MiB
206
+ after excludes) — the same limit `agentbox cp` enforces.
207
+ - **`agentbox-ctl cp fromHost <hostPath> <boxPath>`** (ad-hoc, from inside the box)
208
+ — for a one-off copy. Prompts the user on the host to approve.
209
+
210
+ **The per-copy size limit (important for large/legacy folders).** A single copy is
211
+ blocked above `box.cpMaxBytes` (default **100 MB**) *after* default excludes, so it
212
+ fails loud instead of silently hanging. When blocked you get a `du`-style tree of
213
+ the biggest remaining folders/subfolders. To get under the limit, EITHER:
214
+
215
+ - **drop what the box can regenerate** (the default excludes already remove
216
+ `node_modules`/`.git`/build output; add more with `--exclude=<glob-or-name>`), OR
217
+ - **copy the heavy folders one at a time** so each copy is under the limit, OR
218
+ - pass `--yes` to copy the whole thing anyway (only when you really need it all).
219
+
220
+ Example: a 2.4 GB legacy folder is mostly `packages/` (NuGet) + `.git`; those are
221
+ excluded by default, and what's left can be split:
222
+ `agentbox-ctl cp fromHost ../legacy/src /workspace/legacy/src` then
223
+ `... cp fromHost ../legacy/Database /workspace/legacy/Database`.
224
+
195
225
  ## 7. Validate before handing off
196
226
 
197
227
  - check with `agentbox-ctl reload` and then `agentbox-ctl status` that everything is running as expected.