@madarco/agentbox 0.9.0 → 0.10.1

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 +102 -0
  2. package/README.md +161 -0
  3. package/dist/{_cloud-attach-ZXBCNWJX.js → _cloud-attach-2DGI6FUA.js} +4 -4
  4. package/dist/{chunk-NCJP5MTN.js → chunk-CDKVD6UO.js} +239 -61
  5. package/dist/chunk-CDKVD6UO.js.map +1 -0
  6. package/dist/{chunk-GU5LW4B5.js → chunk-I7NOGCL4.js} +374 -62
  7. package/dist/chunk-I7NOGCL4.js.map +1 -0
  8. package/dist/{chunk-BXQMIEHC.js → chunk-M2UWJKFA.js} +255 -163
  9. package/dist/chunk-M2UWJKFA.js.map +1 -0
  10. package/dist/{chunk-KL36BRN4.js → chunk-PWUVHPN6.js} +66 -17
  11. package/dist/{chunk-KL36BRN4.js.map → chunk-PWUVHPN6.js.map} +1 -1
  12. package/dist/{dist-CX5CGVEB.js → dist-BD5QJRDC.js} +4 -4
  13. package/dist/{dist-GDHP34ZK.js → dist-BNI5PQYK.js} +16 -4
  14. package/dist/dist-BNI5PQYK.js.map +1 -0
  15. package/dist/{dist-32EZBYG4.js → dist-SBCQVFCE.js} +23 -3
  16. package/dist/{dist-XML54CNB.js → dist-SJHY3HYN.js} +31 -6
  17. package/dist/dist-SJHY3HYN.js.map +1 -0
  18. package/dist/index.js +1787 -528
  19. package/dist/index.js.map +1 -1
  20. package/dist/{prepared-state-CL4CWXQA-H5THETIM.js → prepared-state-MQHD3M5F-O5M4NIN4.js} +2 -2
  21. package/package.json +10 -8
  22. package/runtime/docker/apps/cli/share/agentbox-setup/SKILL.md +10 -9
  23. package/runtime/docker/packages/ctl/dist/bin.cjs +38 -3
  24. package/runtime/hetzner/agentbox-setup-skill.md +10 -9
  25. package/runtime/hetzner/ctl.cjs +38 -3
  26. package/runtime/relay/bin.cjs +41 -3
  27. package/runtime/vercel/agentbox-setup-skill.md +10 -9
  28. package/runtime/vercel/ctl.cjs +38 -3
  29. package/runtime/vercel/custom-system-CLAUDE.md +1 -4
  30. package/runtime/vercel/scripts/provision.sh +40 -0
  31. package/share/agentbox-setup/SKILL.md +10 -9
  32. package/dist/chunk-BXQMIEHC.js.map +0 -1
  33. package/dist/chunk-GU5LW4B5.js.map +0 -1
  34. package/dist/chunk-NCJP5MTN.js.map +0 -1
  35. package/dist/dist-GDHP34ZK.js.map +0 -1
  36. package/dist/dist-XML54CNB.js.map +0 -1
  37. /package/dist/{_cloud-attach-ZXBCNWJX.js.map → _cloud-attach-2DGI6FUA.js.map} +0 -0
  38. /package/dist/{dist-CX5CGVEB.js.map → dist-BD5QJRDC.js.map} +0 -0
  39. /package/dist/{dist-32EZBYG4.js.map → dist-SBCQVFCE.js.map} +0 -0
  40. /package/dist/{prepared-state-CL4CWXQA-H5THETIM.js.map → prepared-state-MQHD3M5F-O5M4NIN4.js.map} +0 -0
package/dist/index.js CHANGED
@@ -28,11 +28,13 @@ import {
28
28
  agentSpecsForCloud,
29
29
  ensureAgentVolumesForCloud,
30
30
  listCloudCheckpoints,
31
+ probeCloudCheckpoint,
31
32
  resolveCloudCheckpoint,
32
33
  seedAgentVolumesIfFresh
33
- } from "./chunk-BXQMIEHC.js";
34
+ } from "./chunk-M2UWJKFA.js";
34
35
  import {
35
36
  ADVANCED_HINT_GROUPS,
37
+ ALERT_BAND_ROWS,
36
38
  NEW_BOX_ID,
37
39
  NEW_BOX_LABEL,
38
40
  buildCloudAttachInnerCommand,
@@ -51,6 +53,7 @@ import {
51
53
  providerForBox,
52
54
  providerForCreate,
53
55
  pushTerminalTitle,
56
+ renderAlertBand,
54
57
  renderFooter,
55
58
  runWrappedAttach,
56
59
  setTerminalTitle,
@@ -58,11 +61,12 @@ import {
58
61
  statusLine,
59
62
  stripTitleGlyph,
60
63
  subscribePrompts
61
- } from "./chunk-GU5LW4B5.js";
64
+ } from "./chunk-I7NOGCL4.js";
62
65
  import {
63
66
  AmbiguousBoxError,
64
67
  BOX_STATUS_EVENT,
65
68
  BoxNotFoundError,
69
+ CODEX_CREDENTIALS_BACKUP_FILE,
66
70
  ClaudeSessionError,
67
71
  CodexSessionError,
68
72
  DEFAULT_CODEX_SESSION,
@@ -72,6 +76,7 @@ import {
72
76
  DEFAULT_SHELL_SESSION,
73
77
  GH_PR_OPS,
74
78
  KEY_REGISTRY,
79
+ OPENCODE_CREDENTIALS_BACKUP_FILE,
75
80
  OPENCODE_FORWARDED_ENV_KEYS,
76
81
  OpencodeSessionError,
77
82
  SHARED_CLAUDE_VOLUME,
@@ -115,6 +120,8 @@ import {
115
120
  ensureOpencodeVolume,
116
121
  ensureRelay,
117
122
  execInBox,
123
+ extractCodexCredentials,
124
+ extractOpencodeCredentials,
118
125
  findProjectRoot,
119
126
  formatDetachNotice,
120
127
  getBoxHostPaths,
@@ -122,6 +129,7 @@ import {
122
129
  getRelayStatus,
123
130
  hashRpcParams,
124
131
  hostBackupHasCredentials,
132
+ hostClaudeBackupExpired,
125
133
  ideProfile,
126
134
  injectPrCreateHead,
127
135
  inspectBox,
@@ -199,7 +207,7 @@ import {
199
207
  waitForTmuxPaneContent,
200
208
  warmUpClaudeCredentials,
201
209
  writeJob
202
- } from "./chunk-NCJP5MTN.js";
210
+ } from "./chunk-CDKVD6UO.js";
203
211
  import {
204
212
  DEFAULT_BOX_IMAGE,
205
213
  STATE_DIR,
@@ -207,15 +215,15 @@ import {
207
215
  imageInfo,
208
216
  readState,
209
217
  resolveBoxRef
210
- } from "./chunk-KL36BRN4.js";
218
+ } from "./chunk-PWUVHPN6.js";
211
219
  import "./chunk-G3H2L3O2.js";
212
220
 
213
221
  // src/version.ts
214
- var AGENTBOX_VERSION = true ? "0.9.0" : "0.0.0-dev";
215
- var AGENTBOX_COMMIT = true ? "78c06a7" : "dev";
222
+ var AGENTBOX_VERSION = true ? "0.10.1" : "0.0.0-dev";
223
+ var AGENTBOX_COMMIT = true ? "1957cb4a" : "dev";
216
224
 
217
225
  // src/index.ts
218
- import { Command as Command45 } from "commander";
226
+ import { Command as Command46 } from "commander";
219
227
 
220
228
  // src/engine-override.ts
221
229
  async function applyEngineOverrideAtStartup() {
@@ -275,7 +283,7 @@ function buildGroupedHelp(program2) {
275
283
  if (cmd) terms.push(term(cmd));
276
284
  }
277
285
  }
278
- const pad3 = Math.max(0, ...terms.map((t) => t.length)) + 2;
286
+ const pad4 = Math.max(0, ...terms.map((t) => t.length)) + 2;
279
287
  const lines = ["Commands:"];
280
288
  for (const g of groups) {
281
289
  const title = g.hint ? `${g.title} (${g.hint})` : g.title;
@@ -283,7 +291,7 @@ function buildGroupedHelp(program2) {
283
291
  for (const name of g.commands) {
284
292
  const cmd = byName.get(name);
285
293
  if (!cmd) continue;
286
- lines.push(` ${term(cmd).padEnd(pad3)}${cmd.description()}`);
294
+ lines.push(` ${term(cmd).padEnd(pad4)}${cmd.description()}`);
287
295
  }
288
296
  }
289
297
  lines.push("", "Run `agentbox <command> --help` for command-specific options.");
@@ -681,6 +689,36 @@ function buildPromptArgs(agentKind, prompt, userArgs) {
681
689
  return resolveAgentLauncher(agentKind).buildArgs(prompt, userArgs);
682
690
  }
683
691
 
692
+ // src/lib/skip-permissions.ts
693
+ var CLAUDE_SKIP_PERMISSIONS_FLAG = "--dangerously-skip-permissions";
694
+ var CODEX_SKIP_PERMISSIONS_FLAG = "--dangerously-bypass-approvals-and-sandbox";
695
+ var CLAUDE_CONFLICTING = /* @__PURE__ */ new Set([CLAUDE_SKIP_PERMISSIONS_FLAG, "--permission-mode"]);
696
+ var CODEX_CONFLICTING = /* @__PURE__ */ new Set([
697
+ CODEX_SKIP_PERMISSIONS_FLAG,
698
+ "--yolo",
699
+ "--full-auto",
700
+ "-a",
701
+ "--ask-for-approval",
702
+ "-s",
703
+ "--sandbox"
704
+ ]);
705
+ function inject(args, flag, conflicting) {
706
+ const hasConflict = args.some((a) => {
707
+ const eq = a.indexOf("=");
708
+ return conflicting.has(eq === -1 ? a : a.slice(0, eq));
709
+ });
710
+ if (hasConflict) return args;
711
+ return [flag, ...args];
712
+ }
713
+ function applyClaudeSkipPermissions(args, cfg) {
714
+ if (!cfg.claude.dangerouslySkipPermissions) return args;
715
+ return inject(args, CLAUDE_SKIP_PERMISSIONS_FLAG, CLAUDE_CONFLICTING);
716
+ }
717
+ function applyCodexSkipPermissions(args, cfg) {
718
+ if (!cfg.codex.dangerouslySkipPermissions) return args;
719
+ return inject(args, CODEX_SKIP_PERMISSIONS_FLAG, CODEX_CONFLICTING);
720
+ }
721
+
684
722
  // src/lib/queue/parse-max-option.ts
685
723
  function parseMaxOption(flag, raw) {
686
724
  if (raw === void 0) return void 0;
@@ -1523,16 +1561,16 @@ function extractCodexUuid(filename) {
1523
1561
  return m ? m[1] : null;
1524
1562
  }
1525
1563
  async function peekCodexCwd(file) {
1526
- let firstLine;
1564
+ let firstLine2;
1527
1565
  try {
1528
1566
  const buf = await readFile3(file, "utf8");
1529
1567
  const nl = buf.indexOf("\n");
1530
- firstLine = nl === -1 ? buf : buf.slice(0, nl);
1568
+ firstLine2 = nl === -1 ? buf : buf.slice(0, nl);
1531
1569
  } catch {
1532
1570
  return null;
1533
1571
  }
1534
1572
  try {
1535
- const parsed = JSON.parse(firstLine);
1573
+ const parsed = JSON.parse(firstLine2);
1536
1574
  if (parsed.type === "session_meta" && typeof parsed.payload?.cwd === "string") {
1537
1575
  return parsed.payload.cwd;
1538
1576
  }
@@ -1812,12 +1850,34 @@ async function maybePromptPortless(args) {
1812
1850
  import { confirm as confirm2, isCancel as isCancel3, log as log8, multiselect } from "@clack/prompts";
1813
1851
  import { basename } from "path";
1814
1852
 
1853
+ // src/provider/cloud-backend.ts
1854
+ async function cloudBackendForProvider(provider) {
1855
+ switch (provider) {
1856
+ case "daytona":
1857
+ return (await import("./dist-BD5QJRDC.js")).daytonaBackend;
1858
+ case "hetzner":
1859
+ return (await import("./dist-BNI5PQYK.js")).hetznerBackend;
1860
+ case "vercel":
1861
+ return (await import("./dist-SJHY3HYN.js")).vercelBackend;
1862
+ default:
1863
+ return null;
1864
+ }
1865
+ }
1866
+
1815
1867
  // src/checkpoint-lookup.ts
1816
1868
  async function checkpointExistsForProvider(provider, projectRoot, ref) {
1817
1869
  if (provider === "docker") {
1818
1870
  return await resolveCheckpoint(projectRoot, ref) !== null;
1819
1871
  }
1820
- return await resolveCloudCheckpoint(projectRoot, provider, ref) !== null;
1872
+ if (await resolveCloudCheckpoint(projectRoot, provider, ref) === null) return false;
1873
+ try {
1874
+ const backend = await cloudBackendForProvider(provider);
1875
+ if (!backend) return true;
1876
+ const { live } = await probeCloudCheckpoint(backend, projectRoot, ref);
1877
+ return live;
1878
+ } catch {
1879
+ return true;
1880
+ }
1821
1881
  }
1822
1882
 
1823
1883
  // src/wizard.ts
@@ -1927,6 +1987,7 @@ function pickCreateOpts(opts) {
1927
1987
  sharedDockerCache: opts.sharedDockerCache,
1928
1988
  portless: opts.portless,
1929
1989
  sessionName: opts.sessionName,
1990
+ dangerouslySkipPermissions: opts.dangerouslySkipPermissions,
1930
1991
  memory: opts.memory,
1931
1992
  cpus: opts.cpus,
1932
1993
  pidsLimit: opts.pidsLimit,
@@ -1969,6 +2030,8 @@ function buildClaudeCliOverrides(opts) {
1969
2030
  if (opts.sharedDockerCache === true) box.dockerCacheShared = true;
1970
2031
  const claude = {};
1971
2032
  if (opts.sessionName !== void 0) claude.sessionName = opts.sessionName;
2033
+ if (opts.dangerouslySkipPermissions !== void 0)
2034
+ claude.dangerouslySkipPermissions = opts.dangerouslySkipPermissions;
1972
2035
  const out = {};
1973
2036
  if (Object.keys(box).length > 0) out.box = box;
1974
2037
  if (Object.keys(claude).length > 0) out.claude = claude;
@@ -2018,6 +2081,34 @@ async function maybeRunClaudeLogin(args) {
2018
2081
  }
2019
2082
  log9.success("Signed in with your Claude subscription \u2014 saved for future boxes.");
2020
2083
  }
2084
+ async function maybeRunCloudClaudeLogin(args) {
2085
+ if (!process.stdin.isTTY || args.yes) return;
2086
+ if (args.authSource === "host-env") return;
2087
+ const hasCreds = await hostBackupHasCredentials();
2088
+ const expired = hasCreds && await hostClaudeBackupExpired();
2089
+ if (hasCreds && !expired) return;
2090
+ const message = expired ? "Your saved Claude login looks expired. Sign in again? (saved and reused by every box)" : "Sign in with your Claude subscription? (saved and reused by every box)";
2091
+ const answer = await confirm3({ message, initialValue: true });
2092
+ if (isCancel4(answer) || !answer) {
2093
+ log9.info("Skipped sign-in \u2014 claude will prompt you to /login inside the box.");
2094
+ return;
2095
+ }
2096
+ const s = spinner3();
2097
+ s.start("preparing sandbox image");
2098
+ await ensureImage(args.image, { onProgress: (line) => s.message(clampSpinnerLine(line)) });
2099
+ s.message("preparing claude config");
2100
+ await ensureClaudeVolume(
2101
+ { volume: SHARED_CLAUDE_VOLUME },
2102
+ { syncFromHost: true, image: args.image, hostWorkspace: args.hostWorkspace }
2103
+ );
2104
+ s.stop("image ready");
2105
+ const exitCode = await runClaudeLoginContainer(args.image, ["--claudeai"]);
2106
+ if (exitCode !== 0) {
2107
+ log9.warn("Claude login did not complete; continuing \u2014 run `agentbox claude login` to retry.");
2108
+ return;
2109
+ }
2110
+ log9.success("Signed in with your Claude subscription \u2014 saved for future boxes.");
2111
+ }
2021
2112
  var claudeCommand = new Command2("claude").description("Create a sandboxed box and launch Claude Code in a detachable tmux session").option("-w, --workspace <path>", "host workspace to mount", process.cwd()).option("-n, --name <name>", "friendly box name (default: <workspace-basename>-<id>)").option("--host-snapshot", "APFS-clone the host workspace into a per-box scratch dir before seeding /workspace (stabilizes the tar-pipe source)").option("--no-host-snapshot", "tar-pipe directly from the live host workspace at create time").option(
2022
2113
  "--snapshot <ref>",
2023
2114
  "start from a project checkpoint (see `agentbox checkpoint`); overrides box.defaultCheckpoint"
@@ -2032,6 +2123,12 @@ var claudeCommand = new Command2("claude").description("Create a sandboxed box a
2032
2123
  "--isolate-claude-config",
2033
2124
  "use a per-box ~/.claude volume instead of the shared agentbox-claude-config"
2034
2125
  ).option("--with-playwright", "also install @playwright/cli@latest globally inside the box").option(
2126
+ "--dangerously-skip-permissions",
2127
+ "launch claude with --dangerously-skip-permissions (auto-accept tool use); on by default since boxes are isolated"
2128
+ ).option(
2129
+ "--no-dangerously-skip-permissions",
2130
+ "do not pass --dangerously-skip-permissions to claude in this box"
2131
+ ).option(
2035
2132
  "--with-env",
2036
2133
  "copy host env/config files (.env*, secrets.toml, agentbox.yaml, ...) into /workspace at create time (gitignore-bypassing)"
2037
2134
  ).option("--no-vnc", "disable the per-box Xvnc + noVNC web client (on by default)").option(
@@ -2159,6 +2256,13 @@ var claudeCommand = new Command2("claude").description("Create a sandboxed box a
2159
2256
  yes: !!opts.yes,
2160
2257
  hostWorkspace: opts.workspace
2161
2258
  });
2259
+ } else {
2260
+ await maybeRunCloudClaudeLogin({
2261
+ image: DEFAULT_BOX_IMAGE,
2262
+ authSource: resolved.source,
2263
+ yes: !!opts.yes,
2264
+ hostWorkspace: opts.workspace
2265
+ });
2162
2266
  }
2163
2267
  const portlessEnabled = isCloud ? void 0 : await maybePromptPortless({
2164
2268
  engine: await detectEngine(),
@@ -2198,6 +2302,7 @@ var claudeCommand = new Command2("claude").description("Create a sandboxed box a
2198
2302
  if (wiz.action === "launch-with-prompt" && wiz.initialPrompt) {
2199
2303
  effectiveClaudeArgs = buildPromptArgs("claude-code", wiz.initialPrompt, claudeArgs);
2200
2304
  }
2305
+ effectiveClaudeArgs = applyClaudeSkipPermissions(effectiveClaudeArgs, cfg.effective);
2201
2306
  let fromBranch;
2202
2307
  let useBranch;
2203
2308
  try {
@@ -2385,6 +2490,12 @@ async function startOrAttachClaude(box, claudeArgs, opts, resumePrepared) {
2385
2490
  const attachIn = resolveAttachInOption(opts);
2386
2491
  const cliOverrides = {};
2387
2492
  if (opts.sessionName) cliOverrides.claude = { sessionName: opts.sessionName };
2493
+ if (opts.dangerouslySkipPermissions !== void 0) {
2494
+ cliOverrides.claude = {
2495
+ ...cliOverrides.claude,
2496
+ dangerouslySkipPermissions: opts.dangerouslySkipPermissions
2497
+ };
2498
+ }
2388
2499
  if (attachIn !== void 0) cliOverrides.attach = { openIn: attachIn };
2389
2500
  const cfg = await loadEffectiveConfig(box.workspacePath, { cliOverrides });
2390
2501
  const sessionName = cfg.effective.claude.sessionName;
@@ -2451,7 +2562,7 @@ async function startOrAttachClaude(box, claudeArgs, opts, resumePrepared) {
2451
2562
  volume: box.claudeConfigVolume ?? SHARED_CLAUDE_VOLUME,
2452
2563
  onProgress: (line) => s.message(clampSpinnerLine(line))
2453
2564
  });
2454
- let effectiveArgs = claudeArgs;
2565
+ let effectiveArgs = applyClaudeSkipPermissions(claudeArgs, cfg.effective);
2455
2566
  if (resumePrepared) {
2456
2567
  s.message("uploading claude session into box");
2457
2568
  try {
@@ -2582,8 +2693,12 @@ var claudeStartCommand = new Command2("start").description(
2582
2693
  return;
2583
2694
  }
2584
2695
  const cfg = await loadEffectiveConfig(box.workspacePath, {
2585
- cliOverrides: attachIn ? { attach: { openIn: attachIn } } : {}
2696
+ cliOverrides: {
2697
+ ...attachIn ? { attach: { openIn: attachIn } } : {},
2698
+ ...opts.dangerouslySkipPermissions !== void 0 ? { claude: { dangerouslySkipPermissions: opts.dangerouslySkipPermissions } } : {}
2699
+ }
2586
2700
  });
2701
+ effectiveClaudeArgs = applyClaudeSkipPermissions(effectiveClaudeArgs, cfg.effective);
2587
2702
  if (resumePrepared) {
2588
2703
  try {
2589
2704
  const provider = await providerForBox(box);
@@ -2655,11 +2770,11 @@ var CLOUD_BACKENDS = ["daytona", "hetzner", "vercel"];
2655
2770
  async function cloudProviderFor(backend) {
2656
2771
  switch (backend) {
2657
2772
  case "daytona":
2658
- return (await import("./dist-CX5CGVEB.js")).daytonaProvider;
2773
+ return (await import("./dist-BD5QJRDC.js")).daytonaProvider;
2659
2774
  case "hetzner":
2660
- return (await import("./dist-GDHP34ZK.js")).hetznerProvider;
2775
+ return (await import("./dist-BNI5PQYK.js")).hetznerProvider;
2661
2776
  case "vercel":
2662
- return (await import("./dist-XML54CNB.js")).vercelProvider;
2777
+ return (await import("./dist-SJHY3HYN.js")).vercelProvider;
2663
2778
  }
2664
2779
  }
2665
2780
  var CHECKPOINT_NOTICE = "Checkpoint in progress \u2014 the box will be unresponsive for a moment";
@@ -2673,7 +2788,7 @@ var createSub = new Command3("create").description("Capture a box state as a pro
2673
2788
  ).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(
2674
2789
  "--replace",
2675
2790
  "if a checkpoint with the same name exists, rm it first (idempotent recapture; safe to retry when the previous run's stdout was lost)"
2676
- ).action(async (idOrName, opts) => {
2791
+ ).option("-y, --yes", 'skip the vercel "box will reboot" confirmation prompt').action(async (idOrName, opts) => {
2677
2792
  try {
2678
2793
  const box = await resolveBoxOrExit(idOrName);
2679
2794
  const providerName = box.provider ?? "docker";
@@ -2912,6 +3027,16 @@ async function runCloudCheckpointCreate(box, opts) {
2912
3027
  if (!provider.checkpoint) {
2913
3028
  throw new Error(`provider '${box.provider ?? "docker"}' doesn't support checkpoints`);
2914
3029
  }
3030
+ if ((box.provider ?? "docker") === "vercel" && !opts.yes && process.stdin.isTTY) {
3031
+ const ok = await confirm4({
3032
+ message: "Create checkpoint? The vercel box will stop and reboot.",
3033
+ initialValue: false
3034
+ });
3035
+ if (isCancel5(ok) || !ok) {
3036
+ log10.info("cancelled");
3037
+ return;
3038
+ }
3039
+ }
2915
3040
  const noticeId = await setRelayNotice(
2916
3041
  box.id,
2917
3042
  "checkpoint",
@@ -3016,10 +3141,10 @@ async function writeAgentboxSshAlias(opts) {
3016
3141
  await fs.writeFile(path, next, { mode: 384 });
3017
3142
  await fs.chmod(path, 384);
3018
3143
  }
3019
- function parseSshTarget(argv) {
3144
+ function parseSshTarget(argv2) {
3020
3145
  let target;
3021
- for (let i = argv.length - 1; i >= 0; i--) {
3022
- const v = argv[i];
3146
+ for (let i = argv2.length - 1; i >= 0; i--) {
3147
+ const v = argv2[i];
3023
3148
  if (!v || v.startsWith("-")) continue;
3024
3149
  const at = v.indexOf("@");
3025
3150
  if (at <= 0) continue;
@@ -3028,9 +3153,9 @@ function parseSshTarget(argv) {
3028
3153
  }
3029
3154
  if (!target) return void 0;
3030
3155
  let identityFile;
3031
- for (let i = 0; i < argv.length - 1; i++) {
3032
- if (argv[i] === "-i") {
3033
- identityFile = argv[i + 1];
3156
+ for (let i = 0; i < argv2.length - 1; i++) {
3157
+ if (argv2[i] === "-i") {
3158
+ identityFile = argv2[i + 1];
3034
3159
  break;
3035
3160
  }
3036
3161
  }
@@ -3258,6 +3383,9 @@ async function fetchServiceNamesDocker(container) {
3258
3383
  }
3259
3384
 
3260
3385
  // src/commands/codex.ts
3386
+ import { access } from "fs/promises";
3387
+ import { homedir as homedir8 } from "os";
3388
+ import { join as join10 } from "path";
3261
3389
  import { confirm as confirm5, intro as intro2, isCancel as isCancel6, log as log12, outro as outro3, spinner as spinner4 } from "@clack/prompts";
3262
3390
  import { Command as Command5 } from "commander";
3263
3391
  function reattachRef2(r) {
@@ -3276,6 +3404,7 @@ function pickCodexCreateOpts(opts) {
3276
3404
  sharedDockerCache: opts.sharedDockerCache,
3277
3405
  portless: opts.portless,
3278
3406
  sessionName: opts.sessionName,
3407
+ dangerouslySkipPermissions: opts.dangerouslySkipPermissions,
3279
3408
  memory: opts.memory,
3280
3409
  cpus: opts.cpus,
3281
3410
  pidsLimit: opts.pidsLimit,
@@ -3310,6 +3439,8 @@ function buildCodexCliOverrides(opts) {
3310
3439
  if (opts.sharedDockerCache === true) box.dockerCacheShared = true;
3311
3440
  const codex = {};
3312
3441
  if (opts.sessionName !== void 0) codex.sessionName = opts.sessionName;
3442
+ if (opts.dangerouslySkipPermissions !== void 0)
3443
+ codex.dangerouslySkipPermissions = opts.dangerouslySkipPermissions;
3313
3444
  const out = {};
3314
3445
  if (Object.keys(box).length > 0) out.box = box;
3315
3446
  if (Object.keys(codex).length > 0) out.codex = codex;
@@ -3348,6 +3479,43 @@ async function maybeRunCodexLogin(args) {
3348
3479
  }
3349
3480
  log12.success("Signed in to Codex \u2014 saved for future boxes.");
3350
3481
  }
3482
+ async function cloudCodexCredAvailable(env = process.env) {
3483
+ if ((env["OPENAI_API_KEY"] ?? "").length > 0) return true;
3484
+ for (const p of [CODEX_CREDENTIALS_BACKUP_FILE, join10(homedir8(), ".codex", "auth.json")]) {
3485
+ try {
3486
+ await access(p);
3487
+ return true;
3488
+ } catch {
3489
+ }
3490
+ }
3491
+ return false;
3492
+ }
3493
+ async function maybeRunCloudCodexLogin(args) {
3494
+ if (!process.stdin.isTTY || args.yes) return;
3495
+ if (await cloudCodexCredAvailable()) return;
3496
+ const answer = await confirm5({
3497
+ message: "Sign in to Codex? (saved and reused by every box)",
3498
+ initialValue: true
3499
+ });
3500
+ if (isCancel6(answer) || !answer) {
3501
+ log12.info("Skipped sign-in \u2014 codex will prompt you to sign in inside the box.");
3502
+ return;
3503
+ }
3504
+ const s = spinner4();
3505
+ s.start("preparing sandbox image");
3506
+ await ensureImage(args.image, { onProgress: (line) => s.message(clampSpinnerLine(line)) });
3507
+ s.message("preparing codex config");
3508
+ await ensureCodexVolume({ volume: SHARED_CODEX_VOLUME }, { syncFromHost: true, image: args.image });
3509
+ s.stop("image ready");
3510
+ const exitCode = await runCodexLoginContainer(args.image, []);
3511
+ if (exitCode !== 0) {
3512
+ log12.warn("Codex login did not complete; continuing \u2014 run `agentbox codex login` to retry.");
3513
+ return;
3514
+ }
3515
+ const { copied } = await extractCodexCredentials(SHARED_CODEX_VOLUME, args.image);
3516
+ if (copied) log12.success("Signed in to Codex \u2014 saved for future boxes.");
3517
+ else log12.warn("Codex login finished but no auth.json was captured \u2014 sign in inside the box if needed.");
3518
+ }
3351
3519
  var codexCommand = new Command5("codex").description("Create a sandboxed box and launch OpenAI Codex in a detachable tmux session").option("-w, --workspace <path>", "host workspace to mount", process.cwd()).option("-n, --name <name>", "friendly box name (default: <workspace-basename>-<id>)").option("--host-snapshot", "APFS-clone the host workspace into a per-box scratch dir before seeding /workspace (stabilizes the tar-pipe source)").option("--no-host-snapshot", "tar-pipe directly from the live host workspace at create time").option(
3352
3520
  "--snapshot <ref>",
3353
3521
  "start from a project checkpoint (see `agentbox checkpoint`); overrides box.defaultCheckpoint"
@@ -3362,6 +3530,12 @@ var codexCommand = new Command5("codex").description("Create a sandboxed box and
3362
3530
  "--isolate-codex-config",
3363
3531
  "use a per-box ~/.codex volume instead of the shared agentbox-codex-config"
3364
3532
  ).option("--with-playwright", "also install @playwright/cli@latest globally inside the box").option(
3533
+ "--dangerously-skip-permissions",
3534
+ "launch codex with --dangerously-bypass-approvals-and-sandbox (never prompt for approval); on by default since boxes are isolated"
3535
+ ).option(
3536
+ "--no-dangerously-skip-permissions",
3537
+ "do not pass --dangerously-bypass-approvals-and-sandbox to codex in this box"
3538
+ ).option(
3365
3539
  "--with-env",
3366
3540
  "copy host env/config files (.env*, secrets.toml, agentbox.yaml, ...) into /workspace at create time (gitignore-bypassing)"
3367
3541
  ).option("--no-vnc", "disable the per-box Xvnc + noVNC web client (on by default)").option(
@@ -3521,6 +3695,7 @@ var codexCommand = new Command5("codex").description("Create a sandboxed box and
3521
3695
  throw err;
3522
3696
  }
3523
3697
  if (isCloud) {
3698
+ await maybeRunCloudCodexLogin({ image: DEFAULT_BOX_IMAGE, yes: !!opts.yes });
3524
3699
  const provider = await providerForCreate({ flag: opts.provider, config: cfg.effective });
3525
3700
  const withPlaywright = cfg.effective.box.withPlaywright || cfg.effective.browser.default !== "agent-browser";
3526
3701
  await cloudAgentCreate({
@@ -3542,7 +3717,7 @@ var codexCommand = new Command5("codex").description("Create a sandboxed box and
3542
3717
  binary: "codex",
3543
3718
  sessionName: cfg.effective.codex.sessionName,
3544
3719
  mode: "codex",
3545
- extraArgs: codexArgs,
3720
+ extraArgs: applyCodexSkipPermissions(codexArgs, cfg.effective),
3546
3721
  verbose: opts.verbose === true,
3547
3722
  openIn: cfg.effective.attach.openIn,
3548
3723
  attach: opts.attach !== false,
@@ -3613,7 +3788,7 @@ var codexCommand = new Command5("codex").description("Create a sandboxed box and
3613
3788
  cmdLog.write(line);
3614
3789
  }
3615
3790
  });
3616
- let effectiveCodexArgs = codexArgs;
3791
+ let effectiveCodexArgs = applyCodexSkipPermissions(codexArgs, cfg.effective);
3617
3792
  if (resumePrepared) {
3618
3793
  s.message("uploading codex session into box");
3619
3794
  cmdLog.write("uploading codex session into box");
@@ -3685,6 +3860,12 @@ async function startOrAttachCodex(box, codexArgs, opts, resumePrepared) {
3685
3860
  const attachIn = resolveAttachInOption(opts);
3686
3861
  const cliOverrides = {};
3687
3862
  if (opts.sessionName) cliOverrides.codex = { sessionName: opts.sessionName };
3863
+ if (opts.dangerouslySkipPermissions !== void 0) {
3864
+ cliOverrides.codex = {
3865
+ ...cliOverrides.codex,
3866
+ dangerouslySkipPermissions: opts.dangerouslySkipPermissions
3867
+ };
3868
+ }
3688
3869
  if (attachIn !== void 0) cliOverrides.attach = { openIn: attachIn };
3689
3870
  const cfg = await loadEffectiveConfig(box.workspacePath, { cliOverrides });
3690
3871
  const sessionName = cfg.effective.codex.sessionName;
@@ -3736,7 +3917,7 @@ async function startOrAttachCodex(box, codexArgs, opts, resumePrepared) {
3736
3917
  await ensureCodexInstalled(box.container, {
3737
3918
  onProgress: (line) => s.message(clampSpinnerLine(line))
3738
3919
  });
3739
- let effectiveArgs = codexArgs;
3920
+ let effectiveArgs = applyCodexSkipPermissions(codexArgs, cfg.effective);
3740
3921
  if (resumePrepared) {
3741
3922
  s.message("uploading codex session into box");
3742
3923
  try {
@@ -3857,8 +4038,12 @@ var codexStartCommand = new Command5("start").description(
3857
4038
  return;
3858
4039
  }
3859
4040
  const cfg = await loadEffectiveConfig(box.workspacePath, {
3860
- cliOverrides: attachIn ? { attach: { openIn: attachIn } } : {}
4041
+ cliOverrides: {
4042
+ ...attachIn ? { attach: { openIn: attachIn } } : {},
4043
+ ...opts.dangerouslySkipPermissions !== void 0 ? { codex: { dangerouslySkipPermissions: opts.dangerouslySkipPermissions } } : {}
4044
+ }
3861
4045
  });
4046
+ effectiveCodexArgs = applyCodexSkipPermissions(effectiveCodexArgs, cfg.effective);
3862
4047
  if (resumePrepared) {
3863
4048
  try {
3864
4049
  const provider = await providerForBox(box);
@@ -3926,6 +4111,9 @@ codexCommand.addCommand(codexStartCommand);
3926
4111
  codexCommand.addCommand(codexLoginCommand);
3927
4112
 
3928
4113
  // src/commands/opencode.ts
4114
+ import { access as access2 } from "fs/promises";
4115
+ import { homedir as homedir9 } from "os";
4116
+ import { join as join11 } from "path";
3929
4117
  import { confirm as confirm6, intro as intro3, isCancel as isCancel7, log as log13, outro as outro4, spinner as spinner5 } from "@clack/prompts";
3930
4118
  import { Command as Command6 } from "commander";
3931
4119
  function reattachRef3(r) {
@@ -4019,6 +4207,48 @@ async function maybeRunOpencodeLogin(args) {
4019
4207
  }
4020
4208
  log13.success("Signed in to OpenCode \u2014 saved for future boxes.");
4021
4209
  }
4210
+ async function cloudOpencodeCredAvailable(env = process.env) {
4211
+ for (const k of OPENCODE_FORWARDED_ENV_KEYS) {
4212
+ if ((env[k] ?? "").length > 0) return true;
4213
+ }
4214
+ for (const p of [OPENCODE_CREDENTIALS_BACKUP_FILE, join11(homedir9(), ".local", "share", "opencode", "auth.json")]) {
4215
+ try {
4216
+ await access2(p);
4217
+ return true;
4218
+ } catch {
4219
+ }
4220
+ }
4221
+ return false;
4222
+ }
4223
+ async function maybeRunCloudOpencodeLogin(args) {
4224
+ if (!process.stdin.isTTY || args.yes) return;
4225
+ if (await cloudOpencodeCredAvailable()) return;
4226
+ const answer = await confirm6({
4227
+ message: "Sign in to OpenCode? (pick a provider; saved and reused by every box)",
4228
+ initialValue: true
4229
+ });
4230
+ if (isCancel7(answer) || !answer) {
4231
+ log13.info("Skipped sign-in \u2014 opencode will prompt you to sign in inside the box.");
4232
+ return;
4233
+ }
4234
+ const s = spinner5();
4235
+ s.start("preparing sandbox image");
4236
+ await ensureImage(args.image, { onProgress: (line) => s.message(clampSpinnerLine(line)) });
4237
+ s.message("preparing opencode config");
4238
+ await ensureOpencodeVolume(
4239
+ { volume: SHARED_OPENCODE_VOLUME },
4240
+ { syncFromHost: true, image: args.image }
4241
+ );
4242
+ s.stop("image ready");
4243
+ const exitCode = await runOpencodeLoginContainer(args.image, []);
4244
+ if (exitCode !== 0) {
4245
+ log13.warn("OpenCode login did not complete; continuing \u2014 run `agentbox opencode login` to retry.");
4246
+ return;
4247
+ }
4248
+ const { copied } = await extractOpencodeCredentials(SHARED_OPENCODE_VOLUME, args.image);
4249
+ if (copied) log13.success("Signed in to OpenCode \u2014 saved for future boxes.");
4250
+ else log13.warn("OpenCode login finished but no auth.json was captured \u2014 sign in inside the box if needed.");
4251
+ }
4022
4252
  var opencodeCommand = new Command6("opencode").description("Create a sandboxed box and launch OpenCode in a detachable tmux session").option("-w, --workspace <path>", "host workspace to mount", process.cwd()).option("-n, --name <name>", "friendly box name (default: <workspace-basename>-<id>)").option("--host-snapshot", "APFS-clone the host workspace into a per-box scratch dir before seeding /workspace (stabilizes the tar-pipe source)").option("--no-host-snapshot", "tar-pipe directly from the live host workspace at create time").option(
4023
4253
  "--snapshot <ref>",
4024
4254
  "start from a project checkpoint (see `agentbox checkpoint`); overrides box.defaultCheckpoint"
@@ -4177,6 +4407,7 @@ var opencodeCommand = new Command6("opencode").description("Create a sandboxed b
4177
4407
  throw err;
4178
4408
  }
4179
4409
  if (isCloud) {
4410
+ await maybeRunCloudOpencodeLogin({ image: DEFAULT_BOX_IMAGE, yes: !!opts.yes });
4180
4411
  const provider = await providerForCreate({ flag: opts.provider, config: cfg.effective });
4181
4412
  const withPlaywright = cfg.effective.box.withPlaywright || cfg.effective.browser.default !== "agent-browser";
4182
4413
  await cloudAgentCreate({
@@ -4908,7 +5139,10 @@ async function attachShell(record) {
4908
5139
  var createCommand = new Command9("create").description("Create and start a new agent box (Docker container with /workspace seeded via in-container git worktree)").option("-w, --workspace <path>", "host workspace to mount", process.cwd()).option("-n, --name <name>", "friendly box name (default: <workspace-basename>-<id>)").option("--provider <name>", "sandbox backend: 'docker' (default) or 'daytona' (cloud)").option("--host-snapshot", "APFS-clone the host workspace into a per-box scratch dir before seeding /workspace (stabilizes the tar-pipe source)").option("--no-host-snapshot", "bind the live workspace directly (host edits leak into reads)").option(
4909
5140
  "--snapshot <ref>",
4910
5141
  "start from a project checkpoint (see `agentbox checkpoint`); overrides box.defaultCheckpoint"
4911
- ).option("--image <ref>", "override the box image", void 0).option("--attach", "drop into a shell inside the box after it is ready").option("--with-playwright", "also install @playwright/cli@latest globally inside the box").option(
5142
+ ).option("--image <ref>", "override the box image", void 0).option(
5143
+ "--build",
5144
+ "build the docker base image locally instead of pulling the prebuilt one from the registry"
5145
+ ).option("--attach", "drop into a shell inside the box after it is ready").option("--with-playwright", "also install @playwright/cli@latest globally inside the box").option(
4912
5146
  "--with-env",
4913
5147
  "copy host env/config files (.env*, secrets.toml, agentbox.yaml, ...) into /workspace at create time (gitignore-bypassing)"
4914
5148
  ).option("--no-vnc", "disable the per-box Xvnc + noVNC web client (on by default)").option(
@@ -5046,6 +5280,8 @@ var createCommand = new Command9("create").description("Create and start a new a
5046
5280
  name: opts.name,
5047
5281
  checkpointRef,
5048
5282
  image: cfg.effective.box.image,
5283
+ allowPull: opts.build ? false : void 0,
5284
+ imageRegistry: cfg.effective.box.imageRegistry,
5049
5285
  withPlaywright,
5050
5286
  withEnv: cfg.effective.box.withEnv,
5051
5287
  envFilesToImport: wiz.envFilesToImport,
@@ -5126,7 +5362,7 @@ var createCommand = new Command9("create").description("Create and start a new a
5126
5362
  }
5127
5363
  outro5("done");
5128
5364
  if (attachClaudeAfter) {
5129
- const { cloudAgentAttach: cloudAgentAttach2 } = await import("./_cloud-attach-ZXBCNWJX.js");
5365
+ const { cloudAgentAttach: cloudAgentAttach2 } = await import("./_cloud-attach-2DGI6FUA.js");
5130
5366
  await cloudAgentAttach2({
5131
5367
  box: result.record,
5132
5368
  binary: "claude",
@@ -5169,13 +5405,16 @@ import { Command as Command10 } from "commander";
5169
5405
  var SIDEBAR_WIDTH = 33;
5170
5406
  var MIN_RIGHT_W = 20;
5171
5407
  var MIN_RIGHT_H = 4;
5172
- function computeLayout(cols, rows) {
5408
+ function computeLayout(cols, rows, requestedAlertH = 0) {
5173
5409
  const sidebarW = Math.min(SIDEBAR_WIDTH, Math.max(0, cols - MIN_RIGHT_W - 1));
5174
5410
  const sepX = sidebarW;
5175
5411
  const rightX = sidebarW + 1;
5176
5412
  const rightW = Math.max(0, cols - rightX);
5177
5413
  const statusY = rows - 1;
5178
- const paneH = Math.max(0, statusY);
5414
+ const desired = Math.max(0, requestedAlertH);
5415
+ const alertH = statusY - desired >= MIN_RIGHT_H ? desired : 0;
5416
+ const paneH = Math.max(0, statusY - alertH);
5417
+ const alertY = statusY - alertH;
5179
5418
  return {
5180
5419
  cols,
5181
5420
  rows,
@@ -5183,6 +5422,8 @@ function computeLayout(cols, rows) {
5183
5422
  sepX,
5184
5423
  right: { x: rightX, y: 0, w: rightW, h: paneH },
5185
5424
  statusY,
5425
+ alertH,
5426
+ alertY,
5186
5427
  tooSmall: rightW < MIN_RIGHT_W || paneH < MIN_RIGHT_H
5187
5428
  };
5188
5429
  }
@@ -5478,6 +5719,21 @@ var BLANK = {
5478
5719
  strike: false
5479
5720
  };
5480
5721
  var PtySession = class {
5722
+ /** Box this session attaches to. Identifies it in the compositor's pool. */
5723
+ boxId;
5724
+ /** When true, the compositor keeps this session alive (in its pool) across
5725
+ * box switches instead of disposing it — see {@link Compositor.liveSessions}. */
5726
+ keepAlive;
5727
+ /** Agent/shell mode of this attach. The compositor restores `activeMode`
5728
+ * (drives the footer) from this when re-showing a pooled session. */
5729
+ mode;
5730
+ /**
5731
+ * Whether this session is the one currently shown in the right pane. A
5732
+ * kept-alive hidden session (`active === false`) still consumes PTY output
5733
+ * to keep its headless buffer current, but must NOT trigger right-pane
5734
+ * repaints. The compositor flips this on show/hide.
5735
+ */
5736
+ active = true;
5481
5737
  term;
5482
5738
  pty;
5483
5739
  cleanup;
@@ -5485,7 +5741,10 @@ var PtySession = class {
5485
5741
  // Reused per cell read — valid only until the next cell() call (the renderer
5486
5742
  // consumes it synchronously within composeRow).
5487
5743
  out = { ...BLANK };
5488
- constructor(spawn5, TerminalClass, command, args, cols, rows, onRenderable, onExit, cleanup) {
5744
+ constructor(spawn5, TerminalClass, boxId, keepAlive, mode, command, args, cols, rows, onRenderable, onExit, cleanup) {
5745
+ this.boxId = boxId;
5746
+ this.keepAlive = keepAlive;
5747
+ this.mode = mode;
5489
5748
  this.term = new TerminalClass({
5490
5749
  cols,
5491
5750
  rows,
@@ -5501,13 +5760,15 @@ var PtySession = class {
5501
5760
  env: process.env
5502
5761
  });
5503
5762
  this.pty.onData((d) => {
5504
- this.term.write(d, () => onRenderable());
5763
+ this.term.write(d, () => {
5764
+ if (this.active) onRenderable();
5765
+ });
5505
5766
  });
5506
5767
  this.term.onData((d) => {
5507
5768
  if (!this.disposed) this.pty.write(d);
5508
5769
  });
5509
5770
  this.pty.onExit(() => {
5510
- if (!this.disposed) onExit();
5771
+ if (!this.disposed) onExit(this.boxId);
5511
5772
  });
5512
5773
  }
5513
5774
  write(bytes) {
@@ -5569,6 +5830,7 @@ var SB_AWAITING = SB_BG + "\x1B[38;5;51m\x1B[1m";
5569
5830
  var SGR_RESET = "\x1B[0m";
5570
5831
  var POLL_MS = 1e3;
5571
5832
  var FRAME_MS = 16;
5833
+ var KEEP_ALIVE_MAX = 6;
5572
5834
  var RESIZE_DEBOUNCE_MS = 120;
5573
5835
  var LEADER_LINGER_MS = 1500;
5574
5836
  var NOTICE_SPINNER_MS = 120;
@@ -5581,7 +5843,7 @@ var Compositor = class {
5581
5843
  constructor(deps, initialId) {
5582
5844
  this.deps = deps;
5583
5845
  this.selectedId = initialId;
5584
- this.layout = computeLayout(this.out.columns ?? 100, this.out.rows ?? 30);
5846
+ this.layout = computeLayout(this.out.columns ?? 100, this.out.rows ?? 30, 0);
5585
5847
  this.parser = new InputParser({
5586
5848
  onEvent: (e) => {
5587
5849
  if (e.type === "leader") {
@@ -5642,7 +5904,17 @@ var Compositor = class {
5642
5904
  inp = process.stdin;
5643
5905
  boxes = [];
5644
5906
  selectedId;
5907
+ /** The session currently shown in the right pane (may also be in
5908
+ * {@link liveSessions} when it's keep-alive). */
5645
5909
  session = null;
5910
+ /**
5911
+ * Pool of kept-alive sessions, keyed by box id, for providers whose attach
5912
+ * is expensive to reconnect (vercel). Hidden entries keep their PTY + headless
5913
+ * buffer alive so switching back is instant — no probe, no re-spawn. Map
5914
+ * insertion order doubles as LRU recency (re-set on activate); bounded by
5915
+ * {@link KEEP_ALIVE_MAX}. Reconnect-cheap providers never enter this map.
5916
+ */
5917
+ liveSessions = /* @__PURE__ */ new Map();
5646
5918
  placeholder = null;
5647
5919
  menu = null;
5648
5920
  lifecycleMenu = null;
@@ -5734,6 +6006,22 @@ var Compositor = class {
5734
6006
  } catch {
5735
6007
  }
5736
6008
  this.syncPromptSubscriptions();
6009
+ this.reconcileLiveSessions();
6010
+ }
6011
+ /**
6012
+ * Drop pooled (hidden) sessions whose box is gone or no longer running, so a
6013
+ * paused/stopped/destroyed box can't keep its remote attach process alive.
6014
+ * The *active* session is intentionally skipped — it's torn down on the next
6015
+ * switch/re-resolve via {@link deactivateActive} (which checks box state),
6016
+ * so evicting it here would blank the pane out from under the poll's
6017
+ * re-resolve logic.
6018
+ */
6019
+ reconcileLiveSessions() {
6020
+ for (const boxId of [...this.liveSessions.keys()]) {
6021
+ if (this.session && this.session.boxId === boxId) continue;
6022
+ const running = this.boxes.some((b) => b.id === boxId && b.state === "running");
6023
+ if (!running) this.evictSession(boxId);
6024
+ }
5737
6025
  }
5738
6026
  /**
5739
6027
  * Diff the current box list against {@link promptStreams}: subscribe to
@@ -5758,7 +6046,7 @@ var Compositor = class {
5758
6046
  let changed = this.activePrompts.delete(boxId);
5759
6047
  if (this.activeNotices.delete(boxId)) changed = true;
5760
6048
  if (this.activeNotices.size === 0) this.stopNoticeSpinner();
5761
- if (changed) this.drawChrome();
6049
+ if (changed) this.redrawForAlert();
5762
6050
  }
5763
6051
  }
5764
6052
  for (const boxId of wanted) {
@@ -5769,21 +6057,21 @@ var Compositor = class {
5769
6057
  onPrompt: (ev) => {
5770
6058
  if (this.tornDown) return;
5771
6059
  this.activePrompts.set(boxId, ev);
5772
- this.drawChrome();
6060
+ this.redrawForAlert();
5773
6061
  },
5774
6062
  onResolved: (id) => {
5775
6063
  if (this.tornDown) return;
5776
6064
  const current = this.activePrompts.get(boxId);
5777
6065
  if (current && current.id === id) {
5778
6066
  this.activePrompts.delete(boxId);
5779
- this.drawChrome();
6067
+ this.redrawForAlert();
5780
6068
  }
5781
6069
  },
5782
6070
  onNotice: (ev) => {
5783
6071
  if (this.tornDown) return;
5784
6072
  this.activeNotices.set(boxId, ev);
5785
6073
  this.startNoticeSpinner();
5786
- this.drawChrome();
6074
+ this.redrawForAlert();
5787
6075
  },
5788
6076
  onNoticeCleared: (id) => {
5789
6077
  if (this.tornDown) return;
@@ -5791,7 +6079,7 @@ var Compositor = class {
5791
6079
  if (current && current.id === id) {
5792
6080
  this.activeNotices.delete(boxId);
5793
6081
  if (this.activeNotices.size === 0) this.stopNoticeSpinner();
5794
- this.drawChrome();
6082
+ this.redrawForAlert();
5795
6083
  }
5796
6084
  },
5797
6085
  onError: () => {
@@ -5818,9 +6106,11 @@ var Compositor = class {
5818
6106
  return this.boxes.find((b) => b.id === this.selectedId);
5819
6107
  }
5820
6108
  async poll() {
5821
- const before = JSON.stringify(
6109
+ const stateKey = () => JSON.stringify(
5822
6110
  this.boxes.map((b) => [b.id, b.state, b.activity, b.sessionTitle])
5823
6111
  );
6112
+ const before = stateKey();
6113
+ const beforeAlertH = this.alertHeight();
5824
6114
  await this.refreshBoxes();
5825
6115
  if (this.busy) {
5826
6116
  } else if (!this.boxes.some((b) => b.id === this.selectedId) && this.boxes[0]) {
@@ -5832,19 +6122,99 @@ var Compositor = class {
5832
6122
  const reresolve = this.session && !running || this.placeholder && running || this.menu && !running || this.lifecycleMenu != null && box?.state !== this.lifecycleMenu.state;
5833
6123
  if (reresolve) await this.spawnActive();
5834
6124
  }
5835
- if (JSON.stringify(
5836
- this.boxes.map((b) => [b.id, b.state, b.activity, b.sessionTitle])
5837
- ) !== before) {
6125
+ const stateChanged = stateKey() !== before;
6126
+ const alertChanged = this.alertHeight() !== beforeAlertH;
6127
+ if (alertChanged) {
6128
+ this.relayout();
6129
+ } else if (stateChanged) {
5838
6130
  this.drawChrome();
5839
6131
  }
5840
6132
  }
5841
- disposeSession() {
5842
- if (!this.session) return;
5843
- this.session.dispose();
6133
+ /**
6134
+ * Detach the active session from view. Keep-alive sessions (those in
6135
+ * {@link liveSessions}) stay running in the background; everything else is
6136
+ * disposed — matching the pre-pool dispose-on-switch behaviour for docker /
6137
+ * hetzner / daytona.
6138
+ */
6139
+ deactivateActive() {
6140
+ const s = this.session;
6141
+ if (!s) return;
6142
+ s.active = false;
6143
+ const pooled = this.liveSessions.get(s.boxId) === s;
6144
+ const boxRunning = this.boxes.some((b) => b.id === s.boxId && b.state === "running");
6145
+ if (!pooled || !boxRunning) {
6146
+ if (pooled) this.liveSessions.delete(s.boxId);
6147
+ s.dispose();
6148
+ }
6149
+ this.session = null;
6150
+ }
6151
+ /** Dispose and drop the pooled session for `boxId` (box gone / stopped /
6152
+ * its attach died). Clears the active reference if it was the shown one. */
6153
+ evictSession(boxId) {
6154
+ const pooled = this.liveSessions.get(boxId);
6155
+ if (pooled) {
6156
+ this.liveSessions.delete(boxId);
6157
+ pooled.dispose();
6158
+ }
6159
+ if (this.session && this.session.boxId === boxId) {
6160
+ if (this.session !== pooled) this.session.dispose();
6161
+ this.session = null;
6162
+ }
6163
+ }
6164
+ /** Bound the pool: evict least-recently-used pooled sessions (Map insertion
6165
+ * order) until at most {@link KEEP_ALIVE_MAX} remain, never the active one. */
6166
+ evictLruIfNeeded() {
6167
+ for (const boxId of this.liveSessions.keys()) {
6168
+ if (this.liveSessions.size <= KEEP_ALIVE_MAX) break;
6169
+ if (this.session && this.session.boxId === boxId) continue;
6170
+ this.evictSession(boxId);
6171
+ }
6172
+ }
6173
+ /** Dispose the active session plus every pooled one (teardown). */
6174
+ disposeAllSessions() {
6175
+ const active = this.session;
6176
+ if (active && this.liveSessions.get(active.boxId) !== active) active.dispose();
5844
6177
  this.session = null;
6178
+ for (const s of this.liveSessions.values()) s.dispose();
6179
+ this.liveSessions.clear();
6180
+ }
6181
+ /**
6182
+ * Show a pooled session in the right pane — the fast switch-back path: no
6183
+ * probe, no re-spawn, instant repaint from its already-current headless
6184
+ * buffer. Re-marks it most-recently-used and re-applies the current layout
6185
+ * size (it may have changed while hidden).
6186
+ */
6187
+ activateSession(sess) {
6188
+ this.deactivateActive();
6189
+ this.placeholder = null;
6190
+ this.menu = null;
6191
+ this.lifecycleMenu = null;
6192
+ this.createMenu = null;
6193
+ this.pendingConfirm = null;
6194
+ this.session = sess;
6195
+ sess.active = true;
6196
+ this.activeMode = sess.mode;
6197
+ sess.resize(Math.max(1, this.layout.right.w), Math.max(1, this.layout.right.h));
6198
+ this.liveSessions.delete(sess.boxId);
6199
+ this.liveSessions.set(sess.boxId, sess);
6200
+ this.prevRows = null;
6201
+ if (!this.syncAlertLayout()) this.drawChrome();
6202
+ this.scheduleRender();
6203
+ }
6204
+ /**
6205
+ * Show the selected box. If a kept-alive session is pooled for it, re-show it
6206
+ * synchronously (instant). Otherwise fall through to the async resolve+spawn.
6207
+ */
6208
+ showSelected() {
6209
+ const cached = this.liveSessions.get(this.selectedId);
6210
+ if (cached) {
6211
+ this.activateSession(cached);
6212
+ return;
6213
+ }
6214
+ void this.spawnActive();
5845
6215
  }
5846
6216
  async spawnActive() {
5847
- this.disposeSession();
6217
+ this.deactivateActive();
5848
6218
  this.placeholder = null;
5849
6219
  this.menu = null;
5850
6220
  this.lifecycleMenu = null;
@@ -5858,25 +6228,37 @@ var Compositor = class {
5858
6228
  }
5859
6229
  /** Turn a resolved/started target into the right-pane state. */
5860
6230
  applyTarget(target) {
5861
- this.disposeSession();
6231
+ this.deactivateActive();
5862
6232
  this.placeholder = null;
5863
6233
  this.menu = null;
5864
6234
  this.lifecycleMenu = null;
5865
6235
  this.createMenu = null;
5866
6236
  this.pendingConfirm = null;
5867
6237
  if (target.kind === "attach") {
5868
- this.activeMode = target.mode ?? "claude";
6238
+ const boxId = this.selectedId;
6239
+ const mode = target.mode ?? "claude";
6240
+ const keepAlive = target.keepAlive ?? false;
6241
+ this.activeMode = mode;
5869
6242
  this.session = new PtySession(
5870
6243
  this.deps.ptySpawn,
5871
6244
  this.deps.termCtor,
6245
+ boxId,
6246
+ keepAlive,
6247
+ mode,
5872
6248
  target.command,
5873
6249
  target.args,
5874
6250
  Math.max(1, this.layout.right.w),
5875
6251
  Math.max(1, this.layout.right.h),
5876
6252
  () => this.scheduleRender(),
5877
- () => this.onSessionExit(),
6253
+ (id) => this.onSessionExit(id),
5878
6254
  target.cleanup
5879
6255
  );
6256
+ if (keepAlive) {
6257
+ const prev = this.liveSessions.get(boxId);
6258
+ if (prev && prev !== this.session) prev.dispose();
6259
+ this.liveSessions.set(boxId, this.session);
6260
+ this.evictLruIfNeeded();
6261
+ }
5880
6262
  } else if (target.kind === "menu") {
5881
6263
  this.menu = { boxName: this.selectedBox()?.name ?? this.selectedId };
5882
6264
  } else if (target.kind === "lifecycle-menu") {
@@ -5891,7 +6273,7 @@ var Compositor = class {
5891
6273
  this.placeholder = target.lines;
5892
6274
  }
5893
6275
  this.prevRows = null;
5894
- this.drawChrome();
6276
+ if (!this.syncAlertLayout()) this.drawChrome();
5895
6277
  this.scheduleRender();
5896
6278
  }
5897
6279
  handleMenuKey(bytes) {
@@ -6224,8 +6606,9 @@ var Compositor = class {
6224
6606
  }, 2500);
6225
6607
  this.drawChrome();
6226
6608
  }
6227
- onSessionExit() {
6228
- this.disposeSession();
6609
+ onSessionExit(boxId) {
6610
+ this.evictSession(boxId);
6611
+ if (boxId !== this.selectedId) return;
6229
6612
  this.placeholder = ["", " session ended \u2014 Ctrl-a \u2191/\u2193 to switch boxes"];
6230
6613
  this.prevRows = null;
6231
6614
  this.scheduleRender();
@@ -6241,7 +6624,7 @@ var Compositor = class {
6241
6624
  const next = dir === "prev" ? (i - 1 + n) % n : (i + 1) % n;
6242
6625
  this.selectedId = this.boxes[next].id;
6243
6626
  this.drawChrome();
6244
- void this.spawnActive();
6627
+ this.showSelected();
6245
6628
  }
6246
6629
  /** Blank the right pane and drop the diff cache (next paint is full). */
6247
6630
  clearRightPane() {
@@ -6341,8 +6724,36 @@ var Compositor = class {
6341
6724
  }
6342
6725
  for (let y = 0; y < sidebar.h; y++)
6343
6726
  s += cursorTo2(sepX, y) + SB_HEADER + (y === 0 ? "\u256E" : "\u2502") + SGR_RESET;
6344
- let status;
6345
6727
  const activePromptForSelected = this.activePrompts.get(this.selectedId);
6728
+ const activeNoticeForSelected = this.activeNotices.get(this.selectedId);
6729
+ if (this.layout.alertH > 0) {
6730
+ const bandRows = this.layout.alertH;
6731
+ let bandLines = null;
6732
+ if (activePromptForSelected) {
6733
+ bandLines = renderAlertBand(
6734
+ { kind: "prompt", prompt: activePromptForSelected },
6735
+ this.layout.cols,
6736
+ bandRows
6737
+ );
6738
+ } else if (activeNoticeForSelected) {
6739
+ bandLines = renderAlertBand(
6740
+ { kind: "notice", message: activeNoticeForSelected.message, frame: this.noticeFrame },
6741
+ this.layout.cols,
6742
+ bandRows
6743
+ );
6744
+ } else {
6745
+ const q = this.selectedBox()?.claudeQuestion;
6746
+ if (q) {
6747
+ bandLines = renderAlertBand({ kind: "question", question: q }, this.layout.cols, bandRows);
6748
+ }
6749
+ }
6750
+ if (bandLines) {
6751
+ for (let i = 0; i < bandLines.length; i++) {
6752
+ s += cursorTo2(0, this.layout.alertY + i) + bandLines[i] + SGR_RESET;
6753
+ }
6754
+ }
6755
+ }
6756
+ let status;
6346
6757
  if (this.pendingConfirm) {
6347
6758
  const w = this.layout.cols;
6348
6759
  const txt = ` Destroy ${this.pendingConfirm.name}? y = confirm \xB7 any other key = cancel `.slice(0, w).padEnd(w);
@@ -6351,15 +6762,14 @@ var Compositor = class {
6351
6762
  const w = this.layout.cols;
6352
6763
  const txt = ` ${this.flashMsg} `.slice(0, w).padEnd(w);
6353
6764
  status = `\x1B[7m${txt}\x1B[0m`;
6354
- } else if (activePromptForSelected) {
6765
+ } else if (this.layout.alertH === 0 && activePromptForSelected) {
6355
6766
  status = renderFooter(
6356
6767
  { kind: "prompt", prompt: activePromptForSelected },
6357
6768
  this.layout.cols
6358
6769
  );
6359
- } else if (this.activeNotices.has(this.selectedId)) {
6360
- const notice = this.activeNotices.get(this.selectedId);
6770
+ } else if (this.layout.alertH === 0 && activeNoticeForSelected) {
6361
6771
  status = renderFooter(
6362
- { kind: "notice", message: notice.message, frame: this.noticeFrame },
6772
+ { kind: "notice", message: activeNoticeForSelected.message, frame: this.noticeFrame },
6363
6773
  this.layout.cols
6364
6774
  );
6365
6775
  } else {
@@ -6382,17 +6792,62 @@ var Compositor = class {
6382
6792
  if (this.resizeTimer) clearTimeout(this.resizeTimer);
6383
6793
  this.resizeTimer = setTimeout(() => {
6384
6794
  this.resizeTimer = null;
6385
- this.layout = computeLayout(this.out.columns ?? 100, this.out.rows ?? 30);
6386
- this.prevRows = null;
6387
- const r = this.layout.right;
6388
- if (this.session && !this.layout.tooSmall) {
6389
- this.session.resize(Math.max(1, r.w), Math.max(1, r.h));
6390
- }
6391
- this.out.write(SYNC_BEGIN + "\x1B[2J" + SYNC_END);
6392
- this.drawChrome();
6393
- this.render();
6795
+ this.relayout();
6394
6796
  }, RESIZE_DEBOUNCE_MS);
6395
6797
  }
6798
+ /**
6799
+ * Requested band height for the currently-selected box. Returns
6800
+ * `ALERT_BAND_ROWS` when the box has an active relay prompt, an active
6801
+ * notice (checkpoint), or claude is in the `question` state with a payload;
6802
+ * 0 otherwise. The layout silently drops the band to 0 if reserving it
6803
+ * would push the right pane below MIN_RIGHT_H.
6804
+ */
6805
+ alertHeight() {
6806
+ const id = this.selectedId;
6807
+ if (this.activePrompts.has(id)) return ALERT_BAND_ROWS;
6808
+ if (this.activeNotices.has(id)) return ALERT_BAND_ROWS;
6809
+ const box = this.selectedBox();
6810
+ if (box?.claudeQuestion) return ALERT_BAND_ROWS;
6811
+ return 0;
6812
+ }
6813
+ /**
6814
+ * Recompute the layout against the current alert height, resize the inner
6815
+ * session, and repaint. Called from `scheduleResize` (terminal resize) and
6816
+ * from {@link syncAlertLayout} when the selected box's alert state flips.
6817
+ */
6818
+ relayout() {
6819
+ this.layout = computeLayout(
6820
+ this.out.columns ?? 100,
6821
+ this.out.rows ?? 30,
6822
+ this.alertHeight()
6823
+ );
6824
+ this.prevRows = null;
6825
+ const r = this.layout.right;
6826
+ if (this.session && !this.layout.tooSmall) {
6827
+ this.session.resize(Math.max(1, r.w), Math.max(1, r.h));
6828
+ }
6829
+ this.out.write(SYNC_BEGIN + "\x1B[2J" + SYNC_END);
6830
+ this.drawChrome();
6831
+ this.render();
6832
+ }
6833
+ /**
6834
+ * If the selected box's alert state implies a different band height than
6835
+ * the current layout, run a full {@link relayout}; otherwise return false
6836
+ * so the caller can take the lighter `drawChrome()` path. Used by all
6837
+ * alert-state transitions (SSE handlers, poll, selection change).
6838
+ */
6839
+ syncAlertLayout() {
6840
+ if (this.alertHeight() !== this.layout.alertH) {
6841
+ this.relayout();
6842
+ return true;
6843
+ }
6844
+ return false;
6845
+ }
6846
+ /** Common path for alert-state transitions: relayout when the band's
6847
+ * visibility changes, drawChrome only when it doesn't. */
6848
+ redrawForAlert() {
6849
+ if (!this.syncAlertLayout()) this.drawChrome();
6850
+ }
6396
6851
  teardown() {
6397
6852
  if (this.tornDown) return;
6398
6853
  this.tornDown = true;
@@ -6407,7 +6862,7 @@ var Compositor = class {
6407
6862
  this.activePrompts.clear();
6408
6863
  this.activeNotices.clear();
6409
6864
  this.parser.dispose();
6410
- this.disposeSession();
6865
+ this.disposeAllSessions();
6411
6866
  this.inp.off("data", this.onData);
6412
6867
  this.out.off("resize", this.onResize);
6413
6868
  if (this.inp.isTTY) this.inp.setRawMode(false);
@@ -6419,6 +6874,9 @@ var Compositor = class {
6419
6874
  };
6420
6875
 
6421
6876
  // src/commands/dashboard.ts
6877
+ function providerSupportsKeepAlive(provider) {
6878
+ return provider === "vercel";
6879
+ }
6422
6880
  function sortBoxes(boxes) {
6423
6881
  return [...boxes].sort((a, b) => {
6424
6882
  const ap = a.projectRoot ?? "";
@@ -6455,7 +6913,10 @@ function toSidebar(b) {
6455
6913
  activity: agent.activity,
6456
6914
  sessionTitle: agent.sessionTitle,
6457
6915
  index: b.projectIndex,
6458
- project: b.projectRoot
6916
+ project: b.projectRoot,
6917
+ // Only claude reports an AskUserQuestion payload; gated on claudeActivity
6918
+ // === 'question' upstream (lifecycle.listBoxes) so it's undefined otherwise.
6919
+ claudeQuestion: b.claudeQuestion
6459
6920
  };
6460
6921
  }
6461
6922
  var dashboardCommand = new Command10("dashboard").description("Box list + the selected box live Agent session").argument("[box]", "initial box (default: first running box; -p restricts to the cwd project)").option("-p, --project", "only this project's boxes (default: all boxes globally)").action(async (idOrName, opts) => {
@@ -6578,7 +7039,8 @@ var dashboardCommand = new Command10("dashboard").description("Box list + the se
6578
7039
  command: spec.argv[0],
6579
7040
  args: spec.argv.slice(1),
6580
7041
  ...spec.cleanup ? { cleanup: spec.cleanup } : {},
6581
- mode: which
7042
+ mode: which,
7043
+ ...providerSupportsKeepAlive(record.provider) ? { keepAlive: true } : {}
6582
7044
  };
6583
7045
  };
6584
7046
  const isCloudBox = (box) => (box.provider ?? "docker") !== "docker";
@@ -6612,7 +7074,12 @@ var dashboardCommand = new Command10("dashboard").description("Box list + the se
6612
7074
  { volume: claudeVolume },
6613
7075
  { image: box.image, isolate: claudeVolume !== SHARED_CLAUDE_VOLUME }
6614
7076
  );
6615
- await startClaudeSession({ container: box.container, claudeArgs: [], boxName: box.name });
7077
+ const claudeCfg = await loadEffectiveConfig(box.workspacePath);
7078
+ await startClaudeSession({
7079
+ container: box.container,
7080
+ claudeArgs: applyClaudeSkipPermissions([], claudeCfg.effective),
7081
+ boxName: box.name
7082
+ });
6616
7083
  const info = await claudeSessionInfo(box.container);
6617
7084
  await waitForTmuxPaneContent(box.container, info.sessionName);
6618
7085
  return {
@@ -6629,7 +7096,11 @@ var dashboardCommand = new Command10("dashboard").description("Box list + the se
6629
7096
  }
6630
7097
  await ensureCodexInstalled(box.container);
6631
7098
  if (box.codexConfigVolume) await seedCodexHooks(box.codexConfigVolume, box.image);
6632
- await startCodexSession({ container: box.container, codexArgs: [] });
7099
+ const codexCfg = await loadEffectiveConfig(box.workspacePath);
7100
+ await startCodexSession({
7101
+ container: box.container,
7102
+ codexArgs: applyCodexSkipPermissions([], codexCfg.effective)
7103
+ });
6633
7104
  await waitForTmuxPaneContent(box.container, DEFAULT_CODEX_SESSION);
6634
7105
  return {
6635
7106
  kind: "attach",
@@ -6698,7 +7169,10 @@ var dashboardCommand = new Command10("dashboard").description("Box list + the se
6698
7169
  if (!agent) return { boxId: result.record.id };
6699
7170
  if (agent === "codex") {
6700
7171
  await ensureCodexInstalled(ctr, { onProgress });
6701
- await startCodexSession({ container: ctr, codexArgs: [] });
7172
+ await startCodexSession({
7173
+ container: ctr,
7174
+ codexArgs: applyCodexSkipPermissions([], cfg.effective)
7175
+ });
6702
7176
  await waitForTmuxPaneContent(ctr, DEFAULT_CODEX_SESSION);
6703
7177
  return {
6704
7178
  boxId: result.record.id,
@@ -6725,7 +7199,11 @@ var dashboardCommand = new Command10("dashboard").description("Box list + the se
6725
7199
  };
6726
7200
  }
6727
7201
  await rebuildPluginNativeDeps(ctr, { volume: result.record.claudeConfigVolume });
6728
- await startClaudeSession({ container: ctr, claudeArgs: [], boxName: result.record.name });
7202
+ await startClaudeSession({
7203
+ container: ctr,
7204
+ claudeArgs: applyClaudeSkipPermissions([], cfg.effective),
7205
+ boxName: result.record.name
7206
+ });
6729
7207
  const info = await claudeSessionInfo(ctr);
6730
7208
  await waitForTmuxPaneContent(ctr, info.sessionName);
6731
7209
  return {
@@ -7801,11 +8279,11 @@ function resolveToken(raw) {
7801
8279
  // src/lib/drive/tmux.ts
7802
8280
  var TMUX_USER = "vscode";
7803
8281
  async function captureSession(provider, box, session, opts = {}) {
7804
- const argv = ["tmux", "capture-pane", opts.ansi ? "-pe" : "-p", "-t", session];
8282
+ const argv2 = ["tmux", "capture-pane", opts.ansi ? "-pe" : "-p", "-t", session];
7805
8283
  if (opts.rows) {
7806
- argv.push("-S", String(opts.rows.from), "-E", String(opts.rows.to));
8284
+ argv2.push("-S", String(opts.rows.from), "-E", String(opts.rows.to));
7807
8285
  }
7808
- const res = await provider.exec(box, argv, { user: TMUX_USER });
8286
+ const res = await provider.exec(box, argv2, { user: TMUX_USER });
7809
8287
  if (res.exitCode !== 0) {
7810
8288
  throw new Error(failure("capture-pane", session, res.stderr || res.stdout));
7811
8289
  }
@@ -8062,8 +8540,8 @@ function sleep2(ms) {
8062
8540
  import { log as log28 } from "@clack/prompts";
8063
8541
  import { Command as Command23 } from "commander";
8064
8542
  import { existsSync as existsSync4, readdirSync, statSync } from "fs";
8065
- import { homedir as homedir8 } from "os";
8066
- import { join as join10 } from "path";
8543
+ import { homedir as homedir10 } from "os";
8544
+ import { join as join12 } from "path";
8067
8545
  var FORK_AGENTS = ["claude", "codex", "opencode"];
8068
8546
  var AGENT_COMMAND = {
8069
8547
  claude: claudeCommand,
@@ -8083,12 +8561,12 @@ function resolveSessionArgs(agent, opts) {
8083
8561
  }
8084
8562
  if (opts.session) return ["--resume", opts.session];
8085
8563
  if (agent === "codex") return ["--continue"];
8086
- const dir = join10(homedir8(), ".claude", "projects", encodeClaudeProjectsDir(opts.workspace));
8564
+ const dir = join12(homedir10(), ".claude", "projects", encodeClaudeProjectsDir(opts.workspace));
8087
8565
  if (!existsSync4(dir)) return ["--continue"];
8088
8566
  const now = Date.now();
8089
8567
  const recent = readdirSync(dir).filter((f) => f.endsWith(".jsonl")).map((f) => {
8090
8568
  try {
8091
- return statSync(join10(dir, f)).mtimeMs;
8569
+ return statSync(join12(dir, f)).mtimeMs;
8092
8570
  } catch {
8093
8571
  return 0;
8094
8572
  }
@@ -8166,115 +8644,1010 @@ var forkCommand = new Command23("fork").description(
8166
8644
  });
8167
8645
 
8168
8646
  // src/commands/install.ts
8169
- import { intro as intro5, log as log29, outro as outro6 } from "@clack/prompts";
8170
- import { Command as Command24 } from "commander";
8171
- import { existsSync as existsSync5, mkdirSync as mkdirSync3, readFileSync, writeFileSync as writeFileSync3 } from "fs";
8172
- import { homedir as homedir9 } from "os";
8173
- import { dirname, join as join11, resolve as resolve2 } from "path";
8647
+ import { confirm as confirm14, intro as intro6, isCancel as isCancel15, log as log30, note, outro as outro6, select as select2, spinner as spinner8 } from "@clack/prompts";
8648
+ import { Command as Command25 } from "commander";
8649
+ import { existsSync as existsSync6, mkdirSync as mkdirSync5, readFileSync, writeFileSync as writeFileSync4 } from "fs";
8650
+ import { homedir as homedir13 } from "os";
8651
+ import { dirname as dirname2, join as join15, resolve as resolve2 } from "path";
8174
8652
  import { fileURLToPath } from "url";
8175
- var MANAGED_SENTINEL = "<!-- agentbox-managed:v1 -->";
8176
- var LEGACY_INFO_MARKER = "Drive AgentBox from the host:";
8177
- function installTargets() {
8178
- const home = homedir9();
8179
- const claudeSkills = join11(home, ".claude", "skills");
8180
- return [
8181
- { src: join11("agentbox", "SKILL.md"), dest: join11(claudeSkills, "agentbox", "SKILL.md") },
8182
- { src: join11("agentbox-info", "SKILL.md"), dest: join11(claudeSkills, "agentbox-info", "SKILL.md") },
8183
- {
8184
- src: join11("codex", "agentbox.md"),
8185
- dest: join11(home, ".codex", "prompts", "agentbox.md"),
8186
- gateDir: join11(home, ".codex")
8187
- },
8188
- {
8189
- src: join11("opencode", "agentbox.md"),
8190
- dest: join11(home, ".config", "opencode", "commands", "agentbox.md"),
8191
- gateDir: join11(home, ".config", "opencode")
8192
- }
8193
- ];
8194
- }
8195
- function resolveHostSkillsDir() {
8196
- const here = dirname(fileURLToPath(import.meta.url));
8197
- const candidates = [
8198
- resolve2(here, "..", "share", "host-skills"),
8199
- // bundled: dist/ -> ../share
8200
- resolve2(here, "..", "..", "share", "host-skills")
8201
- // src: src/commands/ -> ../../share
8202
- ];
8203
- for (const c of candidates) {
8204
- if (existsSync5(c)) return c;
8653
+
8654
+ // src/lib/doctor-checks.ts
8655
+ import { accessSync, constants as fsConstants, mkdirSync as mkdirSync3 } from "fs";
8656
+ import { homedir as homedir11 } from "os";
8657
+ import { join as join13 } from "path";
8658
+ import { execa as execa2 } from "execa";
8659
+ var ALL_PROVIDERS = ["docker", "daytona", "hetzner", "vercel"];
8660
+ var NODE_MIN_MAJOR = 20;
8661
+ var NODE_MIN_MINOR = 10;
8662
+ async function probeVersion(bin, args = ["--version"]) {
8663
+ try {
8664
+ const r = await execa2(bin, args, { reject: false });
8665
+ if (r.exitCode !== 0) return null;
8666
+ const out = `${r.stdout ?? ""}${r.stderr ?? ""}`.trim().split("\n")[0] ?? "";
8667
+ return out.length > 0 ? out : bin;
8668
+ } catch {
8669
+ return null;
8205
8670
  }
8206
- throw new Error(
8207
- `could not locate bundled host skills; tried:
8208
- ${candidates.join("\n ")}`
8209
- );
8210
8671
  }
8211
- function writableReason(target, force) {
8212
- if (!existsSync5(target)) return "new";
8213
- const existing = readFileSync(target, "utf8");
8214
- if (existing.includes(MANAGED_SENTINEL) || existing.includes(LEGACY_INFO_MARKER)) {
8215
- return "managed";
8216
- }
8217
- return force ? "forced" : "skip";
8672
+ function parseNodeMajorMinor(v) {
8673
+ const m = /^v?(\d+)\.(\d+)/.exec(v);
8674
+ if (!m) return [0, 0];
8675
+ return [Number(m[1]), Number(m[2])];
8218
8676
  }
8219
- var installCommand = new Command24("install").description(
8220
- "Install AgentBox's host-side /agentbox fork command into Claude (~/.claude/skills), and \u2014 when detected \u2014 into Codex (~/.codex/prompts) and OpenCode (~/.config/opencode/commands). Idempotent."
8221
- ).option("--force", "overwrite existing files even if not AgentBox-managed").option("--dry-run", "print what would be written without changing anything").action((opts) => {
8222
- intro5("Installing AgentBox host commands...");
8223
- const force = opts.force === true;
8224
- const dryRun = opts.dryRun === true;
8225
- let srcDir;
8677
+ function firstLine(s) {
8678
+ const i = s.indexOf("\n");
8679
+ return i === -1 ? s : s.slice(0, i);
8680
+ }
8681
+ function errSummary(err) {
8682
+ return err instanceof Error ? firstLine(err.message) : String(err);
8683
+ }
8684
+ function checkNode() {
8685
+ const v = process.versions.node;
8686
+ const [maj, min] = parseNodeMajorMinor(v);
8687
+ const ok = maj > NODE_MIN_MAJOR || maj === NODE_MIN_MAJOR && min >= NODE_MIN_MINOR;
8688
+ return {
8689
+ label: "node",
8690
+ status: ok ? "ok" : "fail",
8691
+ detail: ok ? `v${v}` : `v${v} (need >=${String(NODE_MIN_MAJOR)}.${String(NODE_MIN_MINOR)})`,
8692
+ hint: ok ? void 0 : "upgrade Node before continuing"
8693
+ };
8694
+ }
8695
+ function checkPlatform() {
8696
+ return { label: "platform", status: "ok", detail: `${process.platform}/${process.arch}` };
8697
+ }
8698
+ function checkAgentboxHome() {
8699
+ const dir = join13(homedir11(), ".agentbox");
8226
8700
  try {
8227
- srcDir = resolveHostSkillsDir();
8701
+ mkdirSync3(dir, { recursive: true });
8702
+ accessSync(dir, fsConstants.W_OK);
8703
+ return { label: "~/.agentbox", status: "ok", detail: dir };
8228
8704
  } catch (err) {
8229
- log29.error(err instanceof Error ? err.message : String(err));
8230
- process.exit(1);
8705
+ return {
8706
+ label: "~/.agentbox",
8707
+ status: "fail",
8708
+ detail: `not writable: ${err instanceof Error ? err.message : String(err)}`,
8709
+ hint: "check directory permissions"
8710
+ };
8231
8711
  }
8232
- const written = [];
8233
- let skipped = 0;
8712
+ }
8713
+ async function checkGit() {
8714
+ const v = await probeVersion("git");
8715
+ return v ? { label: "git", status: "ok", detail: v } : {
8716
+ label: "git",
8717
+ status: "warn",
8718
+ detail: "not found",
8719
+ hint: "install git \u2014 required for the workspace git-bundle seed"
8720
+ };
8721
+ }
8722
+ async function checkSsh() {
8723
+ const v = await probeVersion("ssh", ["-V"]);
8724
+ return v ? { label: "ssh", status: "ok", detail: v } : {
8725
+ label: "ssh",
8726
+ status: "warn",
8727
+ detail: "not found",
8728
+ hint: "install ssh \u2014 required for hetzner and cloud attach"
8729
+ };
8730
+ }
8731
+ async function runSystemChecks() {
8732
+ const [git, ssh] = await Promise.all([checkGit(), checkSsh()]);
8733
+ return [checkNode(), checkPlatform(), checkAgentboxHome(), git, ssh];
8734
+ }
8735
+ async function dockerChecks() {
8736
+ const cli = await probeVersion("docker");
8737
+ if (!cli) {
8738
+ return [
8739
+ {
8740
+ label: "docker cli",
8741
+ status: "warn",
8742
+ detail: "not found",
8743
+ hint: "install Docker Desktop, OrbStack, or docker engine"
8744
+ }
8745
+ ];
8746
+ }
8747
+ const cliRes = { label: "docker cli", status: "ok", detail: cli };
8748
+ const info = await execa2("docker", ["info"], { reject: false });
8749
+ if (info.exitCode !== 0) {
8750
+ return [
8751
+ cliRes,
8752
+ {
8753
+ label: "docker daemon",
8754
+ status: "warn",
8755
+ detail: "unreachable",
8756
+ hint: "start Docker (Desktop / OrbStack / `systemctl start docker`)"
8757
+ }
8758
+ ];
8759
+ }
8760
+ const daemonRes = { label: "docker daemon", status: "ok", detail: "reachable" };
8761
+ const mod = await import("./dist-SBCQVFCE.js");
8762
+ let imgRes;
8763
+ try {
8764
+ const img = await mod.imageInfo(mod.DEFAULT_BOX_IMAGE);
8765
+ imgRes = img.exists ? { label: "box image", status: "ok", detail: `${mod.DEFAULT_BOX_IMAGE} built` } : {
8766
+ label: "box image",
8767
+ status: "warn",
8768
+ detail: `${mod.DEFAULT_BOX_IMAGE} not built`,
8769
+ hint: "run `agentbox prepare --provider docker` (or let the wizard do it)"
8770
+ };
8771
+ } catch (err) {
8772
+ imgRes = {
8773
+ label: "box image",
8774
+ status: "warn",
8775
+ detail: errSummary(err)
8776
+ };
8777
+ }
8778
+ const volNames = [mod.SHARED_CLAUDE_VOLUME, mod.SHARED_CODEX_VOLUME, mod.SHARED_OPENCODE_VOLUME];
8779
+ const vols = await Promise.all(
8780
+ volNames.map(async (n) => ({ name: n, exists: await mod.volumeExists(n).catch(() => false) }))
8781
+ );
8782
+ const present = vols.filter((v) => v.exists).length;
8783
+ const volRes = {
8784
+ label: "shared volumes",
8785
+ status: "ok",
8786
+ detail: `${String(present)}/${String(vols.length)} present (seeded lazily)`
8787
+ };
8788
+ return [cliRes, daemonRes, imgRes, volRes];
8789
+ }
8790
+ async function daytonaChecks() {
8791
+ try {
8792
+ const mod = await import("./dist-BD5QJRDC.js");
8793
+ const status = await mod.getDaytonaStatus();
8794
+ if (!status.configured) {
8795
+ return [
8796
+ {
8797
+ label: "credentials",
8798
+ status: "warn",
8799
+ detail: status.reason ?? "not configured",
8800
+ hint: "`agentbox daytona login`"
8801
+ }
8802
+ ];
8803
+ }
8804
+ const credRes = { label: "credentials", status: "ok", detail: "configured" };
8805
+ const snapRes = status.snapshots.length > 0 ? {
8806
+ label: "base snapshot",
8807
+ status: "ok",
8808
+ detail: `${String(status.snapshots.length)} agentbox snapshot(s)`
8809
+ } : {
8810
+ label: "base snapshot",
8811
+ status: "warn",
8812
+ detail: "none",
8813
+ hint: "`agentbox prepare --provider daytona`"
8814
+ };
8815
+ return [credRes, snapRes];
8816
+ } catch (err) {
8817
+ return [
8818
+ {
8819
+ label: "credentials",
8820
+ status: "warn",
8821
+ detail: errSummary(err)
8822
+ }
8823
+ ];
8824
+ }
8825
+ }
8826
+ async function hetznerChecks() {
8827
+ try {
8828
+ const mod = await import("./dist-BNI5PQYK.js");
8829
+ const cred = mod.readHetznerCredStatus();
8830
+ const credRes = cred.source === "none" ? {
8831
+ label: "credentials",
8832
+ status: "warn",
8833
+ detail: "HCLOUD_TOKEN not set",
8834
+ hint: "`agentbox hetzner login`"
8835
+ } : { label: "credentials", status: "ok", detail: `token from ${cred.source}` };
8836
+ const prepared = mod.readPreparedState();
8837
+ const snapRes = prepared.base?.imageId ? {
8838
+ label: "base snapshot",
8839
+ status: "ok",
8840
+ detail: `image ${String(prepared.base.imageId)} (${prepared.base.cliVersion ?? "\u2014"})`
8841
+ } : {
8842
+ label: "base snapshot",
8843
+ status: "warn",
8844
+ detail: "not baked",
8845
+ hint: "`agentbox prepare --provider hetzner`"
8846
+ };
8847
+ return [credRes, snapRes];
8848
+ } catch (err) {
8849
+ return [
8850
+ {
8851
+ label: "credentials",
8852
+ status: "warn",
8853
+ detail: errSummary(err)
8854
+ }
8855
+ ];
8856
+ }
8857
+ }
8858
+ async function vercelChecks() {
8859
+ try {
8860
+ const mod = await import("./dist-SJHY3HYN.js");
8861
+ const cred = mod.readVercelCredStatus();
8862
+ const credRes = cred.auth === "none" ? {
8863
+ label: "credentials",
8864
+ status: "warn",
8865
+ detail: "not configured",
8866
+ hint: "`agentbox vercel login`"
8867
+ } : {
8868
+ label: "credentials",
8869
+ status: "ok",
8870
+ detail: `${cred.auth} (${cred.source})`
8871
+ };
8872
+ const prepared = mod.readPreparedState();
8873
+ const snapRes = prepared.base?.snapshotId ? {
8874
+ label: "base snapshot",
8875
+ status: "ok",
8876
+ detail: `${prepared.base.snapshotId.slice(0, 16)}\u2026 (${prepared.base.cliVersion ?? "\u2014"})`
8877
+ } : {
8878
+ label: "base snapshot",
8879
+ status: "warn",
8880
+ detail: "not baked",
8881
+ hint: "`agentbox prepare --provider vercel`"
8882
+ };
8883
+ return [credRes, snapRes];
8884
+ } catch (err) {
8885
+ return [
8886
+ {
8887
+ label: "credentials",
8888
+ status: "warn",
8889
+ detail: errSummary(err)
8890
+ }
8891
+ ];
8892
+ }
8893
+ }
8894
+ async function runProviderChecks(name) {
8895
+ let results;
8896
+ switch (name) {
8897
+ case "docker":
8898
+ results = await dockerChecks();
8899
+ break;
8900
+ case "daytona":
8901
+ results = await daytonaChecks();
8902
+ break;
8903
+ case "hetzner":
8904
+ results = await hetznerChecks();
8905
+ break;
8906
+ case "vercel":
8907
+ results = await vercelChecks();
8908
+ break;
8909
+ }
8910
+ return { title: name, results };
8911
+ }
8912
+ async function runAllChecks() {
8913
+ const sys = { title: "system", results: await runSystemChecks() };
8914
+ const providerGroups = await Promise.all(ALL_PROVIDERS.map((n) => runProviderChecks(n)));
8915
+ return [sys, ...providerGroups];
8916
+ }
8917
+ function worstInResults(results) {
8918
+ let worst = "ok";
8919
+ for (const r of results) {
8920
+ if (r.status === "fail") return "fail";
8921
+ if (r.status === "warn") worst = "warn";
8922
+ }
8923
+ return worst;
8924
+ }
8925
+ function worstStatus(groups) {
8926
+ let worst = "ok";
8927
+ for (const g of groups) {
8928
+ const w = worstInResults(g.results);
8929
+ if (w === "fail") return "fail";
8930
+ if (w === "warn") worst = "warn";
8931
+ }
8932
+ return worst;
8933
+ }
8934
+ function summaryToken(group) {
8935
+ const worst = worstInResults(group.results);
8936
+ if (group.title === "system") {
8937
+ if (worst === "fail") return "system FAIL";
8938
+ if (worst === "warn") return "system warn";
8939
+ return "system ok";
8940
+ }
8941
+ if (worst === "fail") return `${group.title} FAIL`;
8942
+ if (worst === "warn") {
8943
+ const cred = group.results.find((r) => r.label === "credentials");
8944
+ if (cred && cred.status === "warn") return `${group.title} login needed`;
8945
+ return `${group.title} not prepared`;
8946
+ }
8947
+ return `${group.title} ready`;
8948
+ }
8949
+ var C_GREEN = "\x1B[32m";
8950
+ var C_YELLOW = "\x1B[33m";
8951
+ var C_RED = "\x1B[31m";
8952
+ var C_RESET = "\x1B[0m";
8953
+ var COLOR = !process.env.NO_COLOR;
8954
+ function statusMarker(s) {
8955
+ const glyph = s === "ok" ? "\u2713" : s === "warn" ? "\u26A0" : "\u2717";
8956
+ if (!COLOR) return glyph;
8957
+ const color = s === "ok" ? C_GREEN : s === "warn" ? C_YELLOW : C_RED;
8958
+ return `${color}${glyph}${C_RESET}`;
8959
+ }
8960
+ function formatCompact(groups) {
8961
+ return groups.map((g) => `${statusMarker(worstInResults(g.results))} ${summaryToken(g)}`).join(" \xB7 ");
8962
+ }
8963
+ function pad2(s, width) {
8964
+ return s.length >= width ? s : s + " ".repeat(width - s.length);
8965
+ }
8966
+ function statusBadge(s) {
8967
+ if (s === "ok") return "[ ok ]";
8968
+ if (s === "warn") return "[warn]";
8969
+ return "[FAIL]";
8970
+ }
8971
+ function formatDetailed(groups) {
8972
+ const lines = [];
8973
+ for (const g of groups) {
8974
+ if (lines.length > 0) lines.push("");
8975
+ lines.push(`${g.title}:`);
8976
+ for (const r of g.results) {
8977
+ const badge = statusBadge(r.status);
8978
+ const tail = r.hint ? ` (${r.hint})` : "";
8979
+ lines.push(` ${badge} ${pad2(r.label, 18)} ${r.detail}${tail}`);
8980
+ }
8981
+ }
8982
+ return lines;
8983
+ }
8984
+
8985
+ // src/lib/first-run.ts
8986
+ import { existsSync as existsSync5, mkdirSync as mkdirSync4, writeFileSync as writeFileSync3 } from "fs";
8987
+ import { homedir as homedir12 } from "os";
8988
+ import { dirname, join as join14 } from "path";
8989
+ var MARKER_VERSION = 1;
8990
+ function setupMarkerPath() {
8991
+ return join14(homedir12(), ".agentbox", "setup-complete.json");
8992
+ }
8993
+ function isFirstRun() {
8994
+ return !existsSync5(setupMarkerPath());
8995
+ }
8996
+ function markSetupComplete(provider) {
8997
+ const path = setupMarkerPath();
8998
+ mkdirSync4(dirname(path), { recursive: true });
8999
+ const body = {
9000
+ version: MARKER_VERSION,
9001
+ completedAt: (/* @__PURE__ */ new Date()).toISOString(),
9002
+ provider
9003
+ };
9004
+ writeFileSync3(path, JSON.stringify(body, null, 2) + "\n");
9005
+ }
9006
+
9007
+ // src/commands/prepare.ts
9008
+ import { intro as intro5, log as log29, spinner as spinner7 } from "@clack/prompts";
9009
+ import { Command as Command24 } from "commander";
9010
+ async function dockerStatus() {
9011
+ let img;
9012
+ try {
9013
+ img = await imageInfo(DEFAULT_BOX_IMAGE);
9014
+ } catch {
9015
+ return { daemon: "unreachable", volumes: [] };
9016
+ }
9017
+ const names = [SHARED_CLAUDE_VOLUME, SHARED_CODEX_VOLUME, SHARED_OPENCODE_VOLUME];
9018
+ const volumes = await Promise.all(
9019
+ names.map(async (name) => ({ name, exists: await volumeExists(name).catch(() => false) }))
9020
+ );
9021
+ return { daemon: "reachable", image: img, volumes };
9022
+ }
9023
+ function humanBytes(n) {
9024
+ if (n === void 0 || !Number.isFinite(n)) return "\u2014";
9025
+ if (n >= 1024 ** 3) return `${(n / 1024 ** 3).toFixed(2)} GB`;
9026
+ if (n >= 1024 ** 2) return `${(n / 1024 ** 2).toFixed(1)} MB`;
9027
+ if (n >= 1024) return `${(n / 1024).toFixed(1)} KB`;
9028
+ return `${String(n)} B`;
9029
+ }
9030
+ function humanAge(iso) {
9031
+ if (!iso) return "\u2014";
9032
+ const t = Date.parse(iso);
9033
+ if (!Number.isFinite(t)) return iso;
9034
+ const ageSec = Math.max(0, (Date.now() - t) / 1e3);
9035
+ if (ageSec < 60) return `${ageSec.toFixed(0)}s ago`;
9036
+ if (ageSec < 3600) return `${(ageSec / 60).toFixed(0)}m ago`;
9037
+ if (ageSec < 86400) return `${(ageSec / 3600).toFixed(1)}h ago`;
9038
+ return `${(ageSec / 86400).toFixed(1)}d ago`;
9039
+ }
9040
+ function pad3(s, width) {
9041
+ return s.length >= width ? s : s + " ".repeat(width - s.length);
9042
+ }
9043
+ async function renderDocker(status) {
9044
+ const out = ["docker:"];
9045
+ if (status.daemon === "unreachable") {
9046
+ out.push(" docker daemon unreachable (is Docker running?)");
9047
+ return out;
9048
+ }
9049
+ if (!status.image?.exists) {
9050
+ out.push(
9051
+ ` image ${DEFAULT_BOX_IMAGE} (not built \u2014 run \`agentbox prepare --provider docker\`)`
9052
+ );
9053
+ } else {
9054
+ out.push(
9055
+ ` image ${pad3(DEFAULT_BOX_IMAGE, 30)} ${pad3(humanBytes(status.image.sizeBytes), 10)} built ${humanAge(status.image.createdAt)}`
9056
+ );
9057
+ }
9058
+ for (const v of status.volumes) {
9059
+ if (v.exists) {
9060
+ out.push(` vol ${pad3(v.name, 30)} present`);
9061
+ } else {
9062
+ out.push(
9063
+ ` vol ${pad3(v.name, 30)} (none \u2014 seeded lazily on first \`agentbox claude/codex/opencode\`)`
9064
+ );
9065
+ }
9066
+ }
9067
+ return out;
9068
+ }
9069
+ async function daytonaStatus() {
9070
+ try {
9071
+ const mod = await import("./dist-BD5QJRDC.js");
9072
+ return await mod.getDaytonaStatus();
9073
+ } catch (err) {
9074
+ return {
9075
+ configured: false,
9076
+ reason: err instanceof Error ? err.message.split("\n")[0] : String(err)
9077
+ };
9078
+ }
9079
+ }
9080
+ function renderDaytona(status, pinnedImage) {
9081
+ const out = ["daytona:"];
9082
+ if (!status.configured) {
9083
+ out.push(
9084
+ ` (not configured \u2014 \`agentbox daytona login\` to set up${status.reason ? `; ${status.reason}` : ""})`
9085
+ );
9086
+ return out;
9087
+ }
9088
+ if (status.reason) out.push(` warn: ${status.reason}`);
9089
+ if (status.snapshots.length === 0) {
9090
+ out.push(" no agentbox snapshots \u2014 run `agentbox prepare --provider daytona`");
9091
+ } else {
9092
+ for (const s of status.snapshots) {
9093
+ const sizeStr = s.sizeGb !== void 0 ? `${s.sizeGb.toFixed(2)} GB` : "\u2014";
9094
+ const pinned = pinnedImage && pinnedImage === s.name ? " (pinned in project)" : "";
9095
+ const tail = s.state === "error" && s.errorReason ? ` error: ${s.errorReason.slice(0, 80)}` : ` ${humanAge(s.createdAt)}`;
9096
+ out.push(
9097
+ ` snap ${pad3(s.name, 40)} ${pad3(s.state ?? "\u2014", 10)} ${pad3(sizeStr, 10)}${tail}${pinned}`
9098
+ );
9099
+ }
9100
+ }
9101
+ if (status.volumes.length === 0) {
9102
+ out.push(" no agentbox volumes \u2014 created lazily on first cloud `agentbox create`");
9103
+ } else {
9104
+ for (const v of status.volumes) {
9105
+ const last = v.lastUsedAt ? ` last used ${humanAge(v.lastUsedAt)}` : "";
9106
+ out.push(` vol ${pad3(v.name, 40)} ${pad3(v.state ?? "\u2014", 10)}${last}`);
9107
+ }
9108
+ }
9109
+ return out;
9110
+ }
9111
+ async function showStatus(opts) {
9112
+ const cfg = await loadEffectiveConfig(process.cwd()).catch(() => null);
9113
+ const pinnedRaw = cfg?.effective.box.image;
9114
+ const pinned = typeof pinnedRaw === "string" && pinnedRaw.length > 0 && pinnedRaw !== DEFAULT_BOX_IMAGE ? pinnedRaw : void 0;
9115
+ const lines = [];
9116
+ const wantDocker = !opts.onlyProvider || opts.onlyProvider === "docker";
9117
+ const wantDaytona = !opts.onlyProvider || opts.onlyProvider === "daytona";
9118
+ if (wantDocker) {
9119
+ const status = await dockerStatus();
9120
+ lines.push(...await renderDocker(status));
9121
+ }
9122
+ if (wantDaytona) {
9123
+ if (lines.length > 0) lines.push("");
9124
+ const status = await daytonaStatus();
9125
+ lines.push(...renderDaytona(status, pinned));
9126
+ }
9127
+ if (pinned) {
9128
+ lines.push("");
9129
+ lines.push(`project pin: box.image = ${pinned}`);
9130
+ }
9131
+ process.stdout.write(lines.join("\n") + "\n");
9132
+ }
9133
+ async function runPrepare(providerName, opts = {}) {
9134
+ if (!isKnownProvider(providerName)) {
9135
+ process.stderr.write("error: --provider must be one of: docker, daytona, hetzner, vercel\n");
9136
+ process.exit(1);
9137
+ }
9138
+ if (providerName === "daytona" && !opts.yes && process.stdin.isTTY) {
9139
+ process.stdout.write(
9140
+ "This will trigger a Daytona image build (~7 min cold, ~seconds with cache) and register a named snapshot in your org.\nRe-run with --yes to skip this notice.\n"
9141
+ );
9142
+ }
9143
+ const provider = await getProvider(providerName);
9144
+ if (typeof provider.prepare !== "function") {
9145
+ log29.error(`provider '${providerName}' does not implement prepare`);
9146
+ process.exit(1);
9147
+ }
9148
+ const cwd = opts.cwd ?? process.cwd();
9149
+ const registry = providerName === "docker" ? await loadEffectiveConfig(cwd).then((c) => c.effective.box.imageRegistry).catch(() => void 0) : void 0;
9150
+ const sp = spinner7();
9151
+ sp.start(`preparing ${providerName}\u2026`);
9152
+ try {
9153
+ const result = await provider.prepare({
9154
+ name: opts.name,
9155
+ hostWorkspace: cwd,
9156
+ force: opts.force,
9157
+ allowPull: opts.build ? false : void 0,
9158
+ registry,
9159
+ onLog: (line) => sp.message(line.slice(0, 80))
9160
+ });
9161
+ if (result.snapshotName !== void 0) {
9162
+ sp.stop(`prepared ${providerName}: snapshot '${result.snapshotName}'`);
9163
+ try {
9164
+ const written = await setConfigValue("project", "box.image", result.snapshotName, cwd);
9165
+ log29.success(`box.image = ${result.snapshotName} (written to ${written.path})`);
9166
+ } catch (err) {
9167
+ const msg = err instanceof Error ? err.message : String(err);
9168
+ log29.warn(
9169
+ `prepared snapshot '${result.snapshotName}', but failed to pin it into the project config: ${msg}
9170
+ Run \`agentbox config set --project box.image ${result.snapshotName}\` manually.`
9171
+ );
9172
+ }
9173
+ } else {
9174
+ sp.stop(`${providerName.slice(0, 1).toUpperCase() + providerName.slice(1)} provider ready`);
9175
+ }
9176
+ if (!opts.suppressStatus) {
9177
+ process.stdout.write("\n");
9178
+ await showStatus({ onlyProvider: providerName });
9179
+ }
9180
+ if (!opts.suppressTip) {
9181
+ log29.info(
9182
+ "tip: install the agentbox host skill so Claude Code on this machine can drive AgentBox for you:\n npx skills add https://github.com/madarco/agentbox --skill agentbox"
9183
+ );
9184
+ }
9185
+ } catch (err) {
9186
+ sp.stop(`prepare failed: ${describeError(err)}`);
9187
+ throw err;
9188
+ }
9189
+ }
9190
+ var prepareCommand = new Command24("prepare").description(
9191
+ "Build base sandbox images / snapshots, or show what is already prepared across providers."
9192
+ ).option(
9193
+ "-p, --provider <name>",
9194
+ "provider to prepare (docker | daytona | hetzner | vercel). Omit for status-only."
9195
+ ).option("-n, --name <name>", "snapshot name (Daytona only; default: agentbox-base-<timestamp>)").option("-f, --force", "rebuild even if the image / snapshot already exists").option(
9196
+ "--build",
9197
+ "docker: build the base image locally instead of pulling the prebuilt one from the registry"
9198
+ ).option("-y, --yes", "skip confirmation prompts (cost / time warnings)").option("--status", "show status without preparing anything").action(async (opts) => {
9199
+ if (!opts.provider || opts.status) {
9200
+ await showStatus({});
9201
+ return;
9202
+ }
9203
+ const providerName = opts.provider.trim();
9204
+ intro5(`preparing ${providerName} base image`);
9205
+ await runPrepare(providerName, {
9206
+ name: opts.name,
9207
+ force: opts.force,
9208
+ build: opts.build,
9209
+ yes: opts.yes
9210
+ });
9211
+ });
9212
+ function describeError(err) {
9213
+ if (!(err instanceof Error)) return String(err);
9214
+ const parts = [err.message];
9215
+ let cause = err.cause;
9216
+ for (let i = 0; i < 5 && cause; i++) {
9217
+ if (cause instanceof Error) {
9218
+ parts.push(`caused by: ${cause.message}`);
9219
+ const code = cause.code;
9220
+ if (typeof code === "string") parts.push(`(${code})`);
9221
+ cause = cause.cause;
9222
+ } else if (typeof cause === "object") {
9223
+ parts.push(`caused by: ${JSON.stringify(cause)}`);
9224
+ break;
9225
+ } else {
9226
+ parts.push(`caused by: ${String(cause)}`);
9227
+ break;
9228
+ }
9229
+ }
9230
+ return parts.join(" \u2014 ");
9231
+ }
9232
+
9233
+ // src/commands/install.ts
9234
+ var MANAGED_SENTINEL = "<!-- agentbox-managed:v1 -->";
9235
+ var LOGO_L1 = "\u2584\u2580\u2588 \u2588\u2580\u2580 \u2588\u2580\u2580 \u2588\u2584\u2591\u2588 \u2580\u2588\u2580 \u2588\u2584\u2584 \u2588\u2580\u2588 \u2580\u2584\u2580";
9236
+ var LOGO_L2 = "\u2588\u2580\u2588 \u2588\u2584\u2588 \u2588\u2588\u2584 \u2588\u2591\u2580\u2588 \u2591\u2588\u2591 \u2588\u2584\u2588 \u2588\u2584\u2588 \u2588\u2591\u2588";
9237
+ var LOGO_WIDTH = LOGO_L1.length;
9238
+ var BANNER = (() => {
9239
+ const art = `${LOGO_L1}
9240
+ ${LOGO_L2}`;
9241
+ const tinted = process.env.NO_COLOR ? art : `\x1B[38;5;39m${art}\x1B[0m`;
9242
+ return `
9243
+ ${tinted}
9244
+
9245
+ `;
9246
+ })();
9247
+ var SYNC_BEGIN2 = "\x1B[?2026h";
9248
+ var SYNC_END2 = "\x1B[?2026l";
9249
+ var HIDE_CURSOR = "\x1B[?25l";
9250
+ var SHOW_CURSOR = "\x1B[?25h";
9251
+ var SPIN = ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"];
9252
+ var sleep3 = (ms) => new Promise((r) => setTimeout(r, ms));
9253
+ function shineColor(dist) {
9254
+ const d = Math.abs(dist);
9255
+ if (d === 0) return 231;
9256
+ if (d === 1) return 159;
9257
+ if (d === 2) return 81;
9258
+ return 39;
9259
+ }
9260
+ function paintLine(line, center) {
9261
+ let out = "";
9262
+ let prev = -1;
9263
+ for (let i = 0; i < line.length; i++) {
9264
+ const ch = line[i];
9265
+ const c = shineColor(i - center);
9266
+ if (c !== prev) {
9267
+ out += `\x1B[38;5;${String(c)}m`;
9268
+ prev = c;
9269
+ }
9270
+ out += ch;
9271
+ }
9272
+ return out + "\x1B[0m";
9273
+ }
9274
+ async function animateBanner() {
9275
+ if (process.env.NO_COLOR || process.env.CI || process.env.AGENTBOX_NO_ANIM || !process.stdout.isTTY) {
9276
+ process.stdout.write(BANNER);
9277
+ return;
9278
+ }
9279
+ const restoreCursor = () => {
9280
+ process.stdout.write(SHOW_CURSOR);
9281
+ };
9282
+ process.once("exit", restoreCursor);
9283
+ const onSigint = () => {
9284
+ restoreCursor();
9285
+ process.exit(130);
9286
+ };
9287
+ process.once("SIGINT", onSigint);
9288
+ const frameMs = 45;
9289
+ const start = -3;
9290
+ const end = LOGO_WIDTH + 2;
9291
+ const down = 4;
9292
+ process.stdout.write(`
9293
+ ${HIDE_CURSOR}`);
9294
+ process.stdout.write("\n".repeat(down) + `\x1B[${String(down)}A`);
9295
+ const statusLine2 = (spin) => ` \x1B[38;5;51m${spin}\x1B[0m \x1B[38;5;245mChecking system\u2026\x1B[0m`;
9296
+ for (let center = start; center <= end; center++) {
9297
+ const spin = SPIN[Math.floor((center - start) / 2) % SPIN.length] ?? SPIN[0];
9298
+ const frame = SYNC_BEGIN2 + paintLine(LOGO_L1, center) + "\n" + paintLine(LOGO_L2, center) + "\n\n\x1B[2K" + // down to the status row (col 0), clear it
9299
+ statusLine2(spin) + "\x1B[3A\r" + // back up to the logo's first row
9300
+ SYNC_END2;
9301
+ process.stdout.write(frame);
9302
+ await sleep3(frameMs);
9303
+ }
9304
+ process.stdout.write(SYNC_BEGIN2 + `\x1B[38;5;39m${LOGO_L1}
9305
+ ${LOGO_L2}\x1B[0m` + SYNC_END2);
9306
+ await sleep3(250);
9307
+ process.stdout.write(SYNC_BEGIN2 + "\n\x1B[2K\n\x1B[2K" + SHOW_CURSOR + SYNC_END2);
9308
+ process.removeListener("exit", restoreCursor);
9309
+ process.removeListener("SIGINT", onSigint);
9310
+ }
9311
+ var LEGACY_INFO_MARKER = "Drive AgentBox from the host:";
9312
+ function installTargets() {
9313
+ const home = homedir13();
9314
+ const claudeSkills = join15(home, ".claude", "skills");
9315
+ return [
9316
+ { src: join15("agentbox", "SKILL.md"), dest: join15(claudeSkills, "agentbox", "SKILL.md") },
9317
+ {
9318
+ src: join15("agentbox-info", "SKILL.md"),
9319
+ dest: join15(claudeSkills, "agentbox-info", "SKILL.md")
9320
+ },
9321
+ {
9322
+ src: join15("codex", "agentbox.md"),
9323
+ dest: join15(home, ".codex", "prompts", "agentbox.md"),
9324
+ gateDir: join15(home, ".codex")
9325
+ },
9326
+ {
9327
+ src: join15("opencode", "agentbox.md"),
9328
+ dest: join15(home, ".config", "opencode", "commands", "agentbox.md"),
9329
+ gateDir: join15(home, ".config", "opencode")
9330
+ }
9331
+ ];
9332
+ }
9333
+ function resolveHostSkillsDir() {
9334
+ const here = dirname2(fileURLToPath(import.meta.url));
9335
+ const candidates = [
9336
+ resolve2(here, "..", "share", "host-skills"),
9337
+ resolve2(here, "..", "..", "share", "host-skills")
9338
+ ];
9339
+ for (const c of candidates) {
9340
+ if (existsSync6(c)) return c;
9341
+ }
9342
+ throw new Error(`could not locate bundled host skills; tried:
9343
+ ${candidates.join("\n ")}`);
9344
+ }
9345
+ function writableReason(target, force) {
9346
+ if (!existsSync6(target)) return "new";
9347
+ const existing = readFileSync(target, "utf8");
9348
+ if (existing.includes(MANAGED_SENTINEL) || existing.includes(LEGACY_INFO_MARKER)) {
9349
+ return "managed";
9350
+ }
9351
+ return force ? "forced" : "skip";
9352
+ }
9353
+ function installHostSkills(opts = {}) {
9354
+ const force = opts.force === true;
9355
+ const dryRun = opts.dryRun === true;
9356
+ const quiet = opts.quiet === true;
9357
+ const srcDir = resolveHostSkillsDir();
9358
+ const written = [];
9359
+ const blocked = [];
9360
+ let skipped = 0;
8234
9361
  for (const t of installTargets()) {
8235
- const src = join11(srcDir, t.src);
8236
- if (!existsSync5(src)) {
8237
- log29.warn(`bundled file missing (skipped): ${src}`);
9362
+ const src = join15(srcDir, t.src);
9363
+ if (!existsSync6(src)) {
9364
+ if (!quiet) log30.warn(`bundled file missing (skipped): ${src}`);
8238
9365
  skipped++;
8239
9366
  continue;
8240
9367
  }
8241
- if (t.gateDir && !existsSync5(t.gateDir)) continue;
9368
+ if (t.gateDir && !existsSync6(t.gateDir)) continue;
8242
9369
  const reason = writableReason(t.dest, force);
8243
9370
  if (reason === "skip") {
8244
- log29.warn(`user-modified file at ${t.dest}, skipping; pass --force to overwrite`);
9371
+ if (!quiet) log30.warn(`user-modified file at ${t.dest}, skipping; pass --force to overwrite`);
9372
+ blocked.push(t.dest);
8245
9373
  skipped++;
8246
9374
  continue;
8247
9375
  }
8248
9376
  if (dryRun) {
8249
- log29.info(`would write ${t.dest} (${reason})`);
9377
+ if (!quiet) log30.info(`would write ${t.dest} (${reason})`);
8250
9378
  written.push(t.dest);
8251
9379
  continue;
8252
9380
  }
8253
- mkdirSync3(dirname(t.dest), { recursive: true });
8254
- writeFileSync3(t.dest, readFileSync(src, "utf8"));
9381
+ mkdirSync5(dirname2(t.dest), { recursive: true });
9382
+ writeFileSync4(t.dest, readFileSync(src, "utf8"));
8255
9383
  written.push(t.dest);
8256
9384
  }
8257
- if (dryRun) {
8258
- outro6(`dry-run: ${String(written.length)} file(s) would be written, ${String(skipped)} skipped`);
8259
- return;
9385
+ return { written, skipped, blocked };
9386
+ }
9387
+ var PROVIDER_HINTS = {
9388
+ docker: "builds a ~1GB local image; no login needed",
9389
+ hetzner: "paste an API token from the Hetzner Console",
9390
+ daytona: "approve a browser sign-in link",
9391
+ vercel: "installs the Vercel sandbox CLI, then a browser sign-in"
9392
+ };
9393
+ var PROVIDER_LABEL = {
9394
+ docker: "Docker (local)",
9395
+ hetzner: "Hetzner (cloud VPS)",
9396
+ daytona: "Daytona (cloud sandbox)",
9397
+ vercel: "Vercel (cloud microVM)"
9398
+ };
9399
+ function ensureTty() {
9400
+ if (process.stdin.isTTY && process.stdout.isTTY) return true;
9401
+ process.stderr.write(
9402
+ "agentbox install: an interactive terminal is required. Run `agentbox <provider> login` and `agentbox prepare --provider <name>` instead.\n"
9403
+ );
9404
+ return false;
9405
+ }
9406
+ async function runProviderLogin(name) {
9407
+ if (name === "docker") return true;
9408
+ if (name === "daytona") {
9409
+ const mod2 = await import("./dist-BD5QJRDC.js");
9410
+ const status2 = await mod2.getDaytonaStatus();
9411
+ if (status2.configured) {
9412
+ log30.info("daytona: already configured");
9413
+ const rotate = await confirm14({ message: "Re-authenticate Daytona?", initialValue: false });
9414
+ if (isCancel15(rotate)) return false;
9415
+ if (rotate) await mod2.ensureDaytonaCredentials({ force: true });
9416
+ return true;
9417
+ }
9418
+ await mod2.ensureDaytonaCredentials();
9419
+ return true;
9420
+ }
9421
+ if (name === "hetzner") {
9422
+ const mod2 = await import("./dist-BNI5PQYK.js");
9423
+ const status2 = mod2.readHetznerCredStatus();
9424
+ if (status2.source !== "none") {
9425
+ log30.info("hetzner: already configured");
9426
+ const rotate = await confirm14({ message: "Re-authenticate Hetzner?", initialValue: false });
9427
+ if (isCancel15(rotate)) return false;
9428
+ if (rotate) await mod2.ensureHetznerCredentials({ force: true });
9429
+ return true;
9430
+ }
9431
+ await mod2.ensureHetznerCredentials();
9432
+ return true;
9433
+ }
9434
+ const mod = await import("./dist-SJHY3HYN.js");
9435
+ const status = mod.readVercelCredStatus();
9436
+ if (status.auth !== "none") {
9437
+ log30.info(`vercel: already configured (${status.auth})`);
9438
+ const rotate = await confirm14({ message: "Re-authenticate Vercel?", initialValue: false });
9439
+ if (isCancel15(rotate)) return false;
9440
+ if (rotate) await mod.ensureVercelCredentials({ force: true });
9441
+ return true;
9442
+ }
9443
+ await mod.ensureVercelCredentials();
9444
+ return true;
9445
+ }
9446
+ function tutorialBody(provider) {
9447
+ const startCmd = provider === "docker" ? "agentbox claude" : `agentbox ${provider} claude`;
9448
+ return `Get started:
9449
+ ${startCmd} # for claude, codex, opencode
9450
+ -> Setup wizard? -> Yes # install dependencies and setup agentbox.yaml
9451
+ -> Ctrl+a d # to detach from the box and leave claude running
9452
+ agentbox claude attach 1 # resume it later
9453
+ agentbox install # to set up another provider`;
9454
+ }
9455
+ var KNOWN_PROVIDERS = ["docker", "hetzner", "daytona", "vercel"];
9456
+ function isProviderName(s) {
9457
+ return KNOWN_PROVIDERS.includes(s);
9458
+ }
9459
+ async function runInstallWizard(opts = {}) {
9460
+ if (!ensureTty()) return false;
9461
+ await animateBanner();
9462
+ intro6("Check system compatibility");
9463
+ const sysResults = await runSystemChecks();
9464
+ const sysGroup = { title: "system", results: sysResults };
9465
+ process.stdout.write(" " + formatCompact([sysGroup]) + "\n");
9466
+ const hardFail = sysResults.find((r) => r.status === "fail");
9467
+ if (hardFail) {
9468
+ log30.error(`system check failed: ${hardFail.label} \u2014 ${hardFail.detail}`);
9469
+ log30.info("run `agentbox doctor` for full detail");
9470
+ const cont = await confirm14({ message: "Continue anyway?", initialValue: false });
9471
+ if (isCancel15(cont) || !cont) {
9472
+ outro6("aborted");
9473
+ return false;
9474
+ }
9475
+ }
9476
+ let providerName;
9477
+ if (opts.provider) {
9478
+ const candidate = opts.provider.trim();
9479
+ if (!isProviderName(candidate)) {
9480
+ log30.error(`unknown --provider: ${candidate}`);
9481
+ return false;
9482
+ }
9483
+ providerName = candidate;
9484
+ } else {
9485
+ const picked = await select2({
9486
+ message: "Which provider do you want to set up?",
9487
+ initialValue: "docker",
9488
+ options: KNOWN_PROVIDERS.map((p) => ({
9489
+ value: p,
9490
+ label: PROVIDER_LABEL[p],
9491
+ hint: PROVIDER_HINTS[p]
9492
+ }))
9493
+ });
9494
+ if (isCancel15(picked)) {
9495
+ outro6("cancelled");
9496
+ return false;
9497
+ }
9498
+ providerName = picked;
8260
9499
  }
8261
- if (written.length === 0) {
8262
- outro6(`nothing installed (${String(skipped)} skipped)`);
9500
+ if (providerName !== "docker") {
9501
+ const loggedIn = await runProviderLogin(providerName);
9502
+ if (!loggedIn) {
9503
+ outro6("cancelled");
9504
+ return false;
9505
+ }
9506
+ }
9507
+ const prepareMsg = providerName === "docker" ? "Build the box image now? (~1GB, a few minutes)" : `Bake the ${providerName} base snapshot now? (a few minutes, uses cloud time)`;
9508
+ const wantPrepare = opts.yes ? true : await confirm14({ message: prepareMsg, initialValue: true });
9509
+ if (isCancel15(wantPrepare)) {
9510
+ outro6("cancelled");
9511
+ return false;
9512
+ }
9513
+ if (wantPrepare) {
9514
+ try {
9515
+ await runPrepare(providerName, {
9516
+ cwd: process.cwd(),
9517
+ yes: true,
9518
+ suppressStatus: true,
9519
+ suppressTip: true
9520
+ });
9521
+ } catch (err) {
9522
+ log30.warn(
9523
+ `prepare failed: ${err instanceof Error ? err.message : String(err)} \u2014 you can rerun \`agentbox prepare --provider ${providerName}\` later`
9524
+ );
9525
+ }
9526
+ } else {
9527
+ log30.info(
9528
+ `skipped \u2014 the ${providerName} base will build lazily on first \`agentbox ${providerName === "docker" ? "" : providerName + " "}create\``
9529
+ );
9530
+ }
9531
+ const sp = spinner8();
9532
+ sp.start("installing host /agentbox skill\u2026");
9533
+ let skillRes;
9534
+ try {
9535
+ skillRes = installHostSkills({ force: opts.force, dryRun: opts.dryRun, quiet: true });
9536
+ if (skillRes.written.length > 0) {
9537
+ sp.stop(`Agentbox Skills: Installed in ${String(skillRes.written.length)} locations`);
9538
+ } else {
9539
+ sp.stop(`Agentbox Skills: nothing to write (${String(skillRes.skipped)} skipped)`);
9540
+ }
9541
+ if (skillRes.blocked.length > 0) {
9542
+ log30.warn(
9543
+ `user-modified host skill file(s) left in place: ${skillRes.blocked.join(", ")}
9544
+ pass \`agentbox install --skills-only --force\` to overwrite`
9545
+ );
9546
+ }
9547
+ } catch (err) {
9548
+ sp.stop("Agentbox Skills: failed");
9549
+ log30.warn(err instanceof Error ? err.message : String(err));
9550
+ }
9551
+ markSetupComplete(providerName);
9552
+ const providerGroup = await runProviderChecks(providerName);
9553
+ process.stdout.write(" " + formatCompact([sysGroup, providerGroup]) + "\n");
9554
+ note(tutorialBody(providerName), "Next steps");
9555
+ outro6(
9556
+ opts.fromAutoTrigger ? "\u2728 Setup complete \u2014 continuing with your command\u2026" : "\u2728 Setup complete"
9557
+ );
9558
+ return true;
9559
+ }
9560
+ var installCommand = new Command25("install").description(
9561
+ "Interactive setup wizard: system check, pick a provider, log in, prepare its base image/snapshot, and install the host /agentbox skill. `--skills-only` runs just the skill install."
9562
+ ).option(
9563
+ "--skills-only",
9564
+ "only install the host /agentbox skill files (no wizard, no login, no prepare)"
9565
+ ).option("--force", "overwrite existing skill files even if not AgentBox-managed").option("--dry-run", "print what would be written without changing anything").option(
9566
+ "-p, --provider <name>",
9567
+ "pre-select the provider to set up (docker | daytona | hetzner | vercel)"
9568
+ ).option("-y, --yes", "auto-confirm the prepare step").action(async (opts) => {
9569
+ if (opts.skillsOnly) {
9570
+ intro6("Installing AgentBox host commands...");
9571
+ let res;
9572
+ try {
9573
+ res = installHostSkills({ force: opts.force, dryRun: opts.dryRun });
9574
+ } catch (err) {
9575
+ log30.error(err instanceof Error ? err.message : String(err));
9576
+ process.exit(1);
9577
+ }
9578
+ if (opts.dryRun) {
9579
+ outro6(
9580
+ `dry-run: ${String(res.written.length)} file(s) would be written, ${String(res.skipped)} skipped`
9581
+ );
9582
+ return;
9583
+ }
9584
+ if (res.written.length === 0) {
9585
+ outro6(`nothing installed (${String(res.skipped)} skipped)`);
9586
+ return;
9587
+ }
9588
+ outro6(`installed: ${res.written.join(", ")}`);
8263
9589
  return;
8264
9590
  }
8265
- outro6(`installed: ${written.join(", ")}`);
9591
+ const ok = await runInstallWizard({
9592
+ provider: opts.provider,
9593
+ yes: opts.yes,
9594
+ force: opts.force,
9595
+ dryRun: opts.dryRun
9596
+ });
9597
+ if (!ok) process.exit(1);
9598
+ });
9599
+
9600
+ // src/commands/doctor.ts
9601
+ import { Command as Command26 } from "commander";
9602
+ var doctorCommand = new Command26("doctor").description(
9603
+ "Diagnose system compatibility and provider readiness (Node, git, ssh, Docker daemon, provider credentials, prepared snapshots)."
9604
+ ).option(
9605
+ "-p, --provider <name>",
9606
+ "limit checks to one provider (docker | daytona | hetzner | vercel)"
9607
+ ).action(async (opts) => {
9608
+ let groups;
9609
+ if (opts.provider) {
9610
+ const name = opts.provider.trim();
9611
+ if (!isKnownProvider(name)) {
9612
+ process.stderr.write(
9613
+ "error: --provider must be one of: docker, daytona, hetzner, vercel\n"
9614
+ );
9615
+ process.exit(1);
9616
+ }
9617
+ groups = [
9618
+ { title: "system", results: await runSystemChecks() },
9619
+ await runProviderChecks(name)
9620
+ ];
9621
+ } else {
9622
+ groups = await runAllChecks();
9623
+ }
9624
+ process.stdout.write(formatDetailed(groups).join("\n") + "\n");
9625
+ const worst = worstStatus(groups);
9626
+ if (worst === "fail") {
9627
+ process.stdout.write(
9628
+ "\nOne or more required checks failed. Fix the FAIL items above before continuing.\n"
9629
+ );
9630
+ process.exit(1);
9631
+ }
9632
+ if (worst === "warn") {
9633
+ process.stdout.write(
9634
+ "\nWarnings are providers that need setup. Run `agentbox install` to configure one,\nor `agentbox prepare --status` to see remote snapshot inventory.\n"
9635
+ );
9636
+ } else {
9637
+ process.stdout.write("\nAll checks passed.\n");
9638
+ }
8266
9639
  });
8267
9640
 
8268
9641
  // src/commands/git.ts
8269
- import { Command as Command25 } from "commander";
9642
+ import { Command as Command27 } from "commander";
8270
9643
  var WORKSPACE = "/workspace";
8271
9644
  var TOKEN_TTL_MS = 12e4;
8272
- async function runInBox(box, argv) {
9645
+ async function runInBox(box, argv2) {
8273
9646
  const provider = await providerForBox(box);
8274
- return provider.exec(box, argv, { cwd: WORKSPACE });
9647
+ return provider.exec(box, argv2, { cwd: WORKSPACE });
8275
9648
  }
8276
- async function runAndStream(box, argv) {
8277
- const r = await runInBox(box, argv);
9649
+ async function runAndStream(box, argv2) {
9650
+ const r = await runInBox(box, argv2);
8278
9651
  if (r.stdout) process.stdout.write(r.stdout);
8279
9652
  if (r.stderr) process.stderr.write(r.stderr);
8280
9653
  return r.exitCode;
@@ -8298,33 +9671,33 @@ function buildPredictedGhPrParams(ghArgs) {
8298
9671
  async function exitWith(code) {
8299
9672
  process.exit(code);
8300
9673
  }
8301
- var pushCommand = new Command25("push").description("Push the box's branch via the host relay (host creds, no prompt)").argument("<box>", "box ref: project index, id, id prefix, name, or container").argument("[args...]", "extra flags forwarded to `agentbox-ctl git push` (e.g. --force-with-lease, --tags)").option("--remote <name>", "remote name (default: origin)").allowExcessArguments(true).allowUnknownOption(true).action(async (boxRef, args, opts) => {
9674
+ var pushCommand = new Command27("push").description("Push the box's branch via the host relay (host creds, no prompt)").argument("<box>", "box ref: project index, id, id prefix, name, or container").argument("[args...]", "extra flags forwarded to `agentbox-ctl git push` (e.g. --force-with-lease, --tags)").option("--remote <name>", "remote name (default: origin)").allowExcessArguments(true).allowUnknownOption(true).action(async (boxRef, args, opts) => {
8302
9675
  try {
8303
9676
  const box = await resolveBoxOrExit(boxRef);
8304
9677
  const predicted = buildPredictedGitParams(opts.remote, args);
8305
9678
  const tokenArgs = await hostInitiatedArgs(box.id, "git.push", predicted);
8306
- const argv = ["agentbox-ctl", "git", "push", ...tokenArgs];
8307
- if (opts.remote) argv.push("--remote", opts.remote);
8308
- argv.push(...args);
8309
- await exitWith(await runAndStream(box, argv));
9679
+ const argv2 = ["agentbox-ctl", "git", "push", ...tokenArgs];
9680
+ if (opts.remote) argv2.push("--remote", opts.remote);
9681
+ argv2.push(...args);
9682
+ await exitWith(await runAndStream(box, argv2));
8310
9683
  } catch (err) {
8311
9684
  handleLifecycleError(err);
8312
9685
  }
8313
9686
  });
8314
- var fetchCommand = new Command25("fetch").description("Fetch via the host relay (refs land in the shared .git)").argument("<box>", "box ref").argument("[args...]", "extra flags forwarded to `agentbox-ctl git fetch` (e.g. --prune)").option("--remote <name>", "remote name (default: origin)").allowExcessArguments(true).allowUnknownOption(true).action(async (boxRef, args, opts) => {
9687
+ var fetchCommand = new Command27("fetch").description("Fetch via the host relay (refs land in the shared .git)").argument("<box>", "box ref").argument("[args...]", "extra flags forwarded to `agentbox-ctl git fetch` (e.g. --prune)").option("--remote <name>", "remote name (default: origin)").allowExcessArguments(true).allowUnknownOption(true).action(async (boxRef, args, opts) => {
8315
9688
  try {
8316
9689
  const box = await resolveBoxOrExit(boxRef);
8317
9690
  const predicted = buildPredictedGitParams(opts.remote, args);
8318
9691
  const tokenArgs = await hostInitiatedArgs(box.id, "git.fetch", predicted);
8319
- const argv = ["agentbox-ctl", "git", "fetch", ...tokenArgs];
8320
- if (opts.remote) argv.push("--remote", opts.remote);
8321
- argv.push(...args);
8322
- await exitWith(await runAndStream(box, argv));
9692
+ const argv2 = ["agentbox-ctl", "git", "fetch", ...tokenArgs];
9693
+ if (opts.remote) argv2.push("--remote", opts.remote);
9694
+ argv2.push(...args);
9695
+ await exitWith(await runAndStream(box, argv2));
8323
9696
  } catch (err) {
8324
9697
  handleLifecycleError(err);
8325
9698
  }
8326
9699
  });
8327
- var pullCommand = new Command25("pull").description(
9700
+ var pullCommand = new Command27("pull").description(
8328
9701
  "Fetch via the relay then merge in /workspace. With <branch>: first `git checkout <branch>` so the box switches base branch and pulls latest \u2014 useful for reusing a box on a new task."
8329
9702
  ).argument("<box>", "box ref").argument("[branch]", "optional branch to switch to before pulling (e.g. main)").argument("[args...]", "extra flags forwarded to `agentbox-ctl git pull`").option("--remote <name>", "remote name (default: origin)").option("--ff-only", "pass --ff-only to the in-box merge").allowExcessArguments(true).allowUnknownOption(true).action(
8330
9703
  async (boxRef, branch, args, opts) => {
@@ -8336,17 +9709,17 @@ var pullCommand = new Command25("pull").description(
8336
9709
  }
8337
9710
  const predicted = buildPredictedGitParams(opts.remote, args);
8338
9711
  const tokenArgs = await hostInitiatedArgs(box.id, "git.fetch", predicted);
8339
- const argv = ["agentbox-ctl", "git", "pull", ...tokenArgs];
8340
- if (opts.remote) argv.push("--remote", opts.remote);
8341
- if (opts.ffOnly) argv.push("--ff-only");
8342
- argv.push(...args);
8343
- await exitWith(await runAndStream(box, argv));
9712
+ const argv2 = ["agentbox-ctl", "git", "pull", ...tokenArgs];
9713
+ if (opts.remote) argv2.push("--remote", opts.remote);
9714
+ if (opts.ffOnly) argv2.push("--ff-only");
9715
+ argv2.push(...args);
9716
+ await exitWith(await runAndStream(box, argv2));
8344
9717
  } catch (err) {
8345
9718
  handleLifecycleError(err);
8346
9719
  }
8347
9720
  }
8348
9721
  );
8349
- var checkoutCommand = new Command25("checkout").description("Change the box's working branch (runs `git checkout <branch>` in /workspace)").argument("<box>", "box ref").argument("<branch>", "branch to check out inside the box").argument("[args...]", "extra flags forwarded to `git checkout`").allowExcessArguments(true).allowUnknownOption(true).action(async (boxRef, branch, args) => {
9722
+ var checkoutCommand = new Command27("checkout").description("Change the box's working branch (runs `git checkout <branch>` in /workspace)").argument("<box>", "box ref").argument("<branch>", "branch to check out inside the box").argument("[args...]", "extra flags forwarded to `git checkout`").allowExcessArguments(true).allowUnknownOption(true).action(async (boxRef, branch, args) => {
8350
9723
  try {
8351
9724
  const box = await resolveBoxOrExit(boxRef);
8352
9725
  await exitWith(await runAndStream(box, ["git", "checkout", branch, ...args]));
@@ -8354,7 +9727,7 @@ var checkoutCommand = new Command25("checkout").description("Change the box's wo
8354
9727
  handleLifecycleError(err);
8355
9728
  }
8356
9729
  });
8357
- var statusCommand = new Command25("status").description("Run `git status` in the box's /workspace (read-only, no relay)").argument("<box>", "box ref").argument("[args...]", "extra flags forwarded to `git status`").allowExcessArguments(true).allowUnknownOption(true).action(async (boxRef, args) => {
9730
+ var statusCommand = new Command27("status").description("Run `git status` in the box's /workspace (read-only, no relay)").argument("<box>", "box ref").argument("[args...]", "extra flags forwarded to `git status`").allowExcessArguments(true).allowUnknownOption(true).action(async (boxRef, args) => {
8358
9731
  try {
8359
9732
  const box = await resolveBoxOrExit(boxRef);
8360
9733
  await exitWith(await runAndStream(box, ["git", "status", ...args]));
@@ -8378,7 +9751,7 @@ function injectPrCreateHead2(op, box, args) {
8378
9751
  return injectPrCreateHead(op, rootWt?.branch, args);
8379
9752
  }
8380
9753
  function buildPrSubcommand(op) {
8381
- return new Command25(op).description(PR_OP_DESCRIPTIONS[op]).argument("<box>", "box ref").argument(
9754
+ return new Command27(op).description(PR_OP_DESCRIPTIONS[op]).argument("<box>", "box ref").argument(
8382
9755
  "[args...]",
8383
9756
  "extra flags forwarded to `gh pr <op>` (e.g. --title, --body, --label, --draft, --json)"
8384
9757
  ).allowExcessArguments(true).allowUnknownOption(true).action(async (boxRef, args) => {
@@ -8387,25 +9760,25 @@ function buildPrSubcommand(op) {
8387
9760
  const ghArgs = injectPrCreateHead2(op, box, args);
8388
9761
  const predicted = buildPredictedGhPrParams(ghArgs);
8389
9762
  const tokenArgs = await hostInitiatedArgs(box.id, `gh.pr.${op}`, predicted);
8390
- const argv = ["agentbox-ctl", "gh", "pr", op, ...tokenArgs, ...ghArgs];
8391
- await exitWith(await runAndStream(box, argv));
9763
+ const argv2 = ["agentbox-ctl", "gh", "pr", op, ...tokenArgs, ...ghArgs];
9764
+ await exitWith(await runAndStream(box, argv2));
8392
9765
  } catch (err) {
8393
9766
  handleLifecycleError(err);
8394
9767
  }
8395
9768
  });
8396
9769
  }
8397
- var prCommand = new Command25("pr").description(
9770
+ var prCommand = new Command27("pr").description(
8398
9771
  "PR operations against a box's branch via the host `gh` CLI"
8399
9772
  );
8400
9773
  for (const op of GH_PR_OPS) {
8401
9774
  const sub = buildPrSubcommand(op);
8402
9775
  prCommand.addCommand(sub, op === "create" ? { isDefault: true } : void 0);
8403
9776
  }
8404
- var gitCommand = new Command25("git").description("Run git / gh pr operations against a box from the host").addCommand(pushCommand).addCommand(fetchCommand).addCommand(pullCommand).addCommand(checkoutCommand).addCommand(statusCommand).addCommand(prCommand);
9777
+ var gitCommand = new Command27("git").description("Run git / gh pr operations against a box from the host").addCommand(pushCommand).addCommand(fetchCommand).addCommand(pullCommand).addCommand(checkoutCommand).addCommand(statusCommand).addCommand(prCommand);
8405
9778
 
8406
9779
  // src/commands/list.ts
8407
- import { log as log30 } from "@clack/prompts";
8408
- import { Command as Command26 } from "commander";
9780
+ import { log as log31 } from "@clack/prompts";
9781
+ import { Command as Command28 } from "commander";
8409
9782
  import { pathToFileURL } from "url";
8410
9783
 
8411
9784
  // src/hyperlink.ts
@@ -8418,6 +9791,38 @@ function hyperlink(label, url, stream) {
8418
9791
  return `${ESC2}]8;;${url}${ST}${label}${ESC2}]8;;${ST}`;
8419
9792
  }
8420
9793
 
9794
+ // src/lib/cloud-state.ts
9795
+ var PROBE_TIMEOUT_MS = 4e3;
9796
+ async function applyLiveCloudStates(boxes) {
9797
+ await Promise.all(
9798
+ boxes.map(async (b) => {
9799
+ if (!b.provider || b.provider === "docker") return;
9800
+ try {
9801
+ const provider = await providerForBox(b);
9802
+ const state = await withTimeout(provider.probeState(b), PROBE_TIMEOUT_MS);
9803
+ if (state !== null) b.state = state;
9804
+ } catch {
9805
+ }
9806
+ })
9807
+ );
9808
+ }
9809
+ function withTimeout(p, ms) {
9810
+ return new Promise((resolve3) => {
9811
+ const t = setTimeout(() => resolve3(null), ms);
9812
+ if (typeof t.unref === "function") t.unref();
9813
+ p.then(
9814
+ (v) => {
9815
+ clearTimeout(t);
9816
+ resolve3(v);
9817
+ },
9818
+ () => {
9819
+ clearTimeout(t);
9820
+ resolve3(null);
9821
+ }
9822
+ );
9823
+ });
9824
+ }
9825
+
8421
9826
  // src/watch.ts
8422
9827
  function withWatchOptions(cmd) {
8423
9828
  return cmd.option("-w, --watch", "redraw continuously until interrupted (Ctrl-C)").option("--interval <seconds>", "refresh interval for --watch", "2");
@@ -8433,7 +9838,7 @@ async function watchRender(produce, rawInterval) {
8433
9838
  process.stdout.write("\x1B[?25l");
8434
9839
  process.once("exit", () => process.stdout.write("\x1B[?25h"));
8435
9840
  process.once("SIGINT", () => process.exit(0));
8436
- const sleep4 = (d) => new Promise((r) => setTimeout(r, d));
9841
+ const sleep5 = (d) => new Promise((r) => setTimeout(r, d));
8437
9842
  for (; ; ) {
8438
9843
  let body;
8439
9844
  try {
@@ -8449,7 +9854,7 @@ async function watchRender(produce, rawInterval) {
8449
9854
  ${body.replace(/\n+$/, "")}
8450
9855
  `
8451
9856
  );
8452
- await sleep4(ms);
9857
+ await sleep5(ms);
8453
9858
  }
8454
9859
  }
8455
9860
 
@@ -8501,6 +9906,7 @@ function workspaceCell(path, target, stream) {
8501
9906
  return { text: hyperlink(display, url, stream), width: display.length };
8502
9907
  }
8503
9908
  function agentSummary(b) {
9909
+ if (b.state !== "running") return "-";
8504
9910
  const agents = [];
8505
9911
  if (b.claudeActivity && b.claudeActivity !== "unknown") {
8506
9912
  agents.push(`claude:${b.claudeActivity}`);
@@ -8554,14 +9960,19 @@ function renderTable(boxes, stream) {
8554
9960
  (row2) => row2.map((cell, i) => padCell(cell ?? plain(""), i)).join(" ").trimEnd()
8555
9961
  ).join("\n");
8556
9962
  }
8557
- async function scopedBoxes(all) {
9963
+ async function scopedBoxes(all, live) {
8558
9964
  const boxes = await listBoxes();
8559
- if (all) return { boxes, projectRoot: "", scoped: false };
9965
+ if (all) {
9966
+ if (live) await applyLiveCloudStates(boxes);
9967
+ return { boxes, projectRoot: "", scoped: false };
9968
+ }
8560
9969
  const { root } = await findProjectRoot(process.cwd());
8561
- return { boxes: boxes.filter((b) => b.projectRoot === root), projectRoot: root, scoped: true };
9970
+ const scoped2 = boxes.filter((b) => b.projectRoot === root);
9971
+ if (live) await applyLiveCloudStates(scoped2);
9972
+ return { boxes: scoped2, projectRoot: root, scoped: true };
8562
9973
  }
8563
- async function buildListText(all) {
8564
- const { boxes, projectRoot, scoped: scoped2 } = await scopedBoxes(all);
9974
+ async function buildListText(all, live) {
9975
+ const { boxes, projectRoot, scoped: scoped2 } = await scopedBoxes(all, live);
8565
9976
  if (boxes.length === 0) {
8566
9977
  if (scoped2) {
8567
9978
  return `no boxes in this project (${projectRoot}) \u2014 run \`agentbox create\`, or \`agentbox list --global\` to see all`;
@@ -8575,31 +9986,35 @@ async function buildListText(all) {
8575
9986
  ${table}`;
8576
9987
  }
8577
9988
  var listCommand2 = withWatchOptions(
8578
- new Command26("list").alias("ls").description("List agent boxes in the current project (-g for all)").option("-j, --json", "machine-readable JSON output").option("-g, --global", "include boxes from all projects")
9989
+ new Command28("list").alias("ls").description("List agent boxes in the current project (-g for all)").option("-j, --json", "machine-readable JSON output").option("-g, --global", "include boxes from all projects").option(
9990
+ "--live",
9991
+ "probe live cloud state via the provider SDK (slower; default: last host-known state)"
9992
+ )
8579
9993
  ).action(async (opts) => {
8580
9994
  if (opts.json && opts.watch) {
8581
- log30.error("cannot combine --json with --watch");
9995
+ log31.error("cannot combine --json with --watch");
8582
9996
  process.exit(2);
8583
9997
  }
8584
9998
  const all = opts.global ?? false;
9999
+ const live = opts.live ?? false;
8585
10000
  if (opts.watch) {
8586
- await watchRender(() => buildListText(all), opts.interval);
10001
+ await watchRender(() => buildListText(all, live), opts.interval);
8587
10002
  return;
8588
10003
  }
8589
10004
  if (opts.json) {
8590
- const { boxes } = await scopedBoxes(all);
10005
+ const { boxes } = await scopedBoxes(all, live);
8591
10006
  process.stdout.write(JSON.stringify(boxes, null, 2) + "\n");
8592
10007
  return;
8593
10008
  }
8594
- process.stdout.write(await buildListText(all) + "\n");
10009
+ process.stdout.write(await buildListText(all, live) + "\n");
8595
10010
  });
8596
10011
 
8597
10012
  // src/commands/logs.ts
8598
- import { log as log31 } from "@clack/prompts";
8599
- import { Command as Command27 } from "commander";
10013
+ import { log as log32 } from "@clack/prompts";
10014
+ import { Command as Command29 } from "commander";
8600
10015
  import { spawn as spawn3 } from "child_process";
8601
10016
  var DAEMON_LOG_PATH = "/var/log/agentbox/ctl-daemon.log";
8602
- var logsCommand = new Command27("logs").description("Print recent log lines from a box service; -f to stream").argument(
10017
+ var logsCommand = new Command29("logs").description("Print recent log lines from a box service; -f to stream").argument(
8603
10018
  "[box]",
8604
10019
  "box ref (optional when cwd has exactly 1 box): project index, id, id prefix, name, or container"
8605
10020
  ).argument("[service]", "service name from agentbox.yaml").option("-n, --tail <n>", "how many recent lines to print first", "200").option("-f, --follow", "keep the connection open and stream new lines").option(
@@ -8617,9 +10032,9 @@ var logsCommand = new Command27("logs").description("Print recent log lines from
8617
10032
  service = boxArg;
8618
10033
  }
8619
10034
  if (!service && !opts.daemon) {
8620
- log31.error("missing <service> argument");
8621
- log31.info("usage: agentbox logs [box] <service> [-n N] [-f]");
8622
- log31.info(" agentbox logs [box] --daemon [-n N] [-f]");
10035
+ log32.error("missing <service> argument");
10036
+ log32.info("usage: agentbox logs [box] <service> [-n N] [-f]");
10037
+ log32.info(" agentbox logs [box] --daemon [-n N] [-f]");
8623
10038
  process.exit(2);
8624
10039
  }
8625
10040
  const box = await resolveBoxOrExit(idOrName);
@@ -8630,7 +10045,7 @@ var logsCommand = new Command27("logs").description("Print recent log lines from
8630
10045
  if (!opts.follow) {
8631
10046
  const proc = await provider.exec(box, args, { user: "vscode" });
8632
10047
  if (proc.exitCode !== 0) {
8633
- log31.error(
10048
+ log32.error(
8634
10049
  `${opts.daemon ? "daemon log" : "agentbox-ctl logs"} failed: ${proc.stderr || proc.stdout}`
8635
10050
  );
8636
10051
  process.exit(1);
@@ -8686,12 +10101,12 @@ var logsCommand = new Command27("logs").description("Print recent log lines from
8686
10101
  });
8687
10102
 
8688
10103
  // src/commands/open.ts
8689
- import { log as log32 } from "@clack/prompts";
8690
- import { execa as execa2 } from "execa";
8691
- import { existsSync as existsSync6, mkdirSync as mkdirSync4 } from "fs";
8692
- import { homedir as homedir10 } from "os";
8693
- import { join as join12 } from "path";
8694
- import { Command as Command28 } from "commander";
10104
+ import { log as log33 } from "@clack/prompts";
10105
+ import { execa as execa3 } from "execa";
10106
+ import { existsSync as existsSync7, mkdirSync as mkdirSync6 } from "fs";
10107
+ import { homedir as homedir14 } from "os";
10108
+ import { join as join16 } from "path";
10109
+ import { Command as Command30 } from "commander";
8695
10110
 
8696
10111
  // src/commands/path.ts
8697
10112
  async function runPath(box, opts) {
@@ -8713,7 +10128,7 @@ async function runPath(box, opts) {
8713
10128
  }
8714
10129
 
8715
10130
  // src/commands/open.ts
8716
- var openCommand = new Command28("open").description("Open a box's /workspace in Finder (docker: rsync'd snapshot; cloud: sshfs mount)").argument(
10131
+ var openCommand = new Command30("open").description("Open a box's /workspace in Finder (docker: rsync'd snapshot; cloud: sshfs mount)").argument(
8717
10132
  "[box]",
8718
10133
  "box ref: project index, id, id prefix, name, or container (default: the only box in this project)"
8719
10134
  ).option("--no-refresh", "skip the rsync; open whatever's already on disk (docker only)").option(
@@ -8752,7 +10167,7 @@ var openCommand = new Command28("open").description("Open a box's /workspace in
8752
10167
  }
8753
10168
  });
8754
10169
  async function runCloudOpen(box, provider, opts) {
8755
- const mountRoot = join12(homedir10(), ".agentbox", "mounts", box.name);
10170
+ const mountRoot = join16(homedir14(), ".agentbox", "mounts", box.name);
8756
10171
  if (opts.unmount) {
8757
10172
  const ok = await tryUnmount(mountRoot);
8758
10173
  if (ok) process.stdout.write(`unmounted ${mountRoot}
@@ -8789,14 +10204,14 @@ async function runCloudOpen(box, provider, opts) {
8789
10204
  user: target.user,
8790
10205
  identityFile: target.identityFile
8791
10206
  });
8792
- if (!existsSync6(mountRoot)) {
8793
- mkdirSync4(mountRoot, { recursive: true, mode: 493 });
10207
+ if (!existsSync7(mountRoot)) {
10208
+ mkdirSync6(mountRoot, { recursive: true, mode: 493 });
8794
10209
  } else if (await isMounted(mountRoot)) {
8795
- log32.info(`re-mounting (stale mount detected at ${mountRoot})`);
10210
+ log33.info(`re-mounting (stale mount detected at ${mountRoot})`);
8796
10211
  await tryUnmount(mountRoot);
8797
10212
  }
8798
- log32.info(`mounting ${alias}:/workspace at ${mountRoot}`);
8799
- const mount = await execa2(
10213
+ log33.info(`mounting ${alias}:/workspace at ${mountRoot}`);
10214
+ const mount = await execa3(
8800
10215
  sshfsBin,
8801
10216
  [
8802
10217
  `${alias}:/workspace`,
@@ -8815,35 +10230,35 @@ async function runCloudOpen(box, provider, opts) {
8815
10230
  if (mount.exitCode !== 0) {
8816
10231
  throw new Error(`sshfs mount failed (exit ${String(mount.exitCode)}): ${mount.stderr || mount.stdout}`);
8817
10232
  }
8818
- await execa2("open", [mountRoot], { reject: false });
10233
+ await execa3("open", [mountRoot], { reject: false });
8819
10234
  process.stdout.write(`opened ${mountRoot}
8820
10235
  `);
8821
10236
  process.stdout.write(`unmount later with: agentbox open ${box.name} --unmount
8822
10237
  `);
8823
10238
  }
8824
10239
  async function locateBinary(name) {
8825
- const r = await execa2("which", [name], { reject: false });
10240
+ const r = await execa3("which", [name], { reject: false });
8826
10241
  if (r.exitCode !== 0) return null;
8827
10242
  const path = (r.stdout ?? "").trim();
8828
10243
  return path.length > 0 ? path : null;
8829
10244
  }
8830
10245
  async function isMounted(path) {
8831
- const r = await execa2("sh", ["-c", `mount | grep -F " on ${path} "`], { reject: false });
10246
+ const r = await execa3("sh", ["-c", `mount | grep -F " on ${path} "`], { reject: false });
8832
10247
  return r.exitCode === 0;
8833
10248
  }
8834
10249
  async function tryUnmount(path) {
8835
10250
  if (await isMounted(path)) {
8836
- const u = await execa2("umount", [path], { reject: false });
10251
+ const u = await execa3("umount", [path], { reject: false });
8837
10252
  if (u.exitCode === 0) return true;
8838
- const d = await execa2("diskutil", ["unmount", path], { reject: false });
10253
+ const d = await execa3("diskutil", ["unmount", path], { reject: false });
8839
10254
  return d.exitCode === 0;
8840
10255
  }
8841
10256
  return false;
8842
10257
  }
8843
10258
 
8844
10259
  // src/commands/pause.ts
8845
- import { Command as Command29 } from "commander";
8846
- var pauseCommand = new Command29("pause").description(
10260
+ import { Command as Command31 } from "commander";
10261
+ var pauseCommand = new Command31("pause").description(
8847
10262
  "Pause a box. Docker: `docker pause` (cgroup freeze \u2014 sub-second resume). Cloud: backend.pause (Daytona archive \u2014 cold storage; resume is slower but uses no quota while archived)."
8848
10263
  ).argument(
8849
10264
  "[box]",
@@ -8865,220 +10280,9 @@ var pauseCommand = new Command29("pause").description(
8865
10280
  }
8866
10281
  });
8867
10282
 
8868
- // src/commands/prepare.ts
8869
- import { intro as intro6, log as log33, spinner as spinner7 } from "@clack/prompts";
8870
- import { Command as Command30 } from "commander";
8871
- async function dockerStatus() {
8872
- let img;
8873
- try {
8874
- img = await imageInfo(DEFAULT_BOX_IMAGE);
8875
- } catch {
8876
- return { daemon: "unreachable", volumes: [] };
8877
- }
8878
- const names = [SHARED_CLAUDE_VOLUME, SHARED_CODEX_VOLUME, SHARED_OPENCODE_VOLUME];
8879
- const volumes = await Promise.all(
8880
- names.map(async (name) => ({ name, exists: await volumeExists(name).catch(() => false) }))
8881
- );
8882
- return { daemon: "reachable", image: img, volumes };
8883
- }
8884
- function humanBytes(n) {
8885
- if (n === void 0 || !Number.isFinite(n)) return "\u2014";
8886
- if (n >= 1024 ** 3) return `${(n / 1024 ** 3).toFixed(2)} GB`;
8887
- if (n >= 1024 ** 2) return `${(n / 1024 ** 2).toFixed(1)} MB`;
8888
- if (n >= 1024) return `${(n / 1024).toFixed(1)} KB`;
8889
- return `${String(n)} B`;
8890
- }
8891
- function humanAge(iso) {
8892
- if (!iso) return "\u2014";
8893
- const t = Date.parse(iso);
8894
- if (!Number.isFinite(t)) return iso;
8895
- const ageSec = Math.max(0, (Date.now() - t) / 1e3);
8896
- if (ageSec < 60) return `${ageSec.toFixed(0)}s ago`;
8897
- if (ageSec < 3600) return `${(ageSec / 60).toFixed(0)}m ago`;
8898
- if (ageSec < 86400) return `${(ageSec / 3600).toFixed(1)}h ago`;
8899
- return `${(ageSec / 86400).toFixed(1)}d ago`;
8900
- }
8901
- function pad2(s, width) {
8902
- return s.length >= width ? s : s + " ".repeat(width - s.length);
8903
- }
8904
- async function renderDocker(status) {
8905
- const out = ["docker:"];
8906
- if (status.daemon === "unreachable") {
8907
- out.push(" docker daemon unreachable (is Docker running?)");
8908
- return out;
8909
- }
8910
- if (!status.image?.exists) {
8911
- out.push(` image ${DEFAULT_BOX_IMAGE} (not built \u2014 run \`agentbox prepare --provider docker\`)`);
8912
- } else {
8913
- out.push(
8914
- ` image ${pad2(DEFAULT_BOX_IMAGE, 30)} ${pad2(humanBytes(status.image.sizeBytes), 10)} built ${humanAge(status.image.createdAt)}`
8915
- );
8916
- }
8917
- for (const v of status.volumes) {
8918
- if (v.exists) {
8919
- out.push(` vol ${pad2(v.name, 30)} present`);
8920
- } else {
8921
- out.push(` vol ${pad2(v.name, 30)} (none \u2014 seeded lazily on first \`agentbox claude/codex/opencode\`)`);
8922
- }
8923
- }
8924
- return out;
8925
- }
8926
- async function daytonaStatus() {
8927
- try {
8928
- const mod = await import("./dist-CX5CGVEB.js");
8929
- return await mod.getDaytonaStatus();
8930
- } catch (err) {
8931
- return { configured: false, reason: err instanceof Error ? err.message.split("\n")[0] : String(err) };
8932
- }
8933
- }
8934
- function renderDaytona(status, pinnedImage) {
8935
- const out = ["daytona:"];
8936
- if (!status.configured) {
8937
- out.push(
8938
- ` (not configured \u2014 \`agentbox daytona login\` to set up${status.reason ? `; ${status.reason}` : ""})`
8939
- );
8940
- return out;
8941
- }
8942
- if (status.reason) out.push(` warn: ${status.reason}`);
8943
- if (status.snapshots.length === 0) {
8944
- out.push(" no agentbox snapshots \u2014 run `agentbox prepare --provider daytona`");
8945
- } else {
8946
- for (const s of status.snapshots) {
8947
- const sizeStr = s.sizeGb !== void 0 ? `${s.sizeGb.toFixed(2)} GB` : "\u2014";
8948
- const pinned = pinnedImage && pinnedImage === s.name ? " (pinned in project)" : "";
8949
- const tail = s.state === "error" && s.errorReason ? ` error: ${s.errorReason.slice(0, 80)}` : ` ${humanAge(s.createdAt)}`;
8950
- out.push(
8951
- ` snap ${pad2(s.name, 40)} ${pad2(s.state ?? "\u2014", 10)} ${pad2(sizeStr, 10)}${tail}${pinned}`
8952
- );
8953
- }
8954
- }
8955
- if (status.volumes.length === 0) {
8956
- out.push(" no agentbox volumes \u2014 created lazily on first cloud `agentbox create`");
8957
- } else {
8958
- for (const v of status.volumes) {
8959
- const last = v.lastUsedAt ? ` last used ${humanAge(v.lastUsedAt)}` : "";
8960
- out.push(` vol ${pad2(v.name, 40)} ${pad2(v.state ?? "\u2014", 10)}${last}`);
8961
- }
8962
- }
8963
- return out;
8964
- }
8965
- async function showStatus(opts) {
8966
- const cfg = await loadEffectiveConfig(process.cwd()).catch(() => null);
8967
- const pinnedRaw = cfg?.effective.box.image;
8968
- const pinned = typeof pinnedRaw === "string" && pinnedRaw.length > 0 && pinnedRaw !== DEFAULT_BOX_IMAGE ? pinnedRaw : void 0;
8969
- const lines = [];
8970
- const wantDocker = !opts.onlyProvider || opts.onlyProvider === "docker";
8971
- const wantDaytona = !opts.onlyProvider || opts.onlyProvider === "daytona";
8972
- if (wantDocker) {
8973
- const status = await dockerStatus();
8974
- lines.push(...await renderDocker(status));
8975
- }
8976
- if (wantDaytona) {
8977
- if (lines.length > 0) lines.push("");
8978
- const status = await daytonaStatus();
8979
- lines.push(...renderDaytona(status, pinned));
8980
- }
8981
- if (pinned) {
8982
- lines.push("");
8983
- lines.push(`project pin: box.image = ${pinned}`);
8984
- }
8985
- process.stdout.write(lines.join("\n") + "\n");
8986
- }
8987
- var prepareCommand = new Command30("prepare").description(
8988
- "Build base sandbox images / snapshots, or show what is already prepared across providers."
8989
- ).option(
8990
- "-p, --provider <name>",
8991
- "provider to prepare (docker | daytona | hetzner | vercel). Omit for status-only."
8992
- ).option(
8993
- "-n, --name <name>",
8994
- "snapshot name (Daytona only; default: agentbox-base-<timestamp>)"
8995
- ).option("-f, --force", "rebuild even if the image / snapshot already exists").option("-y, --yes", "skip confirmation prompts (cost / time warnings)").option("--status", "show status without preparing anything").action(async (opts) => {
8996
- if (!opts.provider || opts.status) {
8997
- await showStatus({});
8998
- return;
8999
- }
9000
- const providerName = opts.provider.trim();
9001
- if (!isKnownProvider(providerName)) {
9002
- process.stderr.write(
9003
- `error: --provider must be one of: docker, daytona, hetzner
9004
- `
9005
- );
9006
- process.exit(1);
9007
- }
9008
- intro6(`preparing ${providerName} base image`);
9009
- if (providerName === "daytona" && !opts.yes && process.stdin.isTTY) {
9010
- process.stdout.write(
9011
- "This will trigger a Daytona image build (~7 min cold, ~seconds with cache) and register a named snapshot in your org.\nRe-run with --yes to skip this notice.\n"
9012
- );
9013
- }
9014
- const provider = await getProvider(providerName);
9015
- if (typeof provider.prepare !== "function") {
9016
- log33.error(`provider '${providerName}' does not implement prepare`);
9017
- process.exit(1);
9018
- }
9019
- const sp = spinner7();
9020
- sp.start(`preparing ${providerName}\u2026`);
9021
- try {
9022
- const result = await provider.prepare({
9023
- name: opts.name,
9024
- hostWorkspace: process.cwd(),
9025
- force: opts.force,
9026
- onLog: (line) => sp.message(line.slice(0, 80))
9027
- });
9028
- if (result.snapshotName !== void 0) {
9029
- sp.stop(`prepared ${providerName}: snapshot '${result.snapshotName}'`);
9030
- try {
9031
- const written = await setConfigValue(
9032
- "project",
9033
- "box.image",
9034
- result.snapshotName,
9035
- process.cwd()
9036
- );
9037
- log33.success(`box.image = ${result.snapshotName} (written to ${written.path})`);
9038
- } catch (err) {
9039
- const msg = err instanceof Error ? err.message : String(err);
9040
- log33.warn(
9041
- `prepared snapshot '${result.snapshotName}', but failed to pin it into the project config: ${msg}
9042
- Run \`agentbox config set --project box.image ${result.snapshotName}\` manually.`
9043
- );
9044
- }
9045
- } else {
9046
- sp.stop(`prepared ${providerName}`);
9047
- }
9048
- process.stdout.write("\n");
9049
- await showStatus({ onlyProvider: providerName });
9050
- log33.info(
9051
- "tip: install the agentbox host skill so Claude Code on this machine can drive AgentBox for you:\n npx skills add https://github.com/madarco/agentbox --skill agentbox"
9052
- );
9053
- } catch (err) {
9054
- sp.stop(`prepare failed: ${describeError(err)}`);
9055
- process.exit(1);
9056
- }
9057
- });
9058
- function describeError(err) {
9059
- if (!(err instanceof Error)) return String(err);
9060
- const parts = [err.message];
9061
- let cause = err.cause;
9062
- for (let i = 0; i < 5 && cause; i++) {
9063
- if (cause instanceof Error) {
9064
- parts.push(`caused by: ${cause.message}`);
9065
- const code = cause.code;
9066
- if (typeof code === "string") parts.push(`(${code})`);
9067
- cause = cause.cause;
9068
- } else if (typeof cause === "object") {
9069
- parts.push(`caused by: ${JSON.stringify(cause)}`);
9070
- break;
9071
- } else {
9072
- parts.push(`caused by: ${String(cause)}`);
9073
- break;
9074
- }
9075
- }
9076
- return parts.join(" \u2014 ");
9077
- }
9078
-
9079
10283
  // src/commands/prune.ts
9080
- import { confirm as confirm14, isCancel as isCancel15, log as log34 } from "@clack/prompts";
9081
- import { Command as Command31 } from "commander";
10284
+ import { confirm as confirm15, isCancel as isCancel16, log as log34 } from "@clack/prompts";
10285
+ import { Command as Command32 } from "commander";
9082
10286
  function totalRemovals(r, projectConfigs) {
9083
10287
  return r.removedRecords.length + r.removedContainers.length + r.removedVolumes.length + r.removedSnapshotDirs.length + r.removedBoxDirs.length + projectConfigs.length;
9084
10288
  }
@@ -9124,7 +10328,7 @@ async function liveProjectRoots() {
9124
10328
  return [];
9125
10329
  }
9126
10330
  }
9127
- var pruneCommand = new Command31("prune").description("Clean up orphan state.json records (and with --all, orphan docker resources)").option("--dry-run", "show what would be removed, don't change anything").option(
10331
+ var pruneCommand = new Command32("prune").description("Clean up orphan state.json records (and with --all, orphan docker resources)").option("--dry-run", "show what would be removed, don't change anything").option(
9128
10332
  "--all",
9129
10333
  "also remove orphan agentbox-* containers, volumes, snapshot dirs, and orphan per-project config dirs"
9130
10334
  ).option("-y, --yes", "skip the confirmation prompt").option(
@@ -9154,8 +10358,8 @@ var pruneCommand = new Command31("prune").description("Clean up orphan state.jso
9154
10358
  ${summary(preview, previewProjects)}`);
9155
10359
  if (dryRun) return;
9156
10360
  if (!opts.yes) {
9157
- const ok = await confirm14({ message: "Proceed with prune?", initialValue: true });
9158
- if (isCancel15(ok) || !ok) {
10361
+ const ok = await confirm15({ message: "Proceed with prune?", initialValue: true });
10362
+ if (isCancel16(ok) || !ok) {
9159
10363
  log34.info("cancelled");
9160
10364
  return;
9161
10365
  }
@@ -9173,19 +10377,9 @@ var CLOUD_PRUNE_PROVIDERS = ["daytona", "hetzner", "vercel"];
9173
10377
  function isCloudPruneProvider(name) {
9174
10378
  return CLOUD_PRUNE_PROVIDERS.includes(name);
9175
10379
  }
9176
- async function cloudBackendFor(provider) {
9177
- switch (provider) {
9178
- case "daytona":
9179
- return (await import("./dist-CX5CGVEB.js")).daytonaBackend;
9180
- case "hetzner":
9181
- return (await import("./dist-GDHP34ZK.js")).hetznerBackend;
9182
- case "vercel":
9183
- return (await import("./dist-XML54CNB.js")).vercelBackend;
9184
- }
9185
- }
9186
10380
  async function pruneCloud(provider, opts) {
9187
10381
  const dryRun = opts.dryRun ?? false;
9188
- const backend = await cloudBackendFor(provider);
10382
+ const backend = await cloudBackendForProvider(provider);
9189
10383
  if (!backend.list) {
9190
10384
  log34.error(`${provider} backend doesn't expose \`list()\`; cannot enumerate sandboxes for prune`);
9191
10385
  process.exit(2);
@@ -9218,11 +10412,11 @@ async function pruneCloud(provider, opts) {
9218
10412
  }
9219
10413
  if (dryRun) return;
9220
10414
  if (!opts.yes) {
9221
- const ok = await confirm14({
10415
+ const ok = await confirm15({
9222
10416
  message: `Delete ${String(orphans.length)} orphan sandbox(es)?`,
9223
10417
  initialValue: false
9224
10418
  });
9225
- if (isCancel15(ok) || !ok) {
10419
+ if (isCancel16(ok) || !ok) {
9226
10420
  log34.info("cancelled");
9227
10421
  return;
9228
10422
  }
@@ -9249,10 +10443,10 @@ async function pruneCloud(provider, opts) {
9249
10443
  // src/commands/queue.ts
9250
10444
  import { readFile as readFile4, stat as stat5 } from "fs/promises";
9251
10445
  import { intro as intro7, log as log35, outro as outro7 } from "@clack/prompts";
9252
- import { Command as Command32 } from "commander";
10446
+ import { Command as Command33 } from "commander";
9253
10447
  var TERMINAL_STATUSES = /* @__PURE__ */ new Set(["done", "failed", "cancelled"]);
9254
- var queueCommand = new Command32("queue").description("Inspect and manage background `agentbox claude|codex|opencode -i` jobs");
9255
- var queueListCommand = new Command32("list").description("List queued, running, and (with --all) terminal background jobs").option("--all", "include done/failed/cancelled jobs (default: hide terminal)").action(async (opts) => {
10448
+ var queueCommand = new Command33("queue").description("Inspect and manage background `agentbox claude|codex|opencode -i` jobs");
10449
+ var queueListCommand = new Command33("list").description("List queued, running, and (with --all) terminal background jobs").option("--all", "include done/failed/cancelled jobs (default: hide terminal)").action(async (opts) => {
9256
10450
  const jobs = await loadQueue();
9257
10451
  const cfg = await loadQueueConfig();
9258
10452
  const visible = opts.all === true ? jobs : jobs.filter((j) => !TERMINAL_STATUSES.has(j.status));
@@ -9275,17 +10469,17 @@ var queueListCommand = new Command32("list").description("List queued, running,
9275
10469
  const widths = headers.map(
9276
10470
  (h) => Math.max(h.length, ...rows.map((r) => String(r[h]).length))
9277
10471
  );
9278
- const pad3 = (s, w) => s + " ".repeat(Math.max(0, w - s.length));
9279
- process.stdout.write(headers.map((h, i) => pad3(h, widths[i])).join(" ") + "\n");
10472
+ const pad4 = (s, w) => s + " ".repeat(Math.max(0, w - s.length));
10473
+ process.stdout.write(headers.map((h, i) => pad4(h, widths[i])).join(" ") + "\n");
9280
10474
  process.stdout.write(widths.map((w) => "-".repeat(w)).join(" ") + "\n");
9281
10475
  for (const r of rows) {
9282
10476
  process.stdout.write(
9283
- headers.map((h, i) => pad3(String(r[h]), widths[i])).join(" ") + "\n"
10477
+ headers.map((h, i) => pad4(String(r[h]), widths[i])).join(" ") + "\n"
9284
10478
  );
9285
10479
  }
9286
10480
  log35.info(`queue.maxConcurrent = ${String(cfg.maxConcurrent)} (queue.enabled=${String(cfg.enabled)})`);
9287
10481
  });
9288
- var queueShowCommand = new Command32("show").description("Dump a job manifest and tail its log").argument("<id>", "queue job id (from `agentbox queue list`)").option("--tail <n>", "lines of log to print (default: 50)", "50").action(async (id, opts) => {
10482
+ var queueShowCommand = new Command33("show").description("Dump a job manifest and tail its log").argument("<id>", "queue job id (from `agentbox queue list`)").option("--tail <n>", "lines of log to print (default: 50)", "50").action(async (id, opts) => {
9289
10483
  const job = await readJob(id);
9290
10484
  if (!job) {
9291
10485
  log35.error(`no job with id ${id}`);
@@ -9307,7 +10501,7 @@ var queueShowCommand = new Command32("show").description("Dump a job manifest an
9307
10501
  log35.info(`(no log at ${job.logPath} yet)`);
9308
10502
  }
9309
10503
  });
9310
- var queueCancelCommand = new Command32("cancel").description("Cancel a queued job; running jobs are NOT killed \u2014 use `agentbox destroy` instead").argument("<id>", "queue job id (from `agentbox queue list`)").action(async (id) => {
10504
+ var queueCancelCommand = new Command33("cancel").description("Cancel a queued job; running jobs are NOT killed \u2014 use `agentbox destroy` instead").argument("<id>", "queue job id (from `agentbox queue list`)").action(async (id) => {
9311
10505
  intro7(`Cancelling queue job ${id}...`);
9312
10506
  const job = await readJob(id);
9313
10507
  if (!job) {
@@ -9329,7 +10523,7 @@ var queueCancelCommand = new Command32("cancel").description("Cancel a queued jo
9329
10523
  await writeJob(cancelled);
9330
10524
  outro7(`job ${id} cancelled`);
9331
10525
  });
9332
- var queueClearCommand = new Command32("clear").description("Sweep terminal-state manifests from ~/.agentbox/queue/").option("--done", "remove done jobs").option("--failed", "remove failed jobs").option("--cancelled", "remove cancelled jobs").option("--all", "remove every terminal-state job (done + failed + cancelled)").action(async (opts) => {
10526
+ var queueClearCommand = new Command33("clear").description("Sweep terminal-state manifests from ~/.agentbox/queue/").option("--done", "remove done jobs").option("--failed", "remove failed jobs").option("--cancelled", "remove cancelled jobs").option("--all", "remove every terminal-state job (done + failed + cancelled)").action(async (opts) => {
9333
10527
  const targets = /* @__PURE__ */ new Set();
9334
10528
  if (opts.all === true || opts.done === true) targets.add("done");
9335
10529
  if (opts.all === true || opts.failed === true) targets.add("failed");
@@ -9358,7 +10552,7 @@ var QUEUE_WAIT_EVENTS = [
9358
10552
  var ACTIVE_JOB_STATUSES = /* @__PURE__ */ new Set(["queued", "running"]);
9359
10553
  var DEFAULT_QUEUE_WAIT_TIMEOUT_MS = 10 * 60 * 1e3;
9360
10554
  var QUEUE_POLL_INTERVAL_MS = 500;
9361
- var queueWaitForCommand = new Command32("wait-for").description(
10555
+ var queueWaitForCommand = new Command33("wait-for").description(
9362
10556
  `Block until a queue / box event fires. <event> one of: ${QUEUE_WAIT_EVENTS.join(" | ")}.`
9363
10557
  ).argument("<event>", `target event: ${QUEUE_WAIT_EVENTS.join(" | ")}`).option("--box <ref>", "box ref (required for box-paused / box-running / box-stopped)").option("--job <id>", "queue job id (required for job-done)").option("--timeout <ms>", `wall-clock cap (default: ${String(DEFAULT_QUEUE_WAIT_TIMEOUT_MS)})`).option("--json", "emit a JSON envelope { matched, elapsedMs, ... }").action(async (eventRaw, opts) => {
9364
10558
  if (!QUEUE_WAIT_EVENTS.includes(eventRaw)) {
@@ -9446,11 +10640,11 @@ async function pollUntil(deadline, probe) {
9446
10640
  if (result !== void 0) return result;
9447
10641
  const remaining = deadline - Date.now();
9448
10642
  if (remaining <= 0) break;
9449
- await sleep3(Math.min(QUEUE_POLL_INTERVAL_MS, remaining));
10643
+ await sleep4(Math.min(QUEUE_POLL_INTERVAL_MS, remaining));
9450
10644
  }
9451
10645
  throw new QueueWaitTimeout();
9452
10646
  }
9453
- function sleep3(ms) {
10647
+ function sleep4(ms) {
9454
10648
  return new Promise((r) => setTimeout(r, ms));
9455
10649
  }
9456
10650
  function parsePositiveInt3(raw, label) {
@@ -9483,8 +10677,8 @@ function truncate(s, max) {
9483
10677
  }
9484
10678
 
9485
10679
  // src/commands/relay.ts
9486
- import { log as log36, spinner as spinner8 } from "@clack/prompts";
9487
- import { Command as Command33 } from "commander";
10680
+ import { log as log36, spinner as spinner9 } from "@clack/prompts";
10681
+ import { Command as Command34 } from "commander";
9488
10682
  async function rehydrateFromState() {
9489
10683
  const state = await readState();
9490
10684
  await rehydrateRelayRegistry(
@@ -9508,12 +10702,14 @@ function renderStatus(s) {
9508
10702
  if (s.running && s.health) {
9509
10703
  return [
9510
10704
  "relay: running",
9511
- ` pid: ${s.pid === null ? "?" : String(s.pid)}`,
9512
- ` port: ${String(s.port)}`,
9513
- ` url: ${s.endpoint.hostUrl}`,
9514
- ` boxes: ${String(s.health.boxes)}`,
9515
- ` events: ${String(s.health.events)}`,
9516
- ` log: ${s.logFile}`
10705
+ ` pid: ${s.pid === null ? "?" : String(s.pid)}`,
10706
+ ` port: ${String(s.port)}`,
10707
+ ` url: ${s.endpoint.hostUrl}`,
10708
+ ` version: ${s.health.version ?? "(unknown \u2014 relay predates version field)"}`,
10709
+ ` commit: ${s.health.commit ?? "(unknown)"}`,
10710
+ ` boxes: ${String(s.health.boxes)}`,
10711
+ ` events: ${String(s.health.events)}`,
10712
+ ` log: ${s.logFile}`
9517
10713
  ].join("\n");
9518
10714
  }
9519
10715
  if (s.pidAlive) {
@@ -9524,7 +10720,7 @@ function renderStatus(s) {
9524
10720
  }
9525
10721
  return ["relay: not running", ` log: ${s.logFile}`].join("\n");
9526
10722
  }
9527
- var statusSub = new Command33("status").description("Show whether the host relay is running, with pid / port / box count").option("--json", "emit RelayStatus as JSON").action(async (opts) => {
10723
+ var statusSub = new Command34("status").description("Show whether the host relay is running, with pid / port / box count").option("--json", "emit RelayStatus as JSON").action(async (opts) => {
9528
10724
  try {
9529
10725
  const s = await getRelayStatus();
9530
10726
  if (opts.json) {
@@ -9536,9 +10732,9 @@ var statusSub = new Command33("status").description("Show whether the host relay
9536
10732
  handleLifecycleError(err);
9537
10733
  }
9538
10734
  });
9539
- var stopSub = new Command33("stop").description("Stop the host relay process (idempotent)").action(async () => {
10735
+ var stopSub = new Command34("stop").description("Stop the host relay process (idempotent)").action(async () => {
9540
10736
  try {
9541
- const s = spinner8();
10737
+ const s = spinner9();
9542
10738
  s.start("stopping relay");
9543
10739
  const result = await stopRelay();
9544
10740
  s.stop(
@@ -9548,9 +10744,9 @@ var stopSub = new Command33("stop").description("Stop the host relay process (id
9548
10744
  handleLifecycleError(err);
9549
10745
  }
9550
10746
  });
9551
- var startSub = new Command33("start").description("Start the host relay if not already running (idempotent)").action(async () => {
10747
+ var startSub = new Command34("start").description("Start the host relay if not already running (idempotent)").action(async () => {
9552
10748
  try {
9553
- const s = spinner8();
10749
+ const s = spinner9();
9554
10750
  s.start("starting relay");
9555
10751
  const ep = await ensureRelay();
9556
10752
  await rehydrateFromState();
@@ -9559,15 +10755,15 @@ var startSub = new Command33("start").description("Start the host relay if not a
9559
10755
  handleLifecycleError(err);
9560
10756
  }
9561
10757
  });
9562
- var restartSub = new Command33("restart").description("Stop then start the host relay").action(async () => {
10758
+ var restartSub = new Command34("restart").description("Stop then start the host relay").action(async () => {
9563
10759
  try {
9564
- const s = spinner8();
10760
+ const s = spinner9();
9565
10761
  s.start("stopping relay");
9566
10762
  const stopped = await stopRelay();
9567
10763
  s.stop(
9568
10764
  stopped.stopped ? `stopped relay (pid ${String(stopped.pid)})` : "relay was not running"
9569
10765
  );
9570
- const s2 = spinner8();
10766
+ const s2 = spinner9();
9571
10767
  s2.start("starting relay");
9572
10768
  try {
9573
10769
  const ep = await ensureRelay();
@@ -9582,11 +10778,11 @@ var restartSub = new Command33("restart").description("Stop then start the host
9582
10778
  handleLifecycleError(err);
9583
10779
  }
9584
10780
  });
9585
- var relayCommand = new Command33("relay").description("Manage the host relay process (status / stop / start / restart)").addCommand(statusSub, { isDefault: true }).addCommand(stopSub).addCommand(startSub).addCommand(restartSub);
10781
+ var relayCommand = new Command34("relay").description("Manage the host relay process (status / stop / start / restart)").addCommand(statusSub, { isDefault: true }).addCommand(stopSub).addCommand(startSub).addCommand(restartSub);
9586
10782
 
9587
10783
  // src/commands/_run-queued-job.ts
9588
- import { Command as Command34 } from "commander";
9589
- var runQueuedJobCommand = new Command34("_run-queued-job").description("internal: run a queued background agent job (do not invoke directly)").argument("<id>", "queue job id (from ~/.agentbox/queue/<id>.json)").action(async (id) => {
10784
+ import { Command as Command35 } from "commander";
10785
+ var runQueuedJobCommand = new Command35("_run-queued-job").description("internal: run a queued background agent job (do not invoke directly)").argument("<id>", "queue job id (from ~/.agentbox/queue/<id>.json)").action(async (id) => {
9590
10786
  const log45 = openCommandLog(`queue-${id}`);
9591
10787
  log45.write(`worker pid=${String(process.pid)} starting for job ${id}`);
9592
10788
  let job = null;
@@ -9682,7 +10878,7 @@ async function runDockerJob(job, log45, onBoxCreated) {
9682
10878
  log45.write(`starting claude session`);
9683
10879
  await startClaudeSession({
9684
10880
  container: result.record.container,
9685
- claudeArgs: promptedArgs,
10881
+ claudeArgs: applyClaudeSkipPermissions(promptedArgs, cfg.effective),
9686
10882
  sessionName: cfg.effective.claude.sessionName,
9687
10883
  boxName: result.record.name
9688
10884
  });
@@ -9694,7 +10890,7 @@ async function runDockerJob(job, log45, onBoxCreated) {
9694
10890
  log45.write(`starting codex session`);
9695
10891
  await startCodexSession({
9696
10892
  container: result.record.container,
9697
- codexArgs: promptedArgs,
10893
+ codexArgs: applyCodexSkipPermissions(promptedArgs, cfg.effective),
9698
10894
  sessionName: cfg.effective.codex.sessionName
9699
10895
  });
9700
10896
  } else if (job.agent === "opencode") {
@@ -9729,13 +10925,20 @@ function buildOverridesFromJob(job) {
9729
10925
  else if (job.agent === "codex") out.codex = { sessionName: opts.sessionName };
9730
10926
  else if (job.agent === "opencode") out.opencode = { sessionName: opts.sessionName };
9731
10927
  }
10928
+ if (opts.dangerouslySkipPermissions !== void 0) {
10929
+ if (job.agent === "claude-code") {
10930
+ out.claude = { ...out.claude, dangerouslySkipPermissions: opts.dangerouslySkipPermissions };
10931
+ } else if (job.agent === "codex") {
10932
+ out.codex = { ...out.codex, dangerouslySkipPermissions: opts.dangerouslySkipPermissions };
10933
+ }
10934
+ }
9732
10935
  return out;
9733
10936
  }
9734
10937
 
9735
10938
  // src/commands/screen.ts
9736
10939
  import { spawnSync as spawnSync3 } from "child_process";
9737
10940
  import { log as log37 } from "@clack/prompts";
9738
- import { Command as Command35 } from "commander";
10941
+ import { Command as Command36 } from "commander";
9739
10942
  var SIGNED_URL_TTL_MIN = 1;
9740
10943
  var SIGNED_URL_TTL_MAX = 86400;
9741
10944
  function parseTtlOrExit(raw) {
@@ -9748,7 +10951,7 @@ function parseTtlOrExit(raw) {
9748
10951
  }
9749
10952
  return n;
9750
10953
  }
9751
- var screenCommand = new Command35("screen").description("Open a box's VNC (noVNC) viewer in the browser (auto-unpause/start)").argument(
10954
+ var screenCommand = new Command36("screen").description("Open a box's VNC (noVNC) viewer in the browser (auto-unpause/start)").argument(
9752
10955
  "[box]",
9753
10956
  "box ref: project index, id, id prefix, name, or container (default: the only box in this project)"
9754
10957
  ).option("--print", "print the URL to stdout instead of launching the browser").option("--loopback", "docker only: use the 127.0.0.1 URL instead of the OrbStack .orb.local URL").option(
@@ -9813,6 +11016,28 @@ var screenCommand = new Command35("screen").description("Open a box's VNC (noVNC
9813
11016
  } else if (state === "missing") {
9814
11017
  throw new Error(`cloud sandbox for ${box.name} is missing; was it deleted?`);
9815
11018
  }
11019
+ const persisted = await readBoxStatus(box);
11020
+ const hasWebService = persisted?.services.some((s) => s.expose) ?? false;
11021
+ if (hasWebService) {
11022
+ try {
11023
+ const webUrl = await p.resolveUrl(box, { kind: "web" });
11024
+ const q = `'${webUrl.replace(/'/g, "'\\''")}'`;
11025
+ const br = await p.exec(box, ["bash", "-lc", `agent-browser open --headed ${q}`], {
11026
+ user: "vscode"
11027
+ });
11028
+ if (br.exitCode === 0) {
11029
+ log37.info(`opened ${webUrl} in the in-box browser (visible in the VNC view)`);
11030
+ } else {
11031
+ log37.warn(
11032
+ `could not open in-box browser (continuing): ${br.stderr.trim() || br.stdout.trim() || `exit ${String(br.exitCode)}`}`
11033
+ );
11034
+ }
11035
+ } catch (err) {
11036
+ log37.warn(
11037
+ `in-box browser skipped: ${err instanceof Error ? err.message : String(err)}`
11038
+ );
11039
+ }
11040
+ }
9816
11041
  const base = await p.resolveUrl(box, { kind: "vnc", ttl });
9817
11042
  url = `${base.replace(/\/$/, "")}/vnc.html?autoconnect=1&password=${encodeURIComponent(box.vncPassword)}`;
9818
11043
  }
@@ -9835,7 +11060,7 @@ var screenCommand = new Command35("screen").description("Open a box's VNC (noVNC
9835
11060
  // src/commands/shell.ts
9836
11061
  import { spawnSync as spawnSync4 } from "child_process";
9837
11062
  import { log as log39 } from "@clack/prompts";
9838
- import { Command as Command36 } from "commander";
11063
+ import { Command as Command37 } from "commander";
9839
11064
 
9840
11065
  // src/commands/_provider-guard.ts
9841
11066
  import { log as log38 } from "@clack/prompts";
@@ -9950,7 +11175,7 @@ async function startOrAttachShell(box, cfg) {
9950
11175
  });
9951
11176
  process.exit(code);
9952
11177
  }
9953
- var shellCommand = new Command36("shell").description(
11178
+ var shellCommand = new Command37("shell").description(
9954
11179
  "Open an interactive shell in a box, in a detachable tmux session (auto-unpause/start)"
9955
11180
  ).argument(
9956
11181
  "[box]",
@@ -10041,7 +11266,7 @@ var shellCommand = new Command36("shell").description(
10041
11266
  handleLifecycleError(err);
10042
11267
  }
10043
11268
  });
10044
- var shellAttachCommand = new Command36("attach").description(
11269
+ var shellAttachCommand = new Command37("attach").description(
10045
11270
  "Attach to a shell tmux session in a box, starting one if none is running (auto-unpause/start)"
10046
11271
  ).argument(
10047
11272
  "[box]",
@@ -10079,7 +11304,7 @@ function renderShellTable(sessions) {
10079
11304
  for (const r of rows) process.stdout.write(`${fmt(r)}
10080
11305
  `);
10081
11306
  }
10082
- var shellLsCommand = new Command36("ls").description("List the shell tmux sessions running in a box").argument(
11307
+ var shellLsCommand = new Command37("ls").description("List the shell tmux sessions running in a box").argument(
10083
11308
  "[box]",
10084
11309
  "box ref: project index, id, id prefix, name, or container (default: the only box in this project)"
10085
11310
  ).action(async (idOrName) => {
@@ -10102,7 +11327,7 @@ var shellLsCommand = new Command36("ls").description("List the shell tmux sessio
10102
11327
  handleLifecycleError(err);
10103
11328
  }
10104
11329
  });
10105
- var shellKillCommand = new Command36("kill").description("Kill a shell tmux session in a box (the shell and anything running in it)").argument(
11330
+ var shellKillCommand = new Command37("kill").description("Kill a shell tmux session in a box (the shell and anything running in it)").argument(
10106
11331
  "[box]",
10107
11332
  "box ref: project index, id, id prefix, name, or container (default: the only box in this project)"
10108
11333
  ).option("-n, --name <label>", "shell label to kill (default: the box default shell)").option("--all", "kill every shell session in the box").action(async function(idOrName) {
@@ -10140,8 +11365,8 @@ shellCommand.addCommand(shellLsCommand);
10140
11365
  shellCommand.addCommand(shellKillCommand);
10141
11366
 
10142
11367
  // src/commands/start.ts
10143
- import { Command as Command37 } from "commander";
10144
- var startCommand = new Command37("start").description(
11368
+ import { Command as Command38 } from "commander";
11369
+ var startCommand = new Command38("start").description(
10145
11370
  "Start a stopped box. Docker: docker start + relaunch ctl/dockerd/vnc daemons. Cloud: backend.start, then re-resolve preview URLs/tokens, re-launch in-sandbox ctl/dockerd daemons, and re-register with the host relay (so the CloudBoxPoller resumes)."
10146
11371
  ).argument(
10147
11372
  "[box]",
@@ -10165,7 +11390,7 @@ var startCommand = new Command37("start").description(
10165
11390
 
10166
11391
  // src/commands/status.ts
10167
11392
  import { log as log41 } from "@clack/prompts";
10168
- import { Command as Command38 } from "commander";
11393
+ import { Command as Command39 } from "commander";
10169
11394
 
10170
11395
  // src/endpoints-render.ts
10171
11396
  function renderEndpointLines(endpoints, stream) {
@@ -10377,7 +11602,7 @@ async function runInspect(box, opts) {
10377
11602
 
10378
11603
  // src/commands/status.ts
10379
11604
  var statusCommand2 = withWatchOptions(
10380
- new Command38("status").description("Show service + task status from a box's agentbox-ctl daemon").argument(
11605
+ new Command39("status").description("Show service + task status from a box's agentbox-ctl daemon").argument(
10381
11606
  "[box]",
10382
11607
  "box ref: project index, id, id prefix, name, or container (default: the only box in this project)"
10383
11608
  ).option("-j, --json", "machine-readable JSON output").option("--inspect", "show detailed box info (volumes, limits, paths) instead of service/task status")
@@ -10551,8 +11776,8 @@ function renderPersisted2(s, state) {
10551
11776
  }
10552
11777
 
10553
11778
  // src/commands/stop.ts
10554
- import { Command as Command39 } from "commander";
10555
- var stopCommand = new Command39("stop").description(
11779
+ import { Command as Command40 } from "commander";
11780
+ var stopCommand = new Command40("stop").description(
10556
11781
  "Stop a box (Docker: docker stop; preserves upper + node_modules volumes. Cloud: backend.stop \u2014 sandbox stays in your account, disk preserved)."
10557
11782
  ).argument(
10558
11783
  "[box]",
@@ -10581,7 +11806,7 @@ restart with: agentbox start ${box.name}
10581
11806
  });
10582
11807
 
10583
11808
  // src/commands/top.ts
10584
- import { Command as Command40 } from "commander";
11809
+ import { Command as Command41 } from "commander";
10585
11810
  var COLS = ["BOX", "STATE", "CPU%", "MEM USAGE / LIMIT", "MEM%", "PIDS", "DISK", "NET I/O"];
10586
11811
  function row(name, state, s) {
10587
11812
  const mem = `${fmtBytes(s.memUsedBytes)} / ${fmtBytes(s.memLimitBytes)}`;
@@ -10614,6 +11839,7 @@ async function selectBoxes(idOrName, opts) {
10614
11839
  }
10615
11840
  async function snapshot(idOrName, opts) {
10616
11841
  const boxes = await selectBoxes(idOrName, opts);
11842
+ if (opts.live) await applyLiveCloudStates(boxes);
10617
11843
  const stats = await Promise.all(
10618
11844
  boxes.map((b) => {
10619
11845
  if ((b.provider ?? "docker") !== "docker") return emptyStats(b.provider ?? "cloud");
@@ -10654,10 +11880,10 @@ async function renderProjectFooters() {
10654
11880
 
10655
11881
  SYSTEM: ${parts.join(" - ")}` : "";
10656
11882
  }
10657
- var topCommand = new Command40("top").description("Live resource monitor (cpu/mem/pids/disk) for a box, the project, or every box").argument(
11883
+ var topCommand = new Command41("top").description("Live resource monitor (cpu/mem/pids/disk) for a box, the project, or every box").argument(
10658
11884
  "[box]",
10659
11885
  "box ref (default: every box on the host; --project narrows to the cwd's project)"
10660
- ).option("-p, --project", "show only boxes in the cwd's project").option("--once", "print a single snapshot instead of watching").option("-j, --json", "machine-readable JSON (implies --once)").option("--interval <seconds>", "refresh interval", "2").action(async (idOrName, opts) => {
11886
+ ).option("-p, --project", "show only boxes in the cwd's project").option("--once", "print a single snapshot instead of watching").option("-j, --json", "machine-readable JSON (implies --once)").option("--interval <seconds>", "refresh interval", "2").option("--live", "probe live cloud state via the provider SDK (slower; default: last host-known)").action(async (idOrName, opts) => {
10661
11887
  try {
10662
11888
  if (opts.json) {
10663
11889
  const { boxes, stats } = await snapshot(idOrName, opts);
@@ -10687,8 +11913,8 @@ var topCommand = new Command40("top").description("Live resource monitor (cpu/me
10687
11913
  });
10688
11914
 
10689
11915
  // src/commands/unpause.ts
10690
- import { Command as Command41 } from "commander";
10691
- var unpauseCommand = new Command41("unpause").description(
11916
+ import { Command as Command42 } from "commander";
11917
+ var unpauseCommand = new Command42("unpause").description(
10692
11918
  "Resume a paused box. Docker: `docker unpause` (sub-second). Cloud: backend.resume (re-hydrates from archive \u2014 slower first time)."
10693
11919
  ).argument(
10694
11920
  "[box]",
@@ -10712,8 +11938,8 @@ var unpauseCommand = new Command41("unpause").description(
10712
11938
 
10713
11939
  // src/commands/update.ts
10714
11940
  import { spawn as spawn4 } from "child_process";
10715
- import { confirm as confirm15, intro as intro8, isCancel as isCancel16, log as log42, outro as outro8, spinner as spinner9 } from "@clack/prompts";
10716
- import { Command as Command42 } from "commander";
11941
+ import { confirm as confirm16, intro as intro8, isCancel as isCancel17, log as log42, outro as outro8, spinner as spinner10 } from "@clack/prompts";
11942
+ import { Command as Command43 } from "commander";
10717
11943
 
10718
11944
  // src/exec-method.ts
10719
11945
  function detectExecutionMethod(input) {
@@ -10757,7 +11983,7 @@ function runInherit(cmd, args) {
10757
11983
  child.on("close", (code) => resolveP(code ?? 0));
10758
11984
  });
10759
11985
  }
10760
- var updateCommand = new Command42("self-update").description(
11986
+ var updateCommand = new Command43("self-update").description(
10761
11987
  "Update agentbox: self-update via npm/pnpm (unless run via npx), wipe the box image so it rebuilds, and reload the relay"
10762
11988
  ).option("-y, --yes", "skip the confirmation prompt").option("--dry-run", "show what would happen, don't change anything").option("--skip-self", "skip the package self-update; only refresh the image + relay").action(async (opts) => {
10763
11989
  try {
@@ -10780,8 +12006,8 @@ var updateCommand = new Command42("self-update").description(
10780
12006
  return;
10781
12007
  }
10782
12008
  if (!opts.yes) {
10783
- const ok = await confirm15({ message: "Proceed with update?", initialValue: true });
10784
- if (isCancel16(ok) || !ok) {
12009
+ const ok = await confirm16({ message: "Proceed with update?", initialValue: true });
12010
+ if (isCancel17(ok) || !ok) {
10785
12011
  log42.info("cancelled");
10786
12012
  return;
10787
12013
  }
@@ -10803,13 +12029,13 @@ var updateCommand = new Command42("self-update").description(
10803
12029
  log42.success(`updated ${PKG} via ${cmd.cmd}`);
10804
12030
  }
10805
12031
  }
10806
- const s = spinner9();
12032
+ const s = spinner10();
10807
12033
  s.start(`removing image ${DEFAULT_BOX_IMAGE}`);
10808
12034
  const removed = await removeImage(DEFAULT_BOX_IMAGE);
10809
12035
  s.stop(
10810
12036
  removed ? `removed image ${DEFAULT_BOX_IMAGE} (rebuilds on next create/claude)` : `image ${DEFAULT_BOX_IMAGE} not present (nothing to remove)`
10811
12037
  );
10812
- const sr = spinner9();
12038
+ const sr = spinner10();
10813
12039
  sr.start("stopping relay");
10814
12040
  const stop = await stopRelay();
10815
12041
  sr.stop(
@@ -10820,7 +12046,7 @@ var updateCommand = new Command42("self-update").description(
10820
12046
  "relay will restart automatically (with the updated build) on your next `agentbox create` / `agentbox claude`"
10821
12047
  );
10822
12048
  } else {
10823
- const sr2 = spinner9();
12049
+ const sr2 = spinner10();
10824
12050
  sr2.start("restarting relay");
10825
12051
  try {
10826
12052
  const ep = await ensureRelay();
@@ -10841,7 +12067,7 @@ var updateCommand = new Command42("self-update").description(
10841
12067
  // src/commands/url.ts
10842
12068
  import { spawnSync as spawnSync5 } from "child_process";
10843
12069
  import { log as log43 } from "@clack/prompts";
10844
- import { Command as Command43 } from "commander";
12070
+ import { Command as Command44 } from "commander";
10845
12071
  var SIGNED_URL_TTL_MIN2 = 1;
10846
12072
  var SIGNED_URL_TTL_MAX2 = 86400;
10847
12073
  function parseTtlOrExit2(raw) {
@@ -10854,7 +12080,7 @@ function parseTtlOrExit2(raw) {
10854
12080
  }
10855
12081
  return n;
10856
12082
  }
10857
- var urlCommand = new Command43("url").description(
12083
+ var urlCommand = new Command44("url").description(
10858
12084
  "Open a box's web app URL in the browser, even when no service declares `expose:` (auto-unpause/start)"
10859
12085
  ).argument(
10860
12086
  "[box]",
@@ -10933,8 +12159,8 @@ var urlCommand = new Command43("url").description(
10933
12159
 
10934
12160
  // src/commands/wait.ts
10935
12161
  import { log as log44 } from "@clack/prompts";
10936
- import { Command as Command44 } from "commander";
10937
- var waitCommand = new Command44("wait").description("Block until the box reports all autostart units ready").argument(
12162
+ import { Command as Command45 } from "commander";
12163
+ var waitCommand = new Command45("wait").description("Block until the box reports all autostart units ready").argument(
10938
12164
  "[box]",
10939
12165
  "box ref: project index, id, id prefix, name, or container (default: the only box in this project)"
10940
12166
  ).option("--timeout <ms>", "overall timeout in milliseconds", "120000").option("--units <names...>", "restrict to the named units").option("-j, --json", "machine-readable JSON output").action(async (idOrName, opts) => {
@@ -10974,14 +12200,14 @@ var SUGARED_COMMANDS = ["create", "claude", "codex", "opencode"];
10974
12200
  function isSugared(name) {
10975
12201
  return SUGARED_COMMANDS.includes(name);
10976
12202
  }
10977
- function rewriteProviderPrefix(argv) {
10978
- if (argv.length < 4) return [...argv];
10979
- const provider = argv[2];
10980
- const subcmd = argv[3];
10981
- if (typeof provider !== "string" || typeof subcmd !== "string") return [...argv];
10982
- if (!isKnownProvider(provider) || !isSugared(subcmd)) return [...argv];
10983
- const head = argv.slice(0, 2);
10984
- const rest = argv.slice(4);
12203
+ function rewriteProviderPrefix(argv2) {
12204
+ if (argv2.length < 4) return [...argv2];
12205
+ const provider = argv2[2];
12206
+ const subcmd = argv2[3];
12207
+ if (typeof provider !== "string" || typeof subcmd !== "string") return [...argv2];
12208
+ if (!isKnownProvider(provider) || !isSugared(subcmd)) return [...argv2];
12209
+ const head = argv2.slice(0, 2);
12210
+ const rest = argv2.slice(4);
10985
12211
  return [...head, subcmd, "--provider", provider, ...rest];
10986
12212
  }
10987
12213
 
@@ -10989,7 +12215,7 @@ function rewriteProviderPrefix(argv) {
10989
12215
  process.env.DOCKER_CLI_HINTS ??= "false";
10990
12216
  process.env.AGENTBOX_CLI_VERSION = AGENTBOX_VERSION;
10991
12217
  process.env.AGENTBOX_CLI_COMMIT = AGENTBOX_COMMIT;
10992
- var program = new Command45();
12218
+ var program = new Command46();
10993
12219
  program.name("agentbox").description("Launch coding agents in isolated sandboxes").version(AGENTBOX_VERSION);
10994
12220
  program.enablePositionalOptions();
10995
12221
  program.addCommand(createCommand);
@@ -11031,10 +12257,43 @@ program.addCommand(vercelCommand);
11031
12257
  program.addCommand(dockerCommand);
11032
12258
  program.addCommand(updateCommand);
11033
12259
  program.addCommand(installCommand);
12260
+ program.addCommand(doctorCommand);
11034
12261
  program.configureHelp({ visibleCommands: () => [] });
11035
12262
  program.addHelpText("after", () => "\n" + buildGroupedHelp(program));
11036
12263
  await applyEngineOverrideAtStartup();
11037
- program.parseAsync(rewriteProviderPrefix(process.argv)).catch((err) => {
12264
+ var argv = rewriteProviderPrefix(process.argv);
12265
+ var FIRST_RUN_EXEMPT = /* @__PURE__ */ new Set([
12266
+ "install",
12267
+ "doctor",
12268
+ "help",
12269
+ "relay",
12270
+ "_run-queued-job",
12271
+ "drive",
12272
+ "screen"
12273
+ ]);
12274
+ function isFirstRunHookEligible(args) {
12275
+ if (!process.stdin.isTTY || !process.stdout.isTTY) return false;
12276
+ const rest = args.slice(2);
12277
+ if (rest.length === 0) return false;
12278
+ for (const a of rest) {
12279
+ if (a === "--help" || a === "-h" || a === "--version" || a === "-V") return false;
12280
+ }
12281
+ const first = rest[0];
12282
+ if (typeof first !== "string" || first.startsWith("-")) return false;
12283
+ if (FIRST_RUN_EXEMPT.has(first)) return false;
12284
+ return true;
12285
+ }
12286
+ if (isFirstRun() && isFirstRunHookEligible(argv)) {
12287
+ try {
12288
+ await runInstallWizard({ fromAutoTrigger: true });
12289
+ } catch (err) {
12290
+ process.stderr.write(
12291
+ `install wizard failed: ${err instanceof Error ? err.message : String(err)}
12292
+ `
12293
+ );
12294
+ }
12295
+ }
12296
+ program.parseAsync(argv).catch((err) => {
11038
12297
  console.error(err);
11039
12298
  process.exit(1);
11040
12299
  });