@madarco/agentbox 0.9.0 → 0.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (36) hide show
  1. package/CHANGELOG.md +89 -0
  2. package/README.md +161 -0
  3. package/dist/{_cloud-attach-ZXBCNWJX.js → _cloud-attach-O6NYTLES.js} +3 -3
  4. package/dist/{chunk-BXQMIEHC.js → chunk-2GPORKYF.js} +254 -162
  5. package/dist/chunk-2GPORKYF.js.map +1 -0
  6. package/dist/{chunk-NCJP5MTN.js → chunk-7UIAO7PC.js} +213 -51
  7. package/dist/chunk-7UIAO7PC.js.map +1 -0
  8. package/dist/{chunk-GU5LW4B5.js → chunk-R4O5WPHW.js} +374 -62
  9. package/dist/chunk-R4O5WPHW.js.map +1 -0
  10. package/dist/{dist-GDHP34ZK.js → dist-5FQGYRW5.js} +15 -3
  11. package/dist/dist-5FQGYRW5.js.map +1 -0
  12. package/dist/{dist-32EZBYG4.js → dist-BQNX7RQE.js} +12 -2
  13. package/dist/{dist-XML54CNB.js → dist-PZW3GWWU.js} +30 -5
  14. package/dist/dist-PZW3GWWU.js.map +1 -0
  15. package/dist/{dist-CX5CGVEB.js → dist-TMHSUVTP.js} +3 -3
  16. package/dist/index.js +1773 -526
  17. package/dist/index.js.map +1 -1
  18. package/package.json +9 -7
  19. package/runtime/docker/apps/cli/share/agentbox-setup/SKILL.md +9 -8
  20. package/runtime/docker/packages/ctl/dist/bin.cjs +32 -3
  21. package/runtime/hetzner/agentbox-setup-skill.md +9 -8
  22. package/runtime/hetzner/ctl.cjs +32 -3
  23. package/runtime/relay/bin.cjs +32 -3
  24. package/runtime/vercel/agentbox-setup-skill.md +9 -8
  25. package/runtime/vercel/ctl.cjs +32 -3
  26. package/runtime/vercel/custom-system-CLAUDE.md +1 -4
  27. package/runtime/vercel/scripts/provision.sh +40 -0
  28. package/share/agentbox-setup/SKILL.md +9 -8
  29. package/dist/chunk-BXQMIEHC.js.map +0 -1
  30. package/dist/chunk-GU5LW4B5.js.map +0 -1
  31. package/dist/chunk-NCJP5MTN.js.map +0 -1
  32. package/dist/dist-GDHP34ZK.js.map +0 -1
  33. package/dist/dist-XML54CNB.js.map +0 -1
  34. /package/dist/{_cloud-attach-ZXBCNWJX.js.map → _cloud-attach-O6NYTLES.js.map} +0 -0
  35. /package/dist/{dist-32EZBYG4.js.map → dist-BQNX7RQE.js.map} +0 -0
  36. /package/dist/{dist-CX5CGVEB.js.map → dist-TMHSUVTP.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-2GPORKYF.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-R4O5WPHW.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-7UIAO7PC.js";
203
211
  import {
204
212
  DEFAULT_BOX_IMAGE,
205
213
  STATE_DIR,
@@ -211,11 +219,11 @@ import {
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.0" : "0.0.0-dev";
223
+ var AGENTBOX_COMMIT = true ? "e0ccad3c" : "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-TMHSUVTP.js")).daytonaBackend;
1858
+ case "hetzner":
1859
+ return (await import("./dist-5FQGYRW5.js")).hetznerBackend;
1860
+ case "vercel":
1861
+ return (await import("./dist-PZW3GWWU.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-TMHSUVTP.js")).daytonaProvider;
2659
2774
  case "hetzner":
2660
- return (await import("./dist-GDHP34ZK.js")).hetznerProvider;
2775
+ return (await import("./dist-5FQGYRW5.js")).hetznerProvider;
2661
2776
  case "vercel":
2662
- return (await import("./dist-XML54CNB.js")).vercelProvider;
2777
+ return (await import("./dist-PZW3GWWU.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({
@@ -5126,7 +5357,7 @@ var createCommand = new Command9("create").description("Create and start a new a
5126
5357
  }
5127
5358
  outro5("done");
5128
5359
  if (attachClaudeAfter) {
5129
- const { cloudAgentAttach: cloudAgentAttach2 } = await import("./_cloud-attach-ZXBCNWJX.js");
5360
+ const { cloudAgentAttach: cloudAgentAttach2 } = await import("./_cloud-attach-O6NYTLES.js");
5130
5361
  await cloudAgentAttach2({
5131
5362
  box: result.record,
5132
5363
  binary: "claude",
@@ -5169,13 +5400,16 @@ import { Command as Command10 } from "commander";
5169
5400
  var SIDEBAR_WIDTH = 33;
5170
5401
  var MIN_RIGHT_W = 20;
5171
5402
  var MIN_RIGHT_H = 4;
5172
- function computeLayout(cols, rows) {
5403
+ function computeLayout(cols, rows, requestedAlertH = 0) {
5173
5404
  const sidebarW = Math.min(SIDEBAR_WIDTH, Math.max(0, cols - MIN_RIGHT_W - 1));
5174
5405
  const sepX = sidebarW;
5175
5406
  const rightX = sidebarW + 1;
5176
5407
  const rightW = Math.max(0, cols - rightX);
5177
5408
  const statusY = rows - 1;
5178
- const paneH = Math.max(0, statusY);
5409
+ const desired = Math.max(0, requestedAlertH);
5410
+ const alertH = statusY - desired >= MIN_RIGHT_H ? desired : 0;
5411
+ const paneH = Math.max(0, statusY - alertH);
5412
+ const alertY = statusY - alertH;
5179
5413
  return {
5180
5414
  cols,
5181
5415
  rows,
@@ -5183,6 +5417,8 @@ function computeLayout(cols, rows) {
5183
5417
  sepX,
5184
5418
  right: { x: rightX, y: 0, w: rightW, h: paneH },
5185
5419
  statusY,
5420
+ alertH,
5421
+ alertY,
5186
5422
  tooSmall: rightW < MIN_RIGHT_W || paneH < MIN_RIGHT_H
5187
5423
  };
5188
5424
  }
@@ -5478,6 +5714,21 @@ var BLANK = {
5478
5714
  strike: false
5479
5715
  };
5480
5716
  var PtySession = class {
5717
+ /** Box this session attaches to. Identifies it in the compositor's pool. */
5718
+ boxId;
5719
+ /** When true, the compositor keeps this session alive (in its pool) across
5720
+ * box switches instead of disposing it — see {@link Compositor.liveSessions}. */
5721
+ keepAlive;
5722
+ /** Agent/shell mode of this attach. The compositor restores `activeMode`
5723
+ * (drives the footer) from this when re-showing a pooled session. */
5724
+ mode;
5725
+ /**
5726
+ * Whether this session is the one currently shown in the right pane. A
5727
+ * kept-alive hidden session (`active === false`) still consumes PTY output
5728
+ * to keep its headless buffer current, but must NOT trigger right-pane
5729
+ * repaints. The compositor flips this on show/hide.
5730
+ */
5731
+ active = true;
5481
5732
  term;
5482
5733
  pty;
5483
5734
  cleanup;
@@ -5485,7 +5736,10 @@ var PtySession = class {
5485
5736
  // Reused per cell read — valid only until the next cell() call (the renderer
5486
5737
  // consumes it synchronously within composeRow).
5487
5738
  out = { ...BLANK };
5488
- constructor(spawn5, TerminalClass, command, args, cols, rows, onRenderable, onExit, cleanup) {
5739
+ constructor(spawn5, TerminalClass, boxId, keepAlive, mode, command, args, cols, rows, onRenderable, onExit, cleanup) {
5740
+ this.boxId = boxId;
5741
+ this.keepAlive = keepAlive;
5742
+ this.mode = mode;
5489
5743
  this.term = new TerminalClass({
5490
5744
  cols,
5491
5745
  rows,
@@ -5501,13 +5755,15 @@ var PtySession = class {
5501
5755
  env: process.env
5502
5756
  });
5503
5757
  this.pty.onData((d) => {
5504
- this.term.write(d, () => onRenderable());
5758
+ this.term.write(d, () => {
5759
+ if (this.active) onRenderable();
5760
+ });
5505
5761
  });
5506
5762
  this.term.onData((d) => {
5507
5763
  if (!this.disposed) this.pty.write(d);
5508
5764
  });
5509
5765
  this.pty.onExit(() => {
5510
- if (!this.disposed) onExit();
5766
+ if (!this.disposed) onExit(this.boxId);
5511
5767
  });
5512
5768
  }
5513
5769
  write(bytes) {
@@ -5569,6 +5825,7 @@ var SB_AWAITING = SB_BG + "\x1B[38;5;51m\x1B[1m";
5569
5825
  var SGR_RESET = "\x1B[0m";
5570
5826
  var POLL_MS = 1e3;
5571
5827
  var FRAME_MS = 16;
5828
+ var KEEP_ALIVE_MAX = 6;
5572
5829
  var RESIZE_DEBOUNCE_MS = 120;
5573
5830
  var LEADER_LINGER_MS = 1500;
5574
5831
  var NOTICE_SPINNER_MS = 120;
@@ -5581,7 +5838,7 @@ var Compositor = class {
5581
5838
  constructor(deps, initialId) {
5582
5839
  this.deps = deps;
5583
5840
  this.selectedId = initialId;
5584
- this.layout = computeLayout(this.out.columns ?? 100, this.out.rows ?? 30);
5841
+ this.layout = computeLayout(this.out.columns ?? 100, this.out.rows ?? 30, 0);
5585
5842
  this.parser = new InputParser({
5586
5843
  onEvent: (e) => {
5587
5844
  if (e.type === "leader") {
@@ -5642,7 +5899,17 @@ var Compositor = class {
5642
5899
  inp = process.stdin;
5643
5900
  boxes = [];
5644
5901
  selectedId;
5902
+ /** The session currently shown in the right pane (may also be in
5903
+ * {@link liveSessions} when it's keep-alive). */
5645
5904
  session = null;
5905
+ /**
5906
+ * Pool of kept-alive sessions, keyed by box id, for providers whose attach
5907
+ * is expensive to reconnect (vercel). Hidden entries keep their PTY + headless
5908
+ * buffer alive so switching back is instant — no probe, no re-spawn. Map
5909
+ * insertion order doubles as LRU recency (re-set on activate); bounded by
5910
+ * {@link KEEP_ALIVE_MAX}. Reconnect-cheap providers never enter this map.
5911
+ */
5912
+ liveSessions = /* @__PURE__ */ new Map();
5646
5913
  placeholder = null;
5647
5914
  menu = null;
5648
5915
  lifecycleMenu = null;
@@ -5734,6 +6001,22 @@ var Compositor = class {
5734
6001
  } catch {
5735
6002
  }
5736
6003
  this.syncPromptSubscriptions();
6004
+ this.reconcileLiveSessions();
6005
+ }
6006
+ /**
6007
+ * Drop pooled (hidden) sessions whose box is gone or no longer running, so a
6008
+ * paused/stopped/destroyed box can't keep its remote attach process alive.
6009
+ * The *active* session is intentionally skipped — it's torn down on the next
6010
+ * switch/re-resolve via {@link deactivateActive} (which checks box state),
6011
+ * so evicting it here would blank the pane out from under the poll's
6012
+ * re-resolve logic.
6013
+ */
6014
+ reconcileLiveSessions() {
6015
+ for (const boxId of [...this.liveSessions.keys()]) {
6016
+ if (this.session && this.session.boxId === boxId) continue;
6017
+ const running = this.boxes.some((b) => b.id === boxId && b.state === "running");
6018
+ if (!running) this.evictSession(boxId);
6019
+ }
5737
6020
  }
5738
6021
  /**
5739
6022
  * Diff the current box list against {@link promptStreams}: subscribe to
@@ -5758,7 +6041,7 @@ var Compositor = class {
5758
6041
  let changed = this.activePrompts.delete(boxId);
5759
6042
  if (this.activeNotices.delete(boxId)) changed = true;
5760
6043
  if (this.activeNotices.size === 0) this.stopNoticeSpinner();
5761
- if (changed) this.drawChrome();
6044
+ if (changed) this.redrawForAlert();
5762
6045
  }
5763
6046
  }
5764
6047
  for (const boxId of wanted) {
@@ -5769,21 +6052,21 @@ var Compositor = class {
5769
6052
  onPrompt: (ev) => {
5770
6053
  if (this.tornDown) return;
5771
6054
  this.activePrompts.set(boxId, ev);
5772
- this.drawChrome();
6055
+ this.redrawForAlert();
5773
6056
  },
5774
6057
  onResolved: (id) => {
5775
6058
  if (this.tornDown) return;
5776
6059
  const current = this.activePrompts.get(boxId);
5777
6060
  if (current && current.id === id) {
5778
6061
  this.activePrompts.delete(boxId);
5779
- this.drawChrome();
6062
+ this.redrawForAlert();
5780
6063
  }
5781
6064
  },
5782
6065
  onNotice: (ev) => {
5783
6066
  if (this.tornDown) return;
5784
6067
  this.activeNotices.set(boxId, ev);
5785
6068
  this.startNoticeSpinner();
5786
- this.drawChrome();
6069
+ this.redrawForAlert();
5787
6070
  },
5788
6071
  onNoticeCleared: (id) => {
5789
6072
  if (this.tornDown) return;
@@ -5791,7 +6074,7 @@ var Compositor = class {
5791
6074
  if (current && current.id === id) {
5792
6075
  this.activeNotices.delete(boxId);
5793
6076
  if (this.activeNotices.size === 0) this.stopNoticeSpinner();
5794
- this.drawChrome();
6077
+ this.redrawForAlert();
5795
6078
  }
5796
6079
  },
5797
6080
  onError: () => {
@@ -5818,9 +6101,11 @@ var Compositor = class {
5818
6101
  return this.boxes.find((b) => b.id === this.selectedId);
5819
6102
  }
5820
6103
  async poll() {
5821
- const before = JSON.stringify(
6104
+ const stateKey = () => JSON.stringify(
5822
6105
  this.boxes.map((b) => [b.id, b.state, b.activity, b.sessionTitle])
5823
6106
  );
6107
+ const before = stateKey();
6108
+ const beforeAlertH = this.alertHeight();
5824
6109
  await this.refreshBoxes();
5825
6110
  if (this.busy) {
5826
6111
  } else if (!this.boxes.some((b) => b.id === this.selectedId) && this.boxes[0]) {
@@ -5832,19 +6117,99 @@ var Compositor = class {
5832
6117
  const reresolve = this.session && !running || this.placeholder && running || this.menu && !running || this.lifecycleMenu != null && box?.state !== this.lifecycleMenu.state;
5833
6118
  if (reresolve) await this.spawnActive();
5834
6119
  }
5835
- if (JSON.stringify(
5836
- this.boxes.map((b) => [b.id, b.state, b.activity, b.sessionTitle])
5837
- ) !== before) {
6120
+ const stateChanged = stateKey() !== before;
6121
+ const alertChanged = this.alertHeight() !== beforeAlertH;
6122
+ if (alertChanged) {
6123
+ this.relayout();
6124
+ } else if (stateChanged) {
5838
6125
  this.drawChrome();
5839
6126
  }
5840
6127
  }
5841
- disposeSession() {
5842
- if (!this.session) return;
5843
- this.session.dispose();
6128
+ /**
6129
+ * Detach the active session from view. Keep-alive sessions (those in
6130
+ * {@link liveSessions}) stay running in the background; everything else is
6131
+ * disposed — matching the pre-pool dispose-on-switch behaviour for docker /
6132
+ * hetzner / daytona.
6133
+ */
6134
+ deactivateActive() {
6135
+ const s = this.session;
6136
+ if (!s) return;
6137
+ s.active = false;
6138
+ const pooled = this.liveSessions.get(s.boxId) === s;
6139
+ const boxRunning = this.boxes.some((b) => b.id === s.boxId && b.state === "running");
6140
+ if (!pooled || !boxRunning) {
6141
+ if (pooled) this.liveSessions.delete(s.boxId);
6142
+ s.dispose();
6143
+ }
6144
+ this.session = null;
6145
+ }
6146
+ /** Dispose and drop the pooled session for `boxId` (box gone / stopped /
6147
+ * its attach died). Clears the active reference if it was the shown one. */
6148
+ evictSession(boxId) {
6149
+ const pooled = this.liveSessions.get(boxId);
6150
+ if (pooled) {
6151
+ this.liveSessions.delete(boxId);
6152
+ pooled.dispose();
6153
+ }
6154
+ if (this.session && this.session.boxId === boxId) {
6155
+ if (this.session !== pooled) this.session.dispose();
6156
+ this.session = null;
6157
+ }
6158
+ }
6159
+ /** Bound the pool: evict least-recently-used pooled sessions (Map insertion
6160
+ * order) until at most {@link KEEP_ALIVE_MAX} remain, never the active one. */
6161
+ evictLruIfNeeded() {
6162
+ for (const boxId of this.liveSessions.keys()) {
6163
+ if (this.liveSessions.size <= KEEP_ALIVE_MAX) break;
6164
+ if (this.session && this.session.boxId === boxId) continue;
6165
+ this.evictSession(boxId);
6166
+ }
6167
+ }
6168
+ /** Dispose the active session plus every pooled one (teardown). */
6169
+ disposeAllSessions() {
6170
+ const active = this.session;
6171
+ if (active && this.liveSessions.get(active.boxId) !== active) active.dispose();
5844
6172
  this.session = null;
6173
+ for (const s of this.liveSessions.values()) s.dispose();
6174
+ this.liveSessions.clear();
6175
+ }
6176
+ /**
6177
+ * Show a pooled session in the right pane — the fast switch-back path: no
6178
+ * probe, no re-spawn, instant repaint from its already-current headless
6179
+ * buffer. Re-marks it most-recently-used and re-applies the current layout
6180
+ * size (it may have changed while hidden).
6181
+ */
6182
+ activateSession(sess) {
6183
+ this.deactivateActive();
6184
+ this.placeholder = null;
6185
+ this.menu = null;
6186
+ this.lifecycleMenu = null;
6187
+ this.createMenu = null;
6188
+ this.pendingConfirm = null;
6189
+ this.session = sess;
6190
+ sess.active = true;
6191
+ this.activeMode = sess.mode;
6192
+ sess.resize(Math.max(1, this.layout.right.w), Math.max(1, this.layout.right.h));
6193
+ this.liveSessions.delete(sess.boxId);
6194
+ this.liveSessions.set(sess.boxId, sess);
6195
+ this.prevRows = null;
6196
+ if (!this.syncAlertLayout()) this.drawChrome();
6197
+ this.scheduleRender();
6198
+ }
6199
+ /**
6200
+ * Show the selected box. If a kept-alive session is pooled for it, re-show it
6201
+ * synchronously (instant). Otherwise fall through to the async resolve+spawn.
6202
+ */
6203
+ showSelected() {
6204
+ const cached = this.liveSessions.get(this.selectedId);
6205
+ if (cached) {
6206
+ this.activateSession(cached);
6207
+ return;
6208
+ }
6209
+ void this.spawnActive();
5845
6210
  }
5846
6211
  async spawnActive() {
5847
- this.disposeSession();
6212
+ this.deactivateActive();
5848
6213
  this.placeholder = null;
5849
6214
  this.menu = null;
5850
6215
  this.lifecycleMenu = null;
@@ -5858,25 +6223,37 @@ var Compositor = class {
5858
6223
  }
5859
6224
  /** Turn a resolved/started target into the right-pane state. */
5860
6225
  applyTarget(target) {
5861
- this.disposeSession();
6226
+ this.deactivateActive();
5862
6227
  this.placeholder = null;
5863
6228
  this.menu = null;
5864
6229
  this.lifecycleMenu = null;
5865
6230
  this.createMenu = null;
5866
6231
  this.pendingConfirm = null;
5867
6232
  if (target.kind === "attach") {
5868
- this.activeMode = target.mode ?? "claude";
6233
+ const boxId = this.selectedId;
6234
+ const mode = target.mode ?? "claude";
6235
+ const keepAlive = target.keepAlive ?? false;
6236
+ this.activeMode = mode;
5869
6237
  this.session = new PtySession(
5870
6238
  this.deps.ptySpawn,
5871
6239
  this.deps.termCtor,
6240
+ boxId,
6241
+ keepAlive,
6242
+ mode,
5872
6243
  target.command,
5873
6244
  target.args,
5874
6245
  Math.max(1, this.layout.right.w),
5875
6246
  Math.max(1, this.layout.right.h),
5876
6247
  () => this.scheduleRender(),
5877
- () => this.onSessionExit(),
6248
+ (id) => this.onSessionExit(id),
5878
6249
  target.cleanup
5879
6250
  );
6251
+ if (keepAlive) {
6252
+ const prev = this.liveSessions.get(boxId);
6253
+ if (prev && prev !== this.session) prev.dispose();
6254
+ this.liveSessions.set(boxId, this.session);
6255
+ this.evictLruIfNeeded();
6256
+ }
5880
6257
  } else if (target.kind === "menu") {
5881
6258
  this.menu = { boxName: this.selectedBox()?.name ?? this.selectedId };
5882
6259
  } else if (target.kind === "lifecycle-menu") {
@@ -5891,7 +6268,7 @@ var Compositor = class {
5891
6268
  this.placeholder = target.lines;
5892
6269
  }
5893
6270
  this.prevRows = null;
5894
- this.drawChrome();
6271
+ if (!this.syncAlertLayout()) this.drawChrome();
5895
6272
  this.scheduleRender();
5896
6273
  }
5897
6274
  handleMenuKey(bytes) {
@@ -6224,8 +6601,9 @@ var Compositor = class {
6224
6601
  }, 2500);
6225
6602
  this.drawChrome();
6226
6603
  }
6227
- onSessionExit() {
6228
- this.disposeSession();
6604
+ onSessionExit(boxId) {
6605
+ this.evictSession(boxId);
6606
+ if (boxId !== this.selectedId) return;
6229
6607
  this.placeholder = ["", " session ended \u2014 Ctrl-a \u2191/\u2193 to switch boxes"];
6230
6608
  this.prevRows = null;
6231
6609
  this.scheduleRender();
@@ -6241,7 +6619,7 @@ var Compositor = class {
6241
6619
  const next = dir === "prev" ? (i - 1 + n) % n : (i + 1) % n;
6242
6620
  this.selectedId = this.boxes[next].id;
6243
6621
  this.drawChrome();
6244
- void this.spawnActive();
6622
+ this.showSelected();
6245
6623
  }
6246
6624
  /** Blank the right pane and drop the diff cache (next paint is full). */
6247
6625
  clearRightPane() {
@@ -6341,8 +6719,36 @@ var Compositor = class {
6341
6719
  }
6342
6720
  for (let y = 0; y < sidebar.h; y++)
6343
6721
  s += cursorTo2(sepX, y) + SB_HEADER + (y === 0 ? "\u256E" : "\u2502") + SGR_RESET;
6344
- let status;
6345
6722
  const activePromptForSelected = this.activePrompts.get(this.selectedId);
6723
+ const activeNoticeForSelected = this.activeNotices.get(this.selectedId);
6724
+ if (this.layout.alertH > 0) {
6725
+ const bandRows = this.layout.alertH;
6726
+ let bandLines = null;
6727
+ if (activePromptForSelected) {
6728
+ bandLines = renderAlertBand(
6729
+ { kind: "prompt", prompt: activePromptForSelected },
6730
+ this.layout.cols,
6731
+ bandRows
6732
+ );
6733
+ } else if (activeNoticeForSelected) {
6734
+ bandLines = renderAlertBand(
6735
+ { kind: "notice", message: activeNoticeForSelected.message, frame: this.noticeFrame },
6736
+ this.layout.cols,
6737
+ bandRows
6738
+ );
6739
+ } else {
6740
+ const q = this.selectedBox()?.claudeQuestion;
6741
+ if (q) {
6742
+ bandLines = renderAlertBand({ kind: "question", question: q }, this.layout.cols, bandRows);
6743
+ }
6744
+ }
6745
+ if (bandLines) {
6746
+ for (let i = 0; i < bandLines.length; i++) {
6747
+ s += cursorTo2(0, this.layout.alertY + i) + bandLines[i] + SGR_RESET;
6748
+ }
6749
+ }
6750
+ }
6751
+ let status;
6346
6752
  if (this.pendingConfirm) {
6347
6753
  const w = this.layout.cols;
6348
6754
  const txt = ` Destroy ${this.pendingConfirm.name}? y = confirm \xB7 any other key = cancel `.slice(0, w).padEnd(w);
@@ -6351,15 +6757,14 @@ var Compositor = class {
6351
6757
  const w = this.layout.cols;
6352
6758
  const txt = ` ${this.flashMsg} `.slice(0, w).padEnd(w);
6353
6759
  status = `\x1B[7m${txt}\x1B[0m`;
6354
- } else if (activePromptForSelected) {
6760
+ } else if (this.layout.alertH === 0 && activePromptForSelected) {
6355
6761
  status = renderFooter(
6356
6762
  { kind: "prompt", prompt: activePromptForSelected },
6357
6763
  this.layout.cols
6358
6764
  );
6359
- } else if (this.activeNotices.has(this.selectedId)) {
6360
- const notice = this.activeNotices.get(this.selectedId);
6765
+ } else if (this.layout.alertH === 0 && activeNoticeForSelected) {
6361
6766
  status = renderFooter(
6362
- { kind: "notice", message: notice.message, frame: this.noticeFrame },
6767
+ { kind: "notice", message: activeNoticeForSelected.message, frame: this.noticeFrame },
6363
6768
  this.layout.cols
6364
6769
  );
6365
6770
  } else {
@@ -6382,17 +6787,62 @@ var Compositor = class {
6382
6787
  if (this.resizeTimer) clearTimeout(this.resizeTimer);
6383
6788
  this.resizeTimer = setTimeout(() => {
6384
6789
  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();
6790
+ this.relayout();
6394
6791
  }, RESIZE_DEBOUNCE_MS);
6395
6792
  }
6793
+ /**
6794
+ * Requested band height for the currently-selected box. Returns
6795
+ * `ALERT_BAND_ROWS` when the box has an active relay prompt, an active
6796
+ * notice (checkpoint), or claude is in the `question` state with a payload;
6797
+ * 0 otherwise. The layout silently drops the band to 0 if reserving it
6798
+ * would push the right pane below MIN_RIGHT_H.
6799
+ */
6800
+ alertHeight() {
6801
+ const id = this.selectedId;
6802
+ if (this.activePrompts.has(id)) return ALERT_BAND_ROWS;
6803
+ if (this.activeNotices.has(id)) return ALERT_BAND_ROWS;
6804
+ const box = this.selectedBox();
6805
+ if (box?.claudeQuestion) return ALERT_BAND_ROWS;
6806
+ return 0;
6807
+ }
6808
+ /**
6809
+ * Recompute the layout against the current alert height, resize the inner
6810
+ * session, and repaint. Called from `scheduleResize` (terminal resize) and
6811
+ * from {@link syncAlertLayout} when the selected box's alert state flips.
6812
+ */
6813
+ relayout() {
6814
+ this.layout = computeLayout(
6815
+ this.out.columns ?? 100,
6816
+ this.out.rows ?? 30,
6817
+ this.alertHeight()
6818
+ );
6819
+ this.prevRows = null;
6820
+ const r = this.layout.right;
6821
+ if (this.session && !this.layout.tooSmall) {
6822
+ this.session.resize(Math.max(1, r.w), Math.max(1, r.h));
6823
+ }
6824
+ this.out.write(SYNC_BEGIN + "\x1B[2J" + SYNC_END);
6825
+ this.drawChrome();
6826
+ this.render();
6827
+ }
6828
+ /**
6829
+ * If the selected box's alert state implies a different band height than
6830
+ * the current layout, run a full {@link relayout}; otherwise return false
6831
+ * so the caller can take the lighter `drawChrome()` path. Used by all
6832
+ * alert-state transitions (SSE handlers, poll, selection change).
6833
+ */
6834
+ syncAlertLayout() {
6835
+ if (this.alertHeight() !== this.layout.alertH) {
6836
+ this.relayout();
6837
+ return true;
6838
+ }
6839
+ return false;
6840
+ }
6841
+ /** Common path for alert-state transitions: relayout when the band's
6842
+ * visibility changes, drawChrome only when it doesn't. */
6843
+ redrawForAlert() {
6844
+ if (!this.syncAlertLayout()) this.drawChrome();
6845
+ }
6396
6846
  teardown() {
6397
6847
  if (this.tornDown) return;
6398
6848
  this.tornDown = true;
@@ -6407,7 +6857,7 @@ var Compositor = class {
6407
6857
  this.activePrompts.clear();
6408
6858
  this.activeNotices.clear();
6409
6859
  this.parser.dispose();
6410
- this.disposeSession();
6860
+ this.disposeAllSessions();
6411
6861
  this.inp.off("data", this.onData);
6412
6862
  this.out.off("resize", this.onResize);
6413
6863
  if (this.inp.isTTY) this.inp.setRawMode(false);
@@ -6419,6 +6869,9 @@ var Compositor = class {
6419
6869
  };
6420
6870
 
6421
6871
  // src/commands/dashboard.ts
6872
+ function providerSupportsKeepAlive(provider) {
6873
+ return provider === "vercel";
6874
+ }
6422
6875
  function sortBoxes(boxes) {
6423
6876
  return [...boxes].sort((a, b) => {
6424
6877
  const ap = a.projectRoot ?? "";
@@ -6455,7 +6908,10 @@ function toSidebar(b) {
6455
6908
  activity: agent.activity,
6456
6909
  sessionTitle: agent.sessionTitle,
6457
6910
  index: b.projectIndex,
6458
- project: b.projectRoot
6911
+ project: b.projectRoot,
6912
+ // Only claude reports an AskUserQuestion payload; gated on claudeActivity
6913
+ // === 'question' upstream (lifecycle.listBoxes) so it's undefined otherwise.
6914
+ claudeQuestion: b.claudeQuestion
6459
6915
  };
6460
6916
  }
6461
6917
  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 +7034,8 @@ var dashboardCommand = new Command10("dashboard").description("Box list + the se
6578
7034
  command: spec.argv[0],
6579
7035
  args: spec.argv.slice(1),
6580
7036
  ...spec.cleanup ? { cleanup: spec.cleanup } : {},
6581
- mode: which
7037
+ mode: which,
7038
+ ...providerSupportsKeepAlive(record.provider) ? { keepAlive: true } : {}
6582
7039
  };
6583
7040
  };
6584
7041
  const isCloudBox = (box) => (box.provider ?? "docker") !== "docker";
@@ -6612,7 +7069,12 @@ var dashboardCommand = new Command10("dashboard").description("Box list + the se
6612
7069
  { volume: claudeVolume },
6613
7070
  { image: box.image, isolate: claudeVolume !== SHARED_CLAUDE_VOLUME }
6614
7071
  );
6615
- await startClaudeSession({ container: box.container, claudeArgs: [], boxName: box.name });
7072
+ const claudeCfg = await loadEffectiveConfig(box.workspacePath);
7073
+ await startClaudeSession({
7074
+ container: box.container,
7075
+ claudeArgs: applyClaudeSkipPermissions([], claudeCfg.effective),
7076
+ boxName: box.name
7077
+ });
6616
7078
  const info = await claudeSessionInfo(box.container);
6617
7079
  await waitForTmuxPaneContent(box.container, info.sessionName);
6618
7080
  return {
@@ -6629,7 +7091,11 @@ var dashboardCommand = new Command10("dashboard").description("Box list + the se
6629
7091
  }
6630
7092
  await ensureCodexInstalled(box.container);
6631
7093
  if (box.codexConfigVolume) await seedCodexHooks(box.codexConfigVolume, box.image);
6632
- await startCodexSession({ container: box.container, codexArgs: [] });
7094
+ const codexCfg = await loadEffectiveConfig(box.workspacePath);
7095
+ await startCodexSession({
7096
+ container: box.container,
7097
+ codexArgs: applyCodexSkipPermissions([], codexCfg.effective)
7098
+ });
6633
7099
  await waitForTmuxPaneContent(box.container, DEFAULT_CODEX_SESSION);
6634
7100
  return {
6635
7101
  kind: "attach",
@@ -6698,7 +7164,10 @@ var dashboardCommand = new Command10("dashboard").description("Box list + the se
6698
7164
  if (!agent) return { boxId: result.record.id };
6699
7165
  if (agent === "codex") {
6700
7166
  await ensureCodexInstalled(ctr, { onProgress });
6701
- await startCodexSession({ container: ctr, codexArgs: [] });
7167
+ await startCodexSession({
7168
+ container: ctr,
7169
+ codexArgs: applyCodexSkipPermissions([], cfg.effective)
7170
+ });
6702
7171
  await waitForTmuxPaneContent(ctr, DEFAULT_CODEX_SESSION);
6703
7172
  return {
6704
7173
  boxId: result.record.id,
@@ -6725,7 +7194,11 @@ var dashboardCommand = new Command10("dashboard").description("Box list + the se
6725
7194
  };
6726
7195
  }
6727
7196
  await rebuildPluginNativeDeps(ctr, { volume: result.record.claudeConfigVolume });
6728
- await startClaudeSession({ container: ctr, claudeArgs: [], boxName: result.record.name });
7197
+ await startClaudeSession({
7198
+ container: ctr,
7199
+ claudeArgs: applyClaudeSkipPermissions([], cfg.effective),
7200
+ boxName: result.record.name
7201
+ });
6729
7202
  const info = await claudeSessionInfo(ctr);
6730
7203
  await waitForTmuxPaneContent(ctr, info.sessionName);
6731
7204
  return {
@@ -7801,11 +8274,11 @@ function resolveToken(raw) {
7801
8274
  // src/lib/drive/tmux.ts
7802
8275
  var TMUX_USER = "vscode";
7803
8276
  async function captureSession(provider, box, session, opts = {}) {
7804
- const argv = ["tmux", "capture-pane", opts.ansi ? "-pe" : "-p", "-t", session];
8277
+ const argv2 = ["tmux", "capture-pane", opts.ansi ? "-pe" : "-p", "-t", session];
7805
8278
  if (opts.rows) {
7806
- argv.push("-S", String(opts.rows.from), "-E", String(opts.rows.to));
8279
+ argv2.push("-S", String(opts.rows.from), "-E", String(opts.rows.to));
7807
8280
  }
7808
- const res = await provider.exec(box, argv, { user: TMUX_USER });
8281
+ const res = await provider.exec(box, argv2, { user: TMUX_USER });
7809
8282
  if (res.exitCode !== 0) {
7810
8283
  throw new Error(failure("capture-pane", session, res.stderr || res.stdout));
7811
8284
  }
@@ -8062,8 +8535,8 @@ function sleep2(ms) {
8062
8535
  import { log as log28 } from "@clack/prompts";
8063
8536
  import { Command as Command23 } from "commander";
8064
8537
  import { existsSync as existsSync4, readdirSync, statSync } from "fs";
8065
- import { homedir as homedir8 } from "os";
8066
- import { join as join10 } from "path";
8538
+ import { homedir as homedir10 } from "os";
8539
+ import { join as join12 } from "path";
8067
8540
  var FORK_AGENTS = ["claude", "codex", "opencode"];
8068
8541
  var AGENT_COMMAND = {
8069
8542
  claude: claudeCommand,
@@ -8083,12 +8556,12 @@ function resolveSessionArgs(agent, opts) {
8083
8556
  }
8084
8557
  if (opts.session) return ["--resume", opts.session];
8085
8558
  if (agent === "codex") return ["--continue"];
8086
- const dir = join10(homedir8(), ".claude", "projects", encodeClaudeProjectsDir(opts.workspace));
8559
+ const dir = join12(homedir10(), ".claude", "projects", encodeClaudeProjectsDir(opts.workspace));
8087
8560
  if (!existsSync4(dir)) return ["--continue"];
8088
8561
  const now = Date.now();
8089
8562
  const recent = readdirSync(dir).filter((f) => f.endsWith(".jsonl")).map((f) => {
8090
8563
  try {
8091
- return statSync(join10(dir, f)).mtimeMs;
8564
+ return statSync(join12(dir, f)).mtimeMs;
8092
8565
  } catch {
8093
8566
  return 0;
8094
8567
  }
@@ -8166,115 +8639,1003 @@ var forkCommand = new Command23("fork").description(
8166
8639
  });
8167
8640
 
8168
8641
  // 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";
8642
+ 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";
8643
+ import { Command as Command25 } from "commander";
8644
+ import { existsSync as existsSync6, mkdirSync as mkdirSync5, readFileSync, writeFileSync as writeFileSync4 } from "fs";
8645
+ import { homedir as homedir13 } from "os";
8646
+ import { dirname as dirname2, join as join15, resolve as resolve2 } from "path";
8174
8647
  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;
8648
+
8649
+ // src/lib/doctor-checks.ts
8650
+ import { accessSync, constants as fsConstants, mkdirSync as mkdirSync3 } from "fs";
8651
+ import { homedir as homedir11 } from "os";
8652
+ import { join as join13 } from "path";
8653
+ import { execa as execa2 } from "execa";
8654
+ var ALL_PROVIDERS = ["docker", "daytona", "hetzner", "vercel"];
8655
+ var NODE_MIN_MAJOR = 20;
8656
+ var NODE_MIN_MINOR = 10;
8657
+ async function probeVersion(bin, args = ["--version"]) {
8658
+ try {
8659
+ const r = await execa2(bin, args, { reject: false });
8660
+ if (r.exitCode !== 0) return null;
8661
+ const out = `${r.stdout ?? ""}${r.stderr ?? ""}`.trim().split("\n")[0] ?? "";
8662
+ return out.length > 0 ? out : bin;
8663
+ } catch {
8664
+ return null;
8205
8665
  }
8206
- throw new Error(
8207
- `could not locate bundled host skills; tried:
8208
- ${candidates.join("\n ")}`
8209
- );
8210
8666
  }
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";
8667
+ function parseNodeMajorMinor(v) {
8668
+ const m = /^v?(\d+)\.(\d+)/.exec(v);
8669
+ if (!m) return [0, 0];
8670
+ return [Number(m[1]), Number(m[2])];
8218
8671
  }
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;
8672
+ function firstLine(s) {
8673
+ const i = s.indexOf("\n");
8674
+ return i === -1 ? s : s.slice(0, i);
8675
+ }
8676
+ function errSummary(err) {
8677
+ return err instanceof Error ? firstLine(err.message) : String(err);
8678
+ }
8679
+ function checkNode() {
8680
+ const v = process.versions.node;
8681
+ const [maj, min] = parseNodeMajorMinor(v);
8682
+ const ok = maj > NODE_MIN_MAJOR || maj === NODE_MIN_MAJOR && min >= NODE_MIN_MINOR;
8683
+ return {
8684
+ label: "node",
8685
+ status: ok ? "ok" : "fail",
8686
+ detail: ok ? `v${v}` : `v${v} (need >=${String(NODE_MIN_MAJOR)}.${String(NODE_MIN_MINOR)})`,
8687
+ hint: ok ? void 0 : "upgrade Node before continuing"
8688
+ };
8689
+ }
8690
+ function checkPlatform() {
8691
+ return { label: "platform", status: "ok", detail: `${process.platform}/${process.arch}` };
8692
+ }
8693
+ function checkAgentboxHome() {
8694
+ const dir = join13(homedir11(), ".agentbox");
8226
8695
  try {
8227
- srcDir = resolveHostSkillsDir();
8696
+ mkdirSync3(dir, { recursive: true });
8697
+ accessSync(dir, fsConstants.W_OK);
8698
+ return { label: "~/.agentbox", status: "ok", detail: dir };
8228
8699
  } catch (err) {
8229
- log29.error(err instanceof Error ? err.message : String(err));
8230
- process.exit(1);
8700
+ return {
8701
+ label: "~/.agentbox",
8702
+ status: "fail",
8703
+ detail: `not writable: ${err instanceof Error ? err.message : String(err)}`,
8704
+ hint: "check directory permissions"
8705
+ };
8231
8706
  }
8232
- const written = [];
8233
- let skipped = 0;
8707
+ }
8708
+ async function checkGit() {
8709
+ const v = await probeVersion("git");
8710
+ return v ? { label: "git", status: "ok", detail: v } : {
8711
+ label: "git",
8712
+ status: "warn",
8713
+ detail: "not found",
8714
+ hint: "install git \u2014 required for the workspace git-bundle seed"
8715
+ };
8716
+ }
8717
+ async function checkSsh() {
8718
+ const v = await probeVersion("ssh", ["-V"]);
8719
+ return v ? { label: "ssh", status: "ok", detail: v } : {
8720
+ label: "ssh",
8721
+ status: "warn",
8722
+ detail: "not found",
8723
+ hint: "install ssh \u2014 required for hetzner and cloud attach"
8724
+ };
8725
+ }
8726
+ async function runSystemChecks() {
8727
+ const [git, ssh] = await Promise.all([checkGit(), checkSsh()]);
8728
+ return [checkNode(), checkPlatform(), checkAgentboxHome(), git, ssh];
8729
+ }
8730
+ async function dockerChecks() {
8731
+ const cli = await probeVersion("docker");
8732
+ if (!cli) {
8733
+ return [
8734
+ {
8735
+ label: "docker cli",
8736
+ status: "warn",
8737
+ detail: "not found",
8738
+ hint: "install Docker Desktop, OrbStack, or docker engine"
8739
+ }
8740
+ ];
8741
+ }
8742
+ const cliRes = { label: "docker cli", status: "ok", detail: cli };
8743
+ const info = await execa2("docker", ["info"], { reject: false });
8744
+ if (info.exitCode !== 0) {
8745
+ return [
8746
+ cliRes,
8747
+ {
8748
+ label: "docker daemon",
8749
+ status: "warn",
8750
+ detail: "unreachable",
8751
+ hint: "start Docker (Desktop / OrbStack / `systemctl start docker`)"
8752
+ }
8753
+ ];
8754
+ }
8755
+ const daemonRes = { label: "docker daemon", status: "ok", detail: "reachable" };
8756
+ const mod = await import("./dist-BQNX7RQE.js");
8757
+ let imgRes;
8758
+ try {
8759
+ const img = await mod.imageInfo(mod.DEFAULT_BOX_IMAGE);
8760
+ imgRes = img.exists ? { label: "box image", status: "ok", detail: `${mod.DEFAULT_BOX_IMAGE} built` } : {
8761
+ label: "box image",
8762
+ status: "warn",
8763
+ detail: `${mod.DEFAULT_BOX_IMAGE} not built`,
8764
+ hint: "run `agentbox prepare --provider docker` (or let the wizard do it)"
8765
+ };
8766
+ } catch (err) {
8767
+ imgRes = {
8768
+ label: "box image",
8769
+ status: "warn",
8770
+ detail: errSummary(err)
8771
+ };
8772
+ }
8773
+ const volNames = [mod.SHARED_CLAUDE_VOLUME, mod.SHARED_CODEX_VOLUME, mod.SHARED_OPENCODE_VOLUME];
8774
+ const vols = await Promise.all(
8775
+ volNames.map(async (n) => ({ name: n, exists: await mod.volumeExists(n).catch(() => false) }))
8776
+ );
8777
+ const present = vols.filter((v) => v.exists).length;
8778
+ const volRes = {
8779
+ label: "shared volumes",
8780
+ status: "ok",
8781
+ detail: `${String(present)}/${String(vols.length)} present (seeded lazily)`
8782
+ };
8783
+ return [cliRes, daemonRes, imgRes, volRes];
8784
+ }
8785
+ async function daytonaChecks() {
8786
+ try {
8787
+ const mod = await import("./dist-TMHSUVTP.js");
8788
+ const status = await mod.getDaytonaStatus();
8789
+ if (!status.configured) {
8790
+ return [
8791
+ {
8792
+ label: "credentials",
8793
+ status: "warn",
8794
+ detail: status.reason ?? "not configured",
8795
+ hint: "`agentbox daytona login`"
8796
+ }
8797
+ ];
8798
+ }
8799
+ const credRes = { label: "credentials", status: "ok", detail: "configured" };
8800
+ const snapRes = status.snapshots.length > 0 ? {
8801
+ label: "base snapshot",
8802
+ status: "ok",
8803
+ detail: `${String(status.snapshots.length)} agentbox snapshot(s)`
8804
+ } : {
8805
+ label: "base snapshot",
8806
+ status: "warn",
8807
+ detail: "none",
8808
+ hint: "`agentbox prepare --provider daytona`"
8809
+ };
8810
+ return [credRes, snapRes];
8811
+ } catch (err) {
8812
+ return [
8813
+ {
8814
+ label: "credentials",
8815
+ status: "warn",
8816
+ detail: errSummary(err)
8817
+ }
8818
+ ];
8819
+ }
8820
+ }
8821
+ async function hetznerChecks() {
8822
+ try {
8823
+ const mod = await import("./dist-5FQGYRW5.js");
8824
+ const cred = mod.readHetznerCredStatus();
8825
+ const credRes = cred.source === "none" ? {
8826
+ label: "credentials",
8827
+ status: "warn",
8828
+ detail: "HCLOUD_TOKEN not set",
8829
+ hint: "`agentbox hetzner login`"
8830
+ } : { label: "credentials", status: "ok", detail: `token from ${cred.source}` };
8831
+ const prepared = mod.readPreparedState();
8832
+ const snapRes = prepared.base?.imageId ? {
8833
+ label: "base snapshot",
8834
+ status: "ok",
8835
+ detail: `image ${String(prepared.base.imageId)} (${prepared.base.cliVersion ?? "\u2014"})`
8836
+ } : {
8837
+ label: "base snapshot",
8838
+ status: "warn",
8839
+ detail: "not baked",
8840
+ hint: "`agentbox prepare --provider hetzner`"
8841
+ };
8842
+ return [credRes, snapRes];
8843
+ } catch (err) {
8844
+ return [
8845
+ {
8846
+ label: "credentials",
8847
+ status: "warn",
8848
+ detail: errSummary(err)
8849
+ }
8850
+ ];
8851
+ }
8852
+ }
8853
+ async function vercelChecks() {
8854
+ try {
8855
+ const mod = await import("./dist-PZW3GWWU.js");
8856
+ const cred = mod.readVercelCredStatus();
8857
+ const credRes = cred.auth === "none" ? {
8858
+ label: "credentials",
8859
+ status: "warn",
8860
+ detail: "not configured",
8861
+ hint: "`agentbox vercel login`"
8862
+ } : {
8863
+ label: "credentials",
8864
+ status: "ok",
8865
+ detail: `${cred.auth} (${cred.source})`
8866
+ };
8867
+ const prepared = mod.readPreparedState();
8868
+ const snapRes = prepared.base?.snapshotId ? {
8869
+ label: "base snapshot",
8870
+ status: "ok",
8871
+ detail: `${prepared.base.snapshotId.slice(0, 16)}\u2026 (${prepared.base.cliVersion ?? "\u2014"})`
8872
+ } : {
8873
+ label: "base snapshot",
8874
+ status: "warn",
8875
+ detail: "not baked",
8876
+ hint: "`agentbox prepare --provider vercel`"
8877
+ };
8878
+ return [credRes, snapRes];
8879
+ } catch (err) {
8880
+ return [
8881
+ {
8882
+ label: "credentials",
8883
+ status: "warn",
8884
+ detail: errSummary(err)
8885
+ }
8886
+ ];
8887
+ }
8888
+ }
8889
+ async function runProviderChecks(name) {
8890
+ let results;
8891
+ switch (name) {
8892
+ case "docker":
8893
+ results = await dockerChecks();
8894
+ break;
8895
+ case "daytona":
8896
+ results = await daytonaChecks();
8897
+ break;
8898
+ case "hetzner":
8899
+ results = await hetznerChecks();
8900
+ break;
8901
+ case "vercel":
8902
+ results = await vercelChecks();
8903
+ break;
8904
+ }
8905
+ return { title: name, results };
8906
+ }
8907
+ async function runAllChecks() {
8908
+ const sys = { title: "system", results: await runSystemChecks() };
8909
+ const providerGroups = await Promise.all(ALL_PROVIDERS.map((n) => runProviderChecks(n)));
8910
+ return [sys, ...providerGroups];
8911
+ }
8912
+ function worstInResults(results) {
8913
+ let worst = "ok";
8914
+ for (const r of results) {
8915
+ if (r.status === "fail") return "fail";
8916
+ if (r.status === "warn") worst = "warn";
8917
+ }
8918
+ return worst;
8919
+ }
8920
+ function worstStatus(groups) {
8921
+ let worst = "ok";
8922
+ for (const g of groups) {
8923
+ const w = worstInResults(g.results);
8924
+ if (w === "fail") return "fail";
8925
+ if (w === "warn") worst = "warn";
8926
+ }
8927
+ return worst;
8928
+ }
8929
+ function summaryToken(group) {
8930
+ const worst = worstInResults(group.results);
8931
+ if (group.title === "system") {
8932
+ if (worst === "fail") return "system FAIL";
8933
+ if (worst === "warn") return "system warn";
8934
+ return "system ok";
8935
+ }
8936
+ if (worst === "fail") return `${group.title} FAIL`;
8937
+ if (worst === "warn") {
8938
+ const cred = group.results.find((r) => r.label === "credentials");
8939
+ if (cred && cred.status === "warn") return `${group.title} login needed`;
8940
+ return `${group.title} not prepared`;
8941
+ }
8942
+ return `${group.title} ready`;
8943
+ }
8944
+ var C_GREEN = "\x1B[32m";
8945
+ var C_YELLOW = "\x1B[33m";
8946
+ var C_RED = "\x1B[31m";
8947
+ var C_RESET = "\x1B[0m";
8948
+ var COLOR = !process.env.NO_COLOR;
8949
+ function statusMarker(s) {
8950
+ const glyph = s === "ok" ? "\u2713" : s === "warn" ? "\u26A0" : "\u2717";
8951
+ if (!COLOR) return glyph;
8952
+ const color = s === "ok" ? C_GREEN : s === "warn" ? C_YELLOW : C_RED;
8953
+ return `${color}${glyph}${C_RESET}`;
8954
+ }
8955
+ function formatCompact(groups) {
8956
+ return groups.map((g) => `${statusMarker(worstInResults(g.results))} ${summaryToken(g)}`).join(" \xB7 ");
8957
+ }
8958
+ function pad2(s, width) {
8959
+ return s.length >= width ? s : s + " ".repeat(width - s.length);
8960
+ }
8961
+ function statusBadge(s) {
8962
+ if (s === "ok") return "[ ok ]";
8963
+ if (s === "warn") return "[warn]";
8964
+ return "[FAIL]";
8965
+ }
8966
+ function formatDetailed(groups) {
8967
+ const lines = [];
8968
+ for (const g of groups) {
8969
+ if (lines.length > 0) lines.push("");
8970
+ lines.push(`${g.title}:`);
8971
+ for (const r of g.results) {
8972
+ const badge = statusBadge(r.status);
8973
+ const tail = r.hint ? ` (${r.hint})` : "";
8974
+ lines.push(` ${badge} ${pad2(r.label, 18)} ${r.detail}${tail}`);
8975
+ }
8976
+ }
8977
+ return lines;
8978
+ }
8979
+
8980
+ // src/lib/first-run.ts
8981
+ import { existsSync as existsSync5, mkdirSync as mkdirSync4, writeFileSync as writeFileSync3 } from "fs";
8982
+ import { homedir as homedir12 } from "os";
8983
+ import { dirname, join as join14 } from "path";
8984
+ var MARKER_VERSION = 1;
8985
+ function setupMarkerPath() {
8986
+ return join14(homedir12(), ".agentbox", "setup-complete.json");
8987
+ }
8988
+ function isFirstRun() {
8989
+ return !existsSync5(setupMarkerPath());
8990
+ }
8991
+ function markSetupComplete(provider) {
8992
+ const path = setupMarkerPath();
8993
+ mkdirSync4(dirname(path), { recursive: true });
8994
+ const body = {
8995
+ version: MARKER_VERSION,
8996
+ completedAt: (/* @__PURE__ */ new Date()).toISOString(),
8997
+ provider
8998
+ };
8999
+ writeFileSync3(path, JSON.stringify(body, null, 2) + "\n");
9000
+ }
9001
+
9002
+ // src/commands/prepare.ts
9003
+ import { intro as intro5, log as log29, spinner as spinner7 } from "@clack/prompts";
9004
+ import { Command as Command24 } from "commander";
9005
+ async function dockerStatus() {
9006
+ let img;
9007
+ try {
9008
+ img = await imageInfo(DEFAULT_BOX_IMAGE);
9009
+ } catch {
9010
+ return { daemon: "unreachable", volumes: [] };
9011
+ }
9012
+ const names = [SHARED_CLAUDE_VOLUME, SHARED_CODEX_VOLUME, SHARED_OPENCODE_VOLUME];
9013
+ const volumes = await Promise.all(
9014
+ names.map(async (name) => ({ name, exists: await volumeExists(name).catch(() => false) }))
9015
+ );
9016
+ return { daemon: "reachable", image: img, volumes };
9017
+ }
9018
+ function humanBytes(n) {
9019
+ if (n === void 0 || !Number.isFinite(n)) return "\u2014";
9020
+ if (n >= 1024 ** 3) return `${(n / 1024 ** 3).toFixed(2)} GB`;
9021
+ if (n >= 1024 ** 2) return `${(n / 1024 ** 2).toFixed(1)} MB`;
9022
+ if (n >= 1024) return `${(n / 1024).toFixed(1)} KB`;
9023
+ return `${String(n)} B`;
9024
+ }
9025
+ function humanAge(iso) {
9026
+ if (!iso) return "\u2014";
9027
+ const t = Date.parse(iso);
9028
+ if (!Number.isFinite(t)) return iso;
9029
+ const ageSec = Math.max(0, (Date.now() - t) / 1e3);
9030
+ if (ageSec < 60) return `${ageSec.toFixed(0)}s ago`;
9031
+ if (ageSec < 3600) return `${(ageSec / 60).toFixed(0)}m ago`;
9032
+ if (ageSec < 86400) return `${(ageSec / 3600).toFixed(1)}h ago`;
9033
+ return `${(ageSec / 86400).toFixed(1)}d ago`;
9034
+ }
9035
+ function pad3(s, width) {
9036
+ return s.length >= width ? s : s + " ".repeat(width - s.length);
9037
+ }
9038
+ async function renderDocker(status) {
9039
+ const out = ["docker:"];
9040
+ if (status.daemon === "unreachable") {
9041
+ out.push(" docker daemon unreachable (is Docker running?)");
9042
+ return out;
9043
+ }
9044
+ if (!status.image?.exists) {
9045
+ out.push(
9046
+ ` image ${DEFAULT_BOX_IMAGE} (not built \u2014 run \`agentbox prepare --provider docker\`)`
9047
+ );
9048
+ } else {
9049
+ out.push(
9050
+ ` image ${pad3(DEFAULT_BOX_IMAGE, 30)} ${pad3(humanBytes(status.image.sizeBytes), 10)} built ${humanAge(status.image.createdAt)}`
9051
+ );
9052
+ }
9053
+ for (const v of status.volumes) {
9054
+ if (v.exists) {
9055
+ out.push(` vol ${pad3(v.name, 30)} present`);
9056
+ } else {
9057
+ out.push(
9058
+ ` vol ${pad3(v.name, 30)} (none \u2014 seeded lazily on first \`agentbox claude/codex/opencode\`)`
9059
+ );
9060
+ }
9061
+ }
9062
+ return out;
9063
+ }
9064
+ async function daytonaStatus() {
9065
+ try {
9066
+ const mod = await import("./dist-TMHSUVTP.js");
9067
+ return await mod.getDaytonaStatus();
9068
+ } catch (err) {
9069
+ return {
9070
+ configured: false,
9071
+ reason: err instanceof Error ? err.message.split("\n")[0] : String(err)
9072
+ };
9073
+ }
9074
+ }
9075
+ function renderDaytona(status, pinnedImage) {
9076
+ const out = ["daytona:"];
9077
+ if (!status.configured) {
9078
+ out.push(
9079
+ ` (not configured \u2014 \`agentbox daytona login\` to set up${status.reason ? `; ${status.reason}` : ""})`
9080
+ );
9081
+ return out;
9082
+ }
9083
+ if (status.reason) out.push(` warn: ${status.reason}`);
9084
+ if (status.snapshots.length === 0) {
9085
+ out.push(" no agentbox snapshots \u2014 run `agentbox prepare --provider daytona`");
9086
+ } else {
9087
+ for (const s of status.snapshots) {
9088
+ const sizeStr = s.sizeGb !== void 0 ? `${s.sizeGb.toFixed(2)} GB` : "\u2014";
9089
+ const pinned = pinnedImage && pinnedImage === s.name ? " (pinned in project)" : "";
9090
+ const tail = s.state === "error" && s.errorReason ? ` error: ${s.errorReason.slice(0, 80)}` : ` ${humanAge(s.createdAt)}`;
9091
+ out.push(
9092
+ ` snap ${pad3(s.name, 40)} ${pad3(s.state ?? "\u2014", 10)} ${pad3(sizeStr, 10)}${tail}${pinned}`
9093
+ );
9094
+ }
9095
+ }
9096
+ if (status.volumes.length === 0) {
9097
+ out.push(" no agentbox volumes \u2014 created lazily on first cloud `agentbox create`");
9098
+ } else {
9099
+ for (const v of status.volumes) {
9100
+ const last = v.lastUsedAt ? ` last used ${humanAge(v.lastUsedAt)}` : "";
9101
+ out.push(` vol ${pad3(v.name, 40)} ${pad3(v.state ?? "\u2014", 10)}${last}`);
9102
+ }
9103
+ }
9104
+ return out;
9105
+ }
9106
+ async function showStatus(opts) {
9107
+ const cfg = await loadEffectiveConfig(process.cwd()).catch(() => null);
9108
+ const pinnedRaw = cfg?.effective.box.image;
9109
+ const pinned = typeof pinnedRaw === "string" && pinnedRaw.length > 0 && pinnedRaw !== DEFAULT_BOX_IMAGE ? pinnedRaw : void 0;
9110
+ const lines = [];
9111
+ const wantDocker = !opts.onlyProvider || opts.onlyProvider === "docker";
9112
+ const wantDaytona = !opts.onlyProvider || opts.onlyProvider === "daytona";
9113
+ if (wantDocker) {
9114
+ const status = await dockerStatus();
9115
+ lines.push(...await renderDocker(status));
9116
+ }
9117
+ if (wantDaytona) {
9118
+ if (lines.length > 0) lines.push("");
9119
+ const status = await daytonaStatus();
9120
+ lines.push(...renderDaytona(status, pinned));
9121
+ }
9122
+ if (pinned) {
9123
+ lines.push("");
9124
+ lines.push(`project pin: box.image = ${pinned}`);
9125
+ }
9126
+ process.stdout.write(lines.join("\n") + "\n");
9127
+ }
9128
+ async function runPrepare(providerName, opts = {}) {
9129
+ if (!isKnownProvider(providerName)) {
9130
+ process.stderr.write("error: --provider must be one of: docker, daytona, hetzner, vercel\n");
9131
+ process.exit(1);
9132
+ }
9133
+ if (providerName === "daytona" && !opts.yes && process.stdin.isTTY) {
9134
+ process.stdout.write(
9135
+ "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"
9136
+ );
9137
+ }
9138
+ const provider = await getProvider(providerName);
9139
+ if (typeof provider.prepare !== "function") {
9140
+ log29.error(`provider '${providerName}' does not implement prepare`);
9141
+ process.exit(1);
9142
+ }
9143
+ const cwd = opts.cwd ?? process.cwd();
9144
+ const sp = spinner7();
9145
+ sp.start(`preparing ${providerName}\u2026`);
9146
+ try {
9147
+ const result = await provider.prepare({
9148
+ name: opts.name,
9149
+ hostWorkspace: cwd,
9150
+ force: opts.force,
9151
+ onLog: (line) => sp.message(line.slice(0, 80))
9152
+ });
9153
+ if (result.snapshotName !== void 0) {
9154
+ sp.stop(`prepared ${providerName}: snapshot '${result.snapshotName}'`);
9155
+ try {
9156
+ const written = await setConfigValue("project", "box.image", result.snapshotName, cwd);
9157
+ log29.success(`box.image = ${result.snapshotName} (written to ${written.path})`);
9158
+ } catch (err) {
9159
+ const msg = err instanceof Error ? err.message : String(err);
9160
+ log29.warn(
9161
+ `prepared snapshot '${result.snapshotName}', but failed to pin it into the project config: ${msg}
9162
+ Run \`agentbox config set --project box.image ${result.snapshotName}\` manually.`
9163
+ );
9164
+ }
9165
+ } else {
9166
+ sp.stop(`${providerName.slice(0, 1).toUpperCase() + providerName.slice(1)} provider ready`);
9167
+ }
9168
+ if (!opts.suppressStatus) {
9169
+ process.stdout.write("\n");
9170
+ await showStatus({ onlyProvider: providerName });
9171
+ }
9172
+ if (!opts.suppressTip) {
9173
+ log29.info(
9174
+ "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"
9175
+ );
9176
+ }
9177
+ } catch (err) {
9178
+ sp.stop(`prepare failed: ${describeError(err)}`);
9179
+ throw err;
9180
+ }
9181
+ }
9182
+ var prepareCommand = new Command24("prepare").description(
9183
+ "Build base sandbox images / snapshots, or show what is already prepared across providers."
9184
+ ).option(
9185
+ "-p, --provider <name>",
9186
+ "provider to prepare (docker | daytona | hetzner | vercel). Omit for status-only."
9187
+ ).option("-n, --name <name>", "snapshot name (Daytona only; default: agentbox-base-<timestamp>)").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) => {
9188
+ if (!opts.provider || opts.status) {
9189
+ await showStatus({});
9190
+ return;
9191
+ }
9192
+ const providerName = opts.provider.trim();
9193
+ intro5(`preparing ${providerName} base image`);
9194
+ await runPrepare(providerName, {
9195
+ name: opts.name,
9196
+ force: opts.force,
9197
+ yes: opts.yes
9198
+ });
9199
+ });
9200
+ function describeError(err) {
9201
+ if (!(err instanceof Error)) return String(err);
9202
+ const parts = [err.message];
9203
+ let cause = err.cause;
9204
+ for (let i = 0; i < 5 && cause; i++) {
9205
+ if (cause instanceof Error) {
9206
+ parts.push(`caused by: ${cause.message}`);
9207
+ const code = cause.code;
9208
+ if (typeof code === "string") parts.push(`(${code})`);
9209
+ cause = cause.cause;
9210
+ } else if (typeof cause === "object") {
9211
+ parts.push(`caused by: ${JSON.stringify(cause)}`);
9212
+ break;
9213
+ } else {
9214
+ parts.push(`caused by: ${String(cause)}`);
9215
+ break;
9216
+ }
9217
+ }
9218
+ return parts.join(" \u2014 ");
9219
+ }
9220
+
9221
+ // src/commands/install.ts
9222
+ var MANAGED_SENTINEL = "<!-- agentbox-managed:v1 -->";
9223
+ 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";
9224
+ 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";
9225
+ var LOGO_WIDTH = LOGO_L1.length;
9226
+ var BANNER = (() => {
9227
+ const art = `${LOGO_L1}
9228
+ ${LOGO_L2}`;
9229
+ const tinted = process.env.NO_COLOR ? art : `\x1B[38;5;39m${art}\x1B[0m`;
9230
+ return `
9231
+ ${tinted}
9232
+
9233
+ `;
9234
+ })();
9235
+ var SYNC_BEGIN2 = "\x1B[?2026h";
9236
+ var SYNC_END2 = "\x1B[?2026l";
9237
+ var HIDE_CURSOR = "\x1B[?25l";
9238
+ var SHOW_CURSOR = "\x1B[?25h";
9239
+ var SPIN = ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"];
9240
+ var sleep3 = (ms) => new Promise((r) => setTimeout(r, ms));
9241
+ function shineColor(dist) {
9242
+ const d = Math.abs(dist);
9243
+ if (d === 0) return 231;
9244
+ if (d === 1) return 159;
9245
+ if (d === 2) return 81;
9246
+ return 39;
9247
+ }
9248
+ function paintLine(line, center) {
9249
+ let out = "";
9250
+ let prev = -1;
9251
+ for (let i = 0; i < line.length; i++) {
9252
+ const ch = line[i];
9253
+ const c = shineColor(i - center);
9254
+ if (c !== prev) {
9255
+ out += `\x1B[38;5;${String(c)}m`;
9256
+ prev = c;
9257
+ }
9258
+ out += ch;
9259
+ }
9260
+ return out + "\x1B[0m";
9261
+ }
9262
+ async function animateBanner() {
9263
+ if (process.env.NO_COLOR || process.env.CI || process.env.AGENTBOX_NO_ANIM || !process.stdout.isTTY) {
9264
+ process.stdout.write(BANNER);
9265
+ return;
9266
+ }
9267
+ const restoreCursor = () => {
9268
+ process.stdout.write(SHOW_CURSOR);
9269
+ };
9270
+ process.once("exit", restoreCursor);
9271
+ const onSigint = () => {
9272
+ restoreCursor();
9273
+ process.exit(130);
9274
+ };
9275
+ process.once("SIGINT", onSigint);
9276
+ const frameMs = 45;
9277
+ const start = -3;
9278
+ const end = LOGO_WIDTH + 2;
9279
+ const down = 4;
9280
+ process.stdout.write(`
9281
+ ${HIDE_CURSOR}`);
9282
+ process.stdout.write("\n".repeat(down) + `\x1B[${String(down)}A`);
9283
+ const statusLine2 = (spin) => ` \x1B[38;5;51m${spin}\x1B[0m \x1B[38;5;245mChecking system\u2026\x1B[0m`;
9284
+ for (let center = start; center <= end; center++) {
9285
+ const spin = SPIN[Math.floor((center - start) / 2) % SPIN.length] ?? SPIN[0];
9286
+ 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
9287
+ statusLine2(spin) + "\x1B[3A\r" + // back up to the logo's first row
9288
+ SYNC_END2;
9289
+ process.stdout.write(frame);
9290
+ await sleep3(frameMs);
9291
+ }
9292
+ process.stdout.write(SYNC_BEGIN2 + `\x1B[38;5;39m${LOGO_L1}
9293
+ ${LOGO_L2}\x1B[0m` + SYNC_END2);
9294
+ await sleep3(250);
9295
+ process.stdout.write(SYNC_BEGIN2 + "\n\x1B[2K\n\x1B[2K" + SHOW_CURSOR + SYNC_END2);
9296
+ process.removeListener("exit", restoreCursor);
9297
+ process.removeListener("SIGINT", onSigint);
9298
+ }
9299
+ var LEGACY_INFO_MARKER = "Drive AgentBox from the host:";
9300
+ function installTargets() {
9301
+ const home = homedir13();
9302
+ const claudeSkills = join15(home, ".claude", "skills");
9303
+ return [
9304
+ { src: join15("agentbox", "SKILL.md"), dest: join15(claudeSkills, "agentbox", "SKILL.md") },
9305
+ {
9306
+ src: join15("agentbox-info", "SKILL.md"),
9307
+ dest: join15(claudeSkills, "agentbox-info", "SKILL.md")
9308
+ },
9309
+ {
9310
+ src: join15("codex", "agentbox.md"),
9311
+ dest: join15(home, ".codex", "prompts", "agentbox.md"),
9312
+ gateDir: join15(home, ".codex")
9313
+ },
9314
+ {
9315
+ src: join15("opencode", "agentbox.md"),
9316
+ dest: join15(home, ".config", "opencode", "commands", "agentbox.md"),
9317
+ gateDir: join15(home, ".config", "opencode")
9318
+ }
9319
+ ];
9320
+ }
9321
+ function resolveHostSkillsDir() {
9322
+ const here = dirname2(fileURLToPath(import.meta.url));
9323
+ const candidates = [
9324
+ resolve2(here, "..", "share", "host-skills"),
9325
+ resolve2(here, "..", "..", "share", "host-skills")
9326
+ ];
9327
+ for (const c of candidates) {
9328
+ if (existsSync6(c)) return c;
9329
+ }
9330
+ throw new Error(`could not locate bundled host skills; tried:
9331
+ ${candidates.join("\n ")}`);
9332
+ }
9333
+ function writableReason(target, force) {
9334
+ if (!existsSync6(target)) return "new";
9335
+ const existing = readFileSync(target, "utf8");
9336
+ if (existing.includes(MANAGED_SENTINEL) || existing.includes(LEGACY_INFO_MARKER)) {
9337
+ return "managed";
9338
+ }
9339
+ return force ? "forced" : "skip";
9340
+ }
9341
+ function installHostSkills(opts = {}) {
9342
+ const force = opts.force === true;
9343
+ const dryRun = opts.dryRun === true;
9344
+ const quiet = opts.quiet === true;
9345
+ const srcDir = resolveHostSkillsDir();
9346
+ const written = [];
9347
+ const blocked = [];
9348
+ let skipped = 0;
8234
9349
  for (const t of installTargets()) {
8235
- const src = join11(srcDir, t.src);
8236
- if (!existsSync5(src)) {
8237
- log29.warn(`bundled file missing (skipped): ${src}`);
9350
+ const src = join15(srcDir, t.src);
9351
+ if (!existsSync6(src)) {
9352
+ if (!quiet) log30.warn(`bundled file missing (skipped): ${src}`);
8238
9353
  skipped++;
8239
9354
  continue;
8240
9355
  }
8241
- if (t.gateDir && !existsSync5(t.gateDir)) continue;
9356
+ if (t.gateDir && !existsSync6(t.gateDir)) continue;
8242
9357
  const reason = writableReason(t.dest, force);
8243
9358
  if (reason === "skip") {
8244
- log29.warn(`user-modified file at ${t.dest}, skipping; pass --force to overwrite`);
9359
+ if (!quiet) log30.warn(`user-modified file at ${t.dest}, skipping; pass --force to overwrite`);
9360
+ blocked.push(t.dest);
8245
9361
  skipped++;
8246
9362
  continue;
8247
9363
  }
8248
9364
  if (dryRun) {
8249
- log29.info(`would write ${t.dest} (${reason})`);
9365
+ if (!quiet) log30.info(`would write ${t.dest} (${reason})`);
8250
9366
  written.push(t.dest);
8251
9367
  continue;
8252
9368
  }
8253
- mkdirSync3(dirname(t.dest), { recursive: true });
8254
- writeFileSync3(t.dest, readFileSync(src, "utf8"));
9369
+ mkdirSync5(dirname2(t.dest), { recursive: true });
9370
+ writeFileSync4(t.dest, readFileSync(src, "utf8"));
8255
9371
  written.push(t.dest);
8256
9372
  }
8257
- if (dryRun) {
8258
- outro6(`dry-run: ${String(written.length)} file(s) would be written, ${String(skipped)} skipped`);
8259
- return;
9373
+ return { written, skipped, blocked };
9374
+ }
9375
+ var PROVIDER_HINTS = {
9376
+ docker: "builds a ~1GB local image; no login needed",
9377
+ hetzner: "paste an API token from the Hetzner Console",
9378
+ daytona: "approve a browser sign-in link",
9379
+ vercel: "installs the Vercel sandbox CLI, then a browser sign-in"
9380
+ };
9381
+ var PROVIDER_LABEL = {
9382
+ docker: "Docker (local)",
9383
+ hetzner: "Hetzner (cloud VPS)",
9384
+ daytona: "Daytona (cloud sandbox)",
9385
+ vercel: "Vercel (cloud microVM)"
9386
+ };
9387
+ function ensureTty() {
9388
+ if (process.stdin.isTTY && process.stdout.isTTY) return true;
9389
+ process.stderr.write(
9390
+ "agentbox install: an interactive terminal is required. Run `agentbox <provider> login` and `agentbox prepare --provider <name>` instead.\n"
9391
+ );
9392
+ return false;
9393
+ }
9394
+ async function runProviderLogin(name) {
9395
+ if (name === "docker") return true;
9396
+ if (name === "daytona") {
9397
+ const mod2 = await import("./dist-TMHSUVTP.js");
9398
+ const status2 = await mod2.getDaytonaStatus();
9399
+ if (status2.configured) {
9400
+ log30.info("daytona: already configured");
9401
+ const rotate = await confirm14({ message: "Re-authenticate Daytona?", initialValue: false });
9402
+ if (isCancel15(rotate)) return false;
9403
+ if (rotate) await mod2.ensureDaytonaCredentials({ force: true });
9404
+ return true;
9405
+ }
9406
+ await mod2.ensureDaytonaCredentials();
9407
+ return true;
9408
+ }
9409
+ if (name === "hetzner") {
9410
+ const mod2 = await import("./dist-5FQGYRW5.js");
9411
+ const status2 = mod2.readHetznerCredStatus();
9412
+ if (status2.source !== "none") {
9413
+ log30.info("hetzner: already configured");
9414
+ const rotate = await confirm14({ message: "Re-authenticate Hetzner?", initialValue: false });
9415
+ if (isCancel15(rotate)) return false;
9416
+ if (rotate) await mod2.ensureHetznerCredentials({ force: true });
9417
+ return true;
9418
+ }
9419
+ await mod2.ensureHetznerCredentials();
9420
+ return true;
9421
+ }
9422
+ const mod = await import("./dist-PZW3GWWU.js");
9423
+ const status = mod.readVercelCredStatus();
9424
+ if (status.auth !== "none") {
9425
+ log30.info(`vercel: already configured (${status.auth})`);
9426
+ const rotate = await confirm14({ message: "Re-authenticate Vercel?", initialValue: false });
9427
+ if (isCancel15(rotate)) return false;
9428
+ if (rotate) await mod.ensureVercelCredentials({ force: true });
9429
+ return true;
9430
+ }
9431
+ await mod.ensureVercelCredentials();
9432
+ return true;
9433
+ }
9434
+ function tutorialBody(provider) {
9435
+ const startCmd = provider === "docker" ? "agentbox claude" : `agentbox ${provider} claude`;
9436
+ return `Get started:
9437
+ ${startCmd} # for claude, codex, opencode
9438
+ -> Setup wizard? -> Yes # install dependencies and setup agentbox.yaml
9439
+ -> Ctrl+a d # to detach from the box and leave claude running
9440
+ agentbox claude attach 1 # resume it later
9441
+ agentbox install # to set up another provider`;
9442
+ }
9443
+ var KNOWN_PROVIDERS = ["docker", "hetzner", "daytona", "vercel"];
9444
+ function isProviderName(s) {
9445
+ return KNOWN_PROVIDERS.includes(s);
9446
+ }
9447
+ async function runInstallWizard(opts = {}) {
9448
+ if (!ensureTty()) return false;
9449
+ await animateBanner();
9450
+ intro6("Check system compatibility");
9451
+ const sysResults = await runSystemChecks();
9452
+ const sysGroup = { title: "system", results: sysResults };
9453
+ process.stdout.write(" " + formatCompact([sysGroup]) + "\n");
9454
+ const hardFail = sysResults.find((r) => r.status === "fail");
9455
+ if (hardFail) {
9456
+ log30.error(`system check failed: ${hardFail.label} \u2014 ${hardFail.detail}`);
9457
+ log30.info("run `agentbox doctor` for full detail");
9458
+ const cont = await confirm14({ message: "Continue anyway?", initialValue: false });
9459
+ if (isCancel15(cont) || !cont) {
9460
+ outro6("aborted");
9461
+ return false;
9462
+ }
9463
+ }
9464
+ let providerName;
9465
+ if (opts.provider) {
9466
+ const candidate = opts.provider.trim();
9467
+ if (!isProviderName(candidate)) {
9468
+ log30.error(`unknown --provider: ${candidate}`);
9469
+ return false;
9470
+ }
9471
+ providerName = candidate;
9472
+ } else {
9473
+ const picked = await select2({
9474
+ message: "Which provider do you want to set up?",
9475
+ initialValue: "docker",
9476
+ options: KNOWN_PROVIDERS.map((p) => ({
9477
+ value: p,
9478
+ label: PROVIDER_LABEL[p],
9479
+ hint: PROVIDER_HINTS[p]
9480
+ }))
9481
+ });
9482
+ if (isCancel15(picked)) {
9483
+ outro6("cancelled");
9484
+ return false;
9485
+ }
9486
+ providerName = picked;
8260
9487
  }
8261
- if (written.length === 0) {
8262
- outro6(`nothing installed (${String(skipped)} skipped)`);
9488
+ if (providerName !== "docker") {
9489
+ const loggedIn = await runProviderLogin(providerName);
9490
+ if (!loggedIn) {
9491
+ outro6("cancelled");
9492
+ return false;
9493
+ }
9494
+ }
9495
+ 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)`;
9496
+ const wantPrepare = opts.yes ? true : await confirm14({ message: prepareMsg, initialValue: true });
9497
+ if (isCancel15(wantPrepare)) {
9498
+ outro6("cancelled");
9499
+ return false;
9500
+ }
9501
+ if (wantPrepare) {
9502
+ try {
9503
+ await runPrepare(providerName, {
9504
+ cwd: process.cwd(),
9505
+ yes: true,
9506
+ suppressStatus: true,
9507
+ suppressTip: true
9508
+ });
9509
+ } catch (err) {
9510
+ log30.warn(
9511
+ `prepare failed: ${err instanceof Error ? err.message : String(err)} \u2014 you can rerun \`agentbox prepare --provider ${providerName}\` later`
9512
+ );
9513
+ }
9514
+ } else {
9515
+ log30.info(
9516
+ `skipped \u2014 the ${providerName} base will build lazily on first \`agentbox ${providerName === "docker" ? "" : providerName + " "}create\``
9517
+ );
9518
+ }
9519
+ const sp = spinner8();
9520
+ sp.start("installing host /agentbox skill\u2026");
9521
+ let skillRes;
9522
+ try {
9523
+ skillRes = installHostSkills({ force: opts.force, dryRun: opts.dryRun, quiet: true });
9524
+ if (skillRes.written.length > 0) {
9525
+ sp.stop(`Agentbox Skills: Installed in ${String(skillRes.written.length)} locations`);
9526
+ } else {
9527
+ sp.stop(`Agentbox Skills: nothing to write (${String(skillRes.skipped)} skipped)`);
9528
+ }
9529
+ if (skillRes.blocked.length > 0) {
9530
+ log30.warn(
9531
+ `user-modified host skill file(s) left in place: ${skillRes.blocked.join(", ")}
9532
+ pass \`agentbox install --skills-only --force\` to overwrite`
9533
+ );
9534
+ }
9535
+ } catch (err) {
9536
+ sp.stop("Agentbox Skills: failed");
9537
+ log30.warn(err instanceof Error ? err.message : String(err));
9538
+ }
9539
+ markSetupComplete(providerName);
9540
+ const providerGroup = await runProviderChecks(providerName);
9541
+ process.stdout.write(" " + formatCompact([sysGroup, providerGroup]) + "\n");
9542
+ note(tutorialBody(providerName), "Next steps");
9543
+ outro6(
9544
+ opts.fromAutoTrigger ? "\u2728 Setup complete \u2014 continuing with your command\u2026" : "\u2728 Setup complete"
9545
+ );
9546
+ return true;
9547
+ }
9548
+ var installCommand = new Command25("install").description(
9549
+ "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."
9550
+ ).option(
9551
+ "--skills-only",
9552
+ "only install the host /agentbox skill files (no wizard, no login, no prepare)"
9553
+ ).option("--force", "overwrite existing skill files even if not AgentBox-managed").option("--dry-run", "print what would be written without changing anything").option(
9554
+ "-p, --provider <name>",
9555
+ "pre-select the provider to set up (docker | daytona | hetzner | vercel)"
9556
+ ).option("-y, --yes", "auto-confirm the prepare step").action(async (opts) => {
9557
+ if (opts.skillsOnly) {
9558
+ intro6("Installing AgentBox host commands...");
9559
+ let res;
9560
+ try {
9561
+ res = installHostSkills({ force: opts.force, dryRun: opts.dryRun });
9562
+ } catch (err) {
9563
+ log30.error(err instanceof Error ? err.message : String(err));
9564
+ process.exit(1);
9565
+ }
9566
+ if (opts.dryRun) {
9567
+ outro6(
9568
+ `dry-run: ${String(res.written.length)} file(s) would be written, ${String(res.skipped)} skipped`
9569
+ );
9570
+ return;
9571
+ }
9572
+ if (res.written.length === 0) {
9573
+ outro6(`nothing installed (${String(res.skipped)} skipped)`);
9574
+ return;
9575
+ }
9576
+ outro6(`installed: ${res.written.join(", ")}`);
8263
9577
  return;
8264
9578
  }
8265
- outro6(`installed: ${written.join(", ")}`);
9579
+ const ok = await runInstallWizard({
9580
+ provider: opts.provider,
9581
+ yes: opts.yes,
9582
+ force: opts.force,
9583
+ dryRun: opts.dryRun
9584
+ });
9585
+ if (!ok) process.exit(1);
9586
+ });
9587
+
9588
+ // src/commands/doctor.ts
9589
+ import { Command as Command26 } from "commander";
9590
+ var doctorCommand = new Command26("doctor").description(
9591
+ "Diagnose system compatibility and provider readiness (Node, git, ssh, Docker daemon, provider credentials, prepared snapshots)."
9592
+ ).option(
9593
+ "-p, --provider <name>",
9594
+ "limit checks to one provider (docker | daytona | hetzner | vercel)"
9595
+ ).action(async (opts) => {
9596
+ let groups;
9597
+ if (opts.provider) {
9598
+ const name = opts.provider.trim();
9599
+ if (!isKnownProvider(name)) {
9600
+ process.stderr.write(
9601
+ "error: --provider must be one of: docker, daytona, hetzner, vercel\n"
9602
+ );
9603
+ process.exit(1);
9604
+ }
9605
+ groups = [
9606
+ { title: "system", results: await runSystemChecks() },
9607
+ await runProviderChecks(name)
9608
+ ];
9609
+ } else {
9610
+ groups = await runAllChecks();
9611
+ }
9612
+ process.stdout.write(formatDetailed(groups).join("\n") + "\n");
9613
+ const worst = worstStatus(groups);
9614
+ if (worst === "fail") {
9615
+ process.stdout.write(
9616
+ "\nOne or more required checks failed. Fix the FAIL items above before continuing.\n"
9617
+ );
9618
+ process.exit(1);
9619
+ }
9620
+ if (worst === "warn") {
9621
+ process.stdout.write(
9622
+ "\nWarnings are providers that need setup. Run `agentbox install` to configure one,\nor `agentbox prepare --status` to see remote snapshot inventory.\n"
9623
+ );
9624
+ } else {
9625
+ process.stdout.write("\nAll checks passed.\n");
9626
+ }
8266
9627
  });
8267
9628
 
8268
9629
  // src/commands/git.ts
8269
- import { Command as Command25 } from "commander";
9630
+ import { Command as Command27 } from "commander";
8270
9631
  var WORKSPACE = "/workspace";
8271
9632
  var TOKEN_TTL_MS = 12e4;
8272
- async function runInBox(box, argv) {
9633
+ async function runInBox(box, argv2) {
8273
9634
  const provider = await providerForBox(box);
8274
- return provider.exec(box, argv, { cwd: WORKSPACE });
9635
+ return provider.exec(box, argv2, { cwd: WORKSPACE });
8275
9636
  }
8276
- async function runAndStream(box, argv) {
8277
- const r = await runInBox(box, argv);
9637
+ async function runAndStream(box, argv2) {
9638
+ const r = await runInBox(box, argv2);
8278
9639
  if (r.stdout) process.stdout.write(r.stdout);
8279
9640
  if (r.stderr) process.stderr.write(r.stderr);
8280
9641
  return r.exitCode;
@@ -8298,33 +9659,33 @@ function buildPredictedGhPrParams(ghArgs) {
8298
9659
  async function exitWith(code) {
8299
9660
  process.exit(code);
8300
9661
  }
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) => {
9662
+ 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
9663
  try {
8303
9664
  const box = await resolveBoxOrExit(boxRef);
8304
9665
  const predicted = buildPredictedGitParams(opts.remote, args);
8305
9666
  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));
9667
+ const argv2 = ["agentbox-ctl", "git", "push", ...tokenArgs];
9668
+ if (opts.remote) argv2.push("--remote", opts.remote);
9669
+ argv2.push(...args);
9670
+ await exitWith(await runAndStream(box, argv2));
8310
9671
  } catch (err) {
8311
9672
  handleLifecycleError(err);
8312
9673
  }
8313
9674
  });
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) => {
9675
+ 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
9676
  try {
8316
9677
  const box = await resolveBoxOrExit(boxRef);
8317
9678
  const predicted = buildPredictedGitParams(opts.remote, args);
8318
9679
  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));
9680
+ const argv2 = ["agentbox-ctl", "git", "fetch", ...tokenArgs];
9681
+ if (opts.remote) argv2.push("--remote", opts.remote);
9682
+ argv2.push(...args);
9683
+ await exitWith(await runAndStream(box, argv2));
8323
9684
  } catch (err) {
8324
9685
  handleLifecycleError(err);
8325
9686
  }
8326
9687
  });
8327
- var pullCommand = new Command25("pull").description(
9688
+ var pullCommand = new Command27("pull").description(
8328
9689
  "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
9690
  ).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
9691
  async (boxRef, branch, args, opts) => {
@@ -8336,17 +9697,17 @@ var pullCommand = new Command25("pull").description(
8336
9697
  }
8337
9698
  const predicted = buildPredictedGitParams(opts.remote, args);
8338
9699
  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));
9700
+ const argv2 = ["agentbox-ctl", "git", "pull", ...tokenArgs];
9701
+ if (opts.remote) argv2.push("--remote", opts.remote);
9702
+ if (opts.ffOnly) argv2.push("--ff-only");
9703
+ argv2.push(...args);
9704
+ await exitWith(await runAndStream(box, argv2));
8344
9705
  } catch (err) {
8345
9706
  handleLifecycleError(err);
8346
9707
  }
8347
9708
  }
8348
9709
  );
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) => {
9710
+ 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
9711
  try {
8351
9712
  const box = await resolveBoxOrExit(boxRef);
8352
9713
  await exitWith(await runAndStream(box, ["git", "checkout", branch, ...args]));
@@ -8354,7 +9715,7 @@ var checkoutCommand = new Command25("checkout").description("Change the box's wo
8354
9715
  handleLifecycleError(err);
8355
9716
  }
8356
9717
  });
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) => {
9718
+ 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
9719
  try {
8359
9720
  const box = await resolveBoxOrExit(boxRef);
8360
9721
  await exitWith(await runAndStream(box, ["git", "status", ...args]));
@@ -8378,7 +9739,7 @@ function injectPrCreateHead2(op, box, args) {
8378
9739
  return injectPrCreateHead(op, rootWt?.branch, args);
8379
9740
  }
8380
9741
  function buildPrSubcommand(op) {
8381
- return new Command25(op).description(PR_OP_DESCRIPTIONS[op]).argument("<box>", "box ref").argument(
9742
+ return new Command27(op).description(PR_OP_DESCRIPTIONS[op]).argument("<box>", "box ref").argument(
8382
9743
  "[args...]",
8383
9744
  "extra flags forwarded to `gh pr <op>` (e.g. --title, --body, --label, --draft, --json)"
8384
9745
  ).allowExcessArguments(true).allowUnknownOption(true).action(async (boxRef, args) => {
@@ -8387,25 +9748,25 @@ function buildPrSubcommand(op) {
8387
9748
  const ghArgs = injectPrCreateHead2(op, box, args);
8388
9749
  const predicted = buildPredictedGhPrParams(ghArgs);
8389
9750
  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));
9751
+ const argv2 = ["agentbox-ctl", "gh", "pr", op, ...tokenArgs, ...ghArgs];
9752
+ await exitWith(await runAndStream(box, argv2));
8392
9753
  } catch (err) {
8393
9754
  handleLifecycleError(err);
8394
9755
  }
8395
9756
  });
8396
9757
  }
8397
- var prCommand = new Command25("pr").description(
9758
+ var prCommand = new Command27("pr").description(
8398
9759
  "PR operations against a box's branch via the host `gh` CLI"
8399
9760
  );
8400
9761
  for (const op of GH_PR_OPS) {
8401
9762
  const sub = buildPrSubcommand(op);
8402
9763
  prCommand.addCommand(sub, op === "create" ? { isDefault: true } : void 0);
8403
9764
  }
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);
9765
+ 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
9766
 
8406
9767
  // src/commands/list.ts
8407
- import { log as log30 } from "@clack/prompts";
8408
- import { Command as Command26 } from "commander";
9768
+ import { log as log31 } from "@clack/prompts";
9769
+ import { Command as Command28 } from "commander";
8409
9770
  import { pathToFileURL } from "url";
8410
9771
 
8411
9772
  // src/hyperlink.ts
@@ -8418,6 +9779,38 @@ function hyperlink(label, url, stream) {
8418
9779
  return `${ESC2}]8;;${url}${ST}${label}${ESC2}]8;;${ST}`;
8419
9780
  }
8420
9781
 
9782
+ // src/lib/cloud-state.ts
9783
+ var PROBE_TIMEOUT_MS = 4e3;
9784
+ async function applyLiveCloudStates(boxes) {
9785
+ await Promise.all(
9786
+ boxes.map(async (b) => {
9787
+ if (!b.provider || b.provider === "docker") return;
9788
+ try {
9789
+ const provider = await providerForBox(b);
9790
+ const state = await withTimeout(provider.probeState(b), PROBE_TIMEOUT_MS);
9791
+ if (state !== null) b.state = state;
9792
+ } catch {
9793
+ }
9794
+ })
9795
+ );
9796
+ }
9797
+ function withTimeout(p, ms) {
9798
+ return new Promise((resolve3) => {
9799
+ const t = setTimeout(() => resolve3(null), ms);
9800
+ if (typeof t.unref === "function") t.unref();
9801
+ p.then(
9802
+ (v) => {
9803
+ clearTimeout(t);
9804
+ resolve3(v);
9805
+ },
9806
+ () => {
9807
+ clearTimeout(t);
9808
+ resolve3(null);
9809
+ }
9810
+ );
9811
+ });
9812
+ }
9813
+
8421
9814
  // src/watch.ts
8422
9815
  function withWatchOptions(cmd) {
8423
9816
  return cmd.option("-w, --watch", "redraw continuously until interrupted (Ctrl-C)").option("--interval <seconds>", "refresh interval for --watch", "2");
@@ -8433,7 +9826,7 @@ async function watchRender(produce, rawInterval) {
8433
9826
  process.stdout.write("\x1B[?25l");
8434
9827
  process.once("exit", () => process.stdout.write("\x1B[?25h"));
8435
9828
  process.once("SIGINT", () => process.exit(0));
8436
- const sleep4 = (d) => new Promise((r) => setTimeout(r, d));
9829
+ const sleep5 = (d) => new Promise((r) => setTimeout(r, d));
8437
9830
  for (; ; ) {
8438
9831
  let body;
8439
9832
  try {
@@ -8449,7 +9842,7 @@ async function watchRender(produce, rawInterval) {
8449
9842
  ${body.replace(/\n+$/, "")}
8450
9843
  `
8451
9844
  );
8452
- await sleep4(ms);
9845
+ await sleep5(ms);
8453
9846
  }
8454
9847
  }
8455
9848
 
@@ -8501,6 +9894,7 @@ function workspaceCell(path, target, stream) {
8501
9894
  return { text: hyperlink(display, url, stream), width: display.length };
8502
9895
  }
8503
9896
  function agentSummary(b) {
9897
+ if (b.state !== "running") return "-";
8504
9898
  const agents = [];
8505
9899
  if (b.claudeActivity && b.claudeActivity !== "unknown") {
8506
9900
  agents.push(`claude:${b.claudeActivity}`);
@@ -8554,14 +9948,19 @@ function renderTable(boxes, stream) {
8554
9948
  (row2) => row2.map((cell, i) => padCell(cell ?? plain(""), i)).join(" ").trimEnd()
8555
9949
  ).join("\n");
8556
9950
  }
8557
- async function scopedBoxes(all) {
9951
+ async function scopedBoxes(all, live) {
8558
9952
  const boxes = await listBoxes();
8559
- if (all) return { boxes, projectRoot: "", scoped: false };
9953
+ if (all) {
9954
+ if (live) await applyLiveCloudStates(boxes);
9955
+ return { boxes, projectRoot: "", scoped: false };
9956
+ }
8560
9957
  const { root } = await findProjectRoot(process.cwd());
8561
- return { boxes: boxes.filter((b) => b.projectRoot === root), projectRoot: root, scoped: true };
9958
+ const scoped2 = boxes.filter((b) => b.projectRoot === root);
9959
+ if (live) await applyLiveCloudStates(scoped2);
9960
+ return { boxes: scoped2, projectRoot: root, scoped: true };
8562
9961
  }
8563
- async function buildListText(all) {
8564
- const { boxes, projectRoot, scoped: scoped2 } = await scopedBoxes(all);
9962
+ async function buildListText(all, live) {
9963
+ const { boxes, projectRoot, scoped: scoped2 } = await scopedBoxes(all, live);
8565
9964
  if (boxes.length === 0) {
8566
9965
  if (scoped2) {
8567
9966
  return `no boxes in this project (${projectRoot}) \u2014 run \`agentbox create\`, or \`agentbox list --global\` to see all`;
@@ -8575,31 +9974,35 @@ async function buildListText(all) {
8575
9974
  ${table}`;
8576
9975
  }
8577
9976
  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")
9977
+ 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(
9978
+ "--live",
9979
+ "probe live cloud state via the provider SDK (slower; default: last host-known state)"
9980
+ )
8579
9981
  ).action(async (opts) => {
8580
9982
  if (opts.json && opts.watch) {
8581
- log30.error("cannot combine --json with --watch");
9983
+ log31.error("cannot combine --json with --watch");
8582
9984
  process.exit(2);
8583
9985
  }
8584
9986
  const all = opts.global ?? false;
9987
+ const live = opts.live ?? false;
8585
9988
  if (opts.watch) {
8586
- await watchRender(() => buildListText(all), opts.interval);
9989
+ await watchRender(() => buildListText(all, live), opts.interval);
8587
9990
  return;
8588
9991
  }
8589
9992
  if (opts.json) {
8590
- const { boxes } = await scopedBoxes(all);
9993
+ const { boxes } = await scopedBoxes(all, live);
8591
9994
  process.stdout.write(JSON.stringify(boxes, null, 2) + "\n");
8592
9995
  return;
8593
9996
  }
8594
- process.stdout.write(await buildListText(all) + "\n");
9997
+ process.stdout.write(await buildListText(all, live) + "\n");
8595
9998
  });
8596
9999
 
8597
10000
  // src/commands/logs.ts
8598
- import { log as log31 } from "@clack/prompts";
8599
- import { Command as Command27 } from "commander";
10001
+ import { log as log32 } from "@clack/prompts";
10002
+ import { Command as Command29 } from "commander";
8600
10003
  import { spawn as spawn3 } from "child_process";
8601
10004
  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(
10005
+ var logsCommand = new Command29("logs").description("Print recent log lines from a box service; -f to stream").argument(
8603
10006
  "[box]",
8604
10007
  "box ref (optional when cwd has exactly 1 box): project index, id, id prefix, name, or container"
8605
10008
  ).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 +10020,9 @@ var logsCommand = new Command27("logs").description("Print recent log lines from
8617
10020
  service = boxArg;
8618
10021
  }
8619
10022
  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]");
10023
+ log32.error("missing <service> argument");
10024
+ log32.info("usage: agentbox logs [box] <service> [-n N] [-f]");
10025
+ log32.info(" agentbox logs [box] --daemon [-n N] [-f]");
8623
10026
  process.exit(2);
8624
10027
  }
8625
10028
  const box = await resolveBoxOrExit(idOrName);
@@ -8630,7 +10033,7 @@ var logsCommand = new Command27("logs").description("Print recent log lines from
8630
10033
  if (!opts.follow) {
8631
10034
  const proc = await provider.exec(box, args, { user: "vscode" });
8632
10035
  if (proc.exitCode !== 0) {
8633
- log31.error(
10036
+ log32.error(
8634
10037
  `${opts.daemon ? "daemon log" : "agentbox-ctl logs"} failed: ${proc.stderr || proc.stdout}`
8635
10038
  );
8636
10039
  process.exit(1);
@@ -8686,12 +10089,12 @@ var logsCommand = new Command27("logs").description("Print recent log lines from
8686
10089
  });
8687
10090
 
8688
10091
  // 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";
10092
+ import { log as log33 } from "@clack/prompts";
10093
+ import { execa as execa3 } from "execa";
10094
+ import { existsSync as existsSync7, mkdirSync as mkdirSync6 } from "fs";
10095
+ import { homedir as homedir14 } from "os";
10096
+ import { join as join16 } from "path";
10097
+ import { Command as Command30 } from "commander";
8695
10098
 
8696
10099
  // src/commands/path.ts
8697
10100
  async function runPath(box, opts) {
@@ -8713,7 +10116,7 @@ async function runPath(box, opts) {
8713
10116
  }
8714
10117
 
8715
10118
  // 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(
10119
+ var openCommand = new Command30("open").description("Open a box's /workspace in Finder (docker: rsync'd snapshot; cloud: sshfs mount)").argument(
8717
10120
  "[box]",
8718
10121
  "box ref: project index, id, id prefix, name, or container (default: the only box in this project)"
8719
10122
  ).option("--no-refresh", "skip the rsync; open whatever's already on disk (docker only)").option(
@@ -8752,7 +10155,7 @@ var openCommand = new Command28("open").description("Open a box's /workspace in
8752
10155
  }
8753
10156
  });
8754
10157
  async function runCloudOpen(box, provider, opts) {
8755
- const mountRoot = join12(homedir10(), ".agentbox", "mounts", box.name);
10158
+ const mountRoot = join16(homedir14(), ".agentbox", "mounts", box.name);
8756
10159
  if (opts.unmount) {
8757
10160
  const ok = await tryUnmount(mountRoot);
8758
10161
  if (ok) process.stdout.write(`unmounted ${mountRoot}
@@ -8789,14 +10192,14 @@ async function runCloudOpen(box, provider, opts) {
8789
10192
  user: target.user,
8790
10193
  identityFile: target.identityFile
8791
10194
  });
8792
- if (!existsSync6(mountRoot)) {
8793
- mkdirSync4(mountRoot, { recursive: true, mode: 493 });
10195
+ if (!existsSync7(mountRoot)) {
10196
+ mkdirSync6(mountRoot, { recursive: true, mode: 493 });
8794
10197
  } else if (await isMounted(mountRoot)) {
8795
- log32.info(`re-mounting (stale mount detected at ${mountRoot})`);
10198
+ log33.info(`re-mounting (stale mount detected at ${mountRoot})`);
8796
10199
  await tryUnmount(mountRoot);
8797
10200
  }
8798
- log32.info(`mounting ${alias}:/workspace at ${mountRoot}`);
8799
- const mount = await execa2(
10201
+ log33.info(`mounting ${alias}:/workspace at ${mountRoot}`);
10202
+ const mount = await execa3(
8800
10203
  sshfsBin,
8801
10204
  [
8802
10205
  `${alias}:/workspace`,
@@ -8815,35 +10218,35 @@ async function runCloudOpen(box, provider, opts) {
8815
10218
  if (mount.exitCode !== 0) {
8816
10219
  throw new Error(`sshfs mount failed (exit ${String(mount.exitCode)}): ${mount.stderr || mount.stdout}`);
8817
10220
  }
8818
- await execa2("open", [mountRoot], { reject: false });
10221
+ await execa3("open", [mountRoot], { reject: false });
8819
10222
  process.stdout.write(`opened ${mountRoot}
8820
10223
  `);
8821
10224
  process.stdout.write(`unmount later with: agentbox open ${box.name} --unmount
8822
10225
  `);
8823
10226
  }
8824
10227
  async function locateBinary(name) {
8825
- const r = await execa2("which", [name], { reject: false });
10228
+ const r = await execa3("which", [name], { reject: false });
8826
10229
  if (r.exitCode !== 0) return null;
8827
10230
  const path = (r.stdout ?? "").trim();
8828
10231
  return path.length > 0 ? path : null;
8829
10232
  }
8830
10233
  async function isMounted(path) {
8831
- const r = await execa2("sh", ["-c", `mount | grep -F " on ${path} "`], { reject: false });
10234
+ const r = await execa3("sh", ["-c", `mount | grep -F " on ${path} "`], { reject: false });
8832
10235
  return r.exitCode === 0;
8833
10236
  }
8834
10237
  async function tryUnmount(path) {
8835
10238
  if (await isMounted(path)) {
8836
- const u = await execa2("umount", [path], { reject: false });
10239
+ const u = await execa3("umount", [path], { reject: false });
8837
10240
  if (u.exitCode === 0) return true;
8838
- const d = await execa2("diskutil", ["unmount", path], { reject: false });
10241
+ const d = await execa3("diskutil", ["unmount", path], { reject: false });
8839
10242
  return d.exitCode === 0;
8840
10243
  }
8841
10244
  return false;
8842
10245
  }
8843
10246
 
8844
10247
  // src/commands/pause.ts
8845
- import { Command as Command29 } from "commander";
8846
- var pauseCommand = new Command29("pause").description(
10248
+ import { Command as Command31 } from "commander";
10249
+ var pauseCommand = new Command31("pause").description(
8847
10250
  "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
10251
  ).argument(
8849
10252
  "[box]",
@@ -8865,220 +10268,9 @@ var pauseCommand = new Command29("pause").description(
8865
10268
  }
8866
10269
  });
8867
10270
 
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
10271
  // 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";
10272
+ import { confirm as confirm15, isCancel as isCancel16, log as log34 } from "@clack/prompts";
10273
+ import { Command as Command32 } from "commander";
9082
10274
  function totalRemovals(r, projectConfigs) {
9083
10275
  return r.removedRecords.length + r.removedContainers.length + r.removedVolumes.length + r.removedSnapshotDirs.length + r.removedBoxDirs.length + projectConfigs.length;
9084
10276
  }
@@ -9124,7 +10316,7 @@ async function liveProjectRoots() {
9124
10316
  return [];
9125
10317
  }
9126
10318
  }
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(
10319
+ 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
10320
  "--all",
9129
10321
  "also remove orphan agentbox-* containers, volumes, snapshot dirs, and orphan per-project config dirs"
9130
10322
  ).option("-y, --yes", "skip the confirmation prompt").option(
@@ -9154,8 +10346,8 @@ var pruneCommand = new Command31("prune").description("Clean up orphan state.jso
9154
10346
  ${summary(preview, previewProjects)}`);
9155
10347
  if (dryRun) return;
9156
10348
  if (!opts.yes) {
9157
- const ok = await confirm14({ message: "Proceed with prune?", initialValue: true });
9158
- if (isCancel15(ok) || !ok) {
10349
+ const ok = await confirm15({ message: "Proceed with prune?", initialValue: true });
10350
+ if (isCancel16(ok) || !ok) {
9159
10351
  log34.info("cancelled");
9160
10352
  return;
9161
10353
  }
@@ -9173,19 +10365,9 @@ var CLOUD_PRUNE_PROVIDERS = ["daytona", "hetzner", "vercel"];
9173
10365
  function isCloudPruneProvider(name) {
9174
10366
  return CLOUD_PRUNE_PROVIDERS.includes(name);
9175
10367
  }
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
10368
  async function pruneCloud(provider, opts) {
9187
10369
  const dryRun = opts.dryRun ?? false;
9188
- const backend = await cloudBackendFor(provider);
10370
+ const backend = await cloudBackendForProvider(provider);
9189
10371
  if (!backend.list) {
9190
10372
  log34.error(`${provider} backend doesn't expose \`list()\`; cannot enumerate sandboxes for prune`);
9191
10373
  process.exit(2);
@@ -9218,11 +10400,11 @@ async function pruneCloud(provider, opts) {
9218
10400
  }
9219
10401
  if (dryRun) return;
9220
10402
  if (!opts.yes) {
9221
- const ok = await confirm14({
10403
+ const ok = await confirm15({
9222
10404
  message: `Delete ${String(orphans.length)} orphan sandbox(es)?`,
9223
10405
  initialValue: false
9224
10406
  });
9225
- if (isCancel15(ok) || !ok) {
10407
+ if (isCancel16(ok) || !ok) {
9226
10408
  log34.info("cancelled");
9227
10409
  return;
9228
10410
  }
@@ -9249,10 +10431,10 @@ async function pruneCloud(provider, opts) {
9249
10431
  // src/commands/queue.ts
9250
10432
  import { readFile as readFile4, stat as stat5 } from "fs/promises";
9251
10433
  import { intro as intro7, log as log35, outro as outro7 } from "@clack/prompts";
9252
- import { Command as Command32 } from "commander";
10434
+ import { Command as Command33 } from "commander";
9253
10435
  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) => {
10436
+ var queueCommand = new Command33("queue").description("Inspect and manage background `agentbox claude|codex|opencode -i` jobs");
10437
+ 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
10438
  const jobs = await loadQueue();
9257
10439
  const cfg = await loadQueueConfig();
9258
10440
  const visible = opts.all === true ? jobs : jobs.filter((j) => !TERMINAL_STATUSES.has(j.status));
@@ -9275,17 +10457,17 @@ var queueListCommand = new Command32("list").description("List queued, running,
9275
10457
  const widths = headers.map(
9276
10458
  (h) => Math.max(h.length, ...rows.map((r) => String(r[h]).length))
9277
10459
  );
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");
10460
+ const pad4 = (s, w) => s + " ".repeat(Math.max(0, w - s.length));
10461
+ process.stdout.write(headers.map((h, i) => pad4(h, widths[i])).join(" ") + "\n");
9280
10462
  process.stdout.write(widths.map((w) => "-".repeat(w)).join(" ") + "\n");
9281
10463
  for (const r of rows) {
9282
10464
  process.stdout.write(
9283
- headers.map((h, i) => pad3(String(r[h]), widths[i])).join(" ") + "\n"
10465
+ headers.map((h, i) => pad4(String(r[h]), widths[i])).join(" ") + "\n"
9284
10466
  );
9285
10467
  }
9286
10468
  log35.info(`queue.maxConcurrent = ${String(cfg.maxConcurrent)} (queue.enabled=${String(cfg.enabled)})`);
9287
10469
  });
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) => {
10470
+ 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
10471
  const job = await readJob(id);
9290
10472
  if (!job) {
9291
10473
  log35.error(`no job with id ${id}`);
@@ -9307,7 +10489,7 @@ var queueShowCommand = new Command32("show").description("Dump a job manifest an
9307
10489
  log35.info(`(no log at ${job.logPath} yet)`);
9308
10490
  }
9309
10491
  });
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) => {
10492
+ 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
10493
  intro7(`Cancelling queue job ${id}...`);
9312
10494
  const job = await readJob(id);
9313
10495
  if (!job) {
@@ -9329,7 +10511,7 @@ var queueCancelCommand = new Command32("cancel").description("Cancel a queued jo
9329
10511
  await writeJob(cancelled);
9330
10512
  outro7(`job ${id} cancelled`);
9331
10513
  });
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) => {
10514
+ 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
10515
  const targets = /* @__PURE__ */ new Set();
9334
10516
  if (opts.all === true || opts.done === true) targets.add("done");
9335
10517
  if (opts.all === true || opts.failed === true) targets.add("failed");
@@ -9358,7 +10540,7 @@ var QUEUE_WAIT_EVENTS = [
9358
10540
  var ACTIVE_JOB_STATUSES = /* @__PURE__ */ new Set(["queued", "running"]);
9359
10541
  var DEFAULT_QUEUE_WAIT_TIMEOUT_MS = 10 * 60 * 1e3;
9360
10542
  var QUEUE_POLL_INTERVAL_MS = 500;
9361
- var queueWaitForCommand = new Command32("wait-for").description(
10543
+ var queueWaitForCommand = new Command33("wait-for").description(
9362
10544
  `Block until a queue / box event fires. <event> one of: ${QUEUE_WAIT_EVENTS.join(" | ")}.`
9363
10545
  ).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
10546
  if (!QUEUE_WAIT_EVENTS.includes(eventRaw)) {
@@ -9446,11 +10628,11 @@ async function pollUntil(deadline, probe) {
9446
10628
  if (result !== void 0) return result;
9447
10629
  const remaining = deadline - Date.now();
9448
10630
  if (remaining <= 0) break;
9449
- await sleep3(Math.min(QUEUE_POLL_INTERVAL_MS, remaining));
10631
+ await sleep4(Math.min(QUEUE_POLL_INTERVAL_MS, remaining));
9450
10632
  }
9451
10633
  throw new QueueWaitTimeout();
9452
10634
  }
9453
- function sleep3(ms) {
10635
+ function sleep4(ms) {
9454
10636
  return new Promise((r) => setTimeout(r, ms));
9455
10637
  }
9456
10638
  function parsePositiveInt3(raw, label) {
@@ -9483,8 +10665,8 @@ function truncate(s, max) {
9483
10665
  }
9484
10666
 
9485
10667
  // src/commands/relay.ts
9486
- import { log as log36, spinner as spinner8 } from "@clack/prompts";
9487
- import { Command as Command33 } from "commander";
10668
+ import { log as log36, spinner as spinner9 } from "@clack/prompts";
10669
+ import { Command as Command34 } from "commander";
9488
10670
  async function rehydrateFromState() {
9489
10671
  const state = await readState();
9490
10672
  await rehydrateRelayRegistry(
@@ -9508,12 +10690,14 @@ function renderStatus(s) {
9508
10690
  if (s.running && s.health) {
9509
10691
  return [
9510
10692
  "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}`
10693
+ ` pid: ${s.pid === null ? "?" : String(s.pid)}`,
10694
+ ` port: ${String(s.port)}`,
10695
+ ` url: ${s.endpoint.hostUrl}`,
10696
+ ` version: ${s.health.version ?? "(unknown \u2014 relay predates version field)"}`,
10697
+ ` commit: ${s.health.commit ?? "(unknown)"}`,
10698
+ ` boxes: ${String(s.health.boxes)}`,
10699
+ ` events: ${String(s.health.events)}`,
10700
+ ` log: ${s.logFile}`
9517
10701
  ].join("\n");
9518
10702
  }
9519
10703
  if (s.pidAlive) {
@@ -9524,7 +10708,7 @@ function renderStatus(s) {
9524
10708
  }
9525
10709
  return ["relay: not running", ` log: ${s.logFile}`].join("\n");
9526
10710
  }
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) => {
10711
+ 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
10712
  try {
9529
10713
  const s = await getRelayStatus();
9530
10714
  if (opts.json) {
@@ -9536,9 +10720,9 @@ var statusSub = new Command33("status").description("Show whether the host relay
9536
10720
  handleLifecycleError(err);
9537
10721
  }
9538
10722
  });
9539
- var stopSub = new Command33("stop").description("Stop the host relay process (idempotent)").action(async () => {
10723
+ var stopSub = new Command34("stop").description("Stop the host relay process (idempotent)").action(async () => {
9540
10724
  try {
9541
- const s = spinner8();
10725
+ const s = spinner9();
9542
10726
  s.start("stopping relay");
9543
10727
  const result = await stopRelay();
9544
10728
  s.stop(
@@ -9548,9 +10732,9 @@ var stopSub = new Command33("stop").description("Stop the host relay process (id
9548
10732
  handleLifecycleError(err);
9549
10733
  }
9550
10734
  });
9551
- var startSub = new Command33("start").description("Start the host relay if not already running (idempotent)").action(async () => {
10735
+ var startSub = new Command34("start").description("Start the host relay if not already running (idempotent)").action(async () => {
9552
10736
  try {
9553
- const s = spinner8();
10737
+ const s = spinner9();
9554
10738
  s.start("starting relay");
9555
10739
  const ep = await ensureRelay();
9556
10740
  await rehydrateFromState();
@@ -9559,15 +10743,15 @@ var startSub = new Command33("start").description("Start the host relay if not a
9559
10743
  handleLifecycleError(err);
9560
10744
  }
9561
10745
  });
9562
- var restartSub = new Command33("restart").description("Stop then start the host relay").action(async () => {
10746
+ var restartSub = new Command34("restart").description("Stop then start the host relay").action(async () => {
9563
10747
  try {
9564
- const s = spinner8();
10748
+ const s = spinner9();
9565
10749
  s.start("stopping relay");
9566
10750
  const stopped = await stopRelay();
9567
10751
  s.stop(
9568
10752
  stopped.stopped ? `stopped relay (pid ${String(stopped.pid)})` : "relay was not running"
9569
10753
  );
9570
- const s2 = spinner8();
10754
+ const s2 = spinner9();
9571
10755
  s2.start("starting relay");
9572
10756
  try {
9573
10757
  const ep = await ensureRelay();
@@ -9582,11 +10766,11 @@ var restartSub = new Command33("restart").description("Stop then start the host
9582
10766
  handleLifecycleError(err);
9583
10767
  }
9584
10768
  });
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);
10769
+ 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
10770
 
9587
10771
  // 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) => {
10772
+ import { Command as Command35 } from "commander";
10773
+ 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
10774
  const log45 = openCommandLog(`queue-${id}`);
9591
10775
  log45.write(`worker pid=${String(process.pid)} starting for job ${id}`);
9592
10776
  let job = null;
@@ -9682,7 +10866,7 @@ async function runDockerJob(job, log45, onBoxCreated) {
9682
10866
  log45.write(`starting claude session`);
9683
10867
  await startClaudeSession({
9684
10868
  container: result.record.container,
9685
- claudeArgs: promptedArgs,
10869
+ claudeArgs: applyClaudeSkipPermissions(promptedArgs, cfg.effective),
9686
10870
  sessionName: cfg.effective.claude.sessionName,
9687
10871
  boxName: result.record.name
9688
10872
  });
@@ -9694,7 +10878,7 @@ async function runDockerJob(job, log45, onBoxCreated) {
9694
10878
  log45.write(`starting codex session`);
9695
10879
  await startCodexSession({
9696
10880
  container: result.record.container,
9697
- codexArgs: promptedArgs,
10881
+ codexArgs: applyCodexSkipPermissions(promptedArgs, cfg.effective),
9698
10882
  sessionName: cfg.effective.codex.sessionName
9699
10883
  });
9700
10884
  } else if (job.agent === "opencode") {
@@ -9729,13 +10913,20 @@ function buildOverridesFromJob(job) {
9729
10913
  else if (job.agent === "codex") out.codex = { sessionName: opts.sessionName };
9730
10914
  else if (job.agent === "opencode") out.opencode = { sessionName: opts.sessionName };
9731
10915
  }
10916
+ if (opts.dangerouslySkipPermissions !== void 0) {
10917
+ if (job.agent === "claude-code") {
10918
+ out.claude = { ...out.claude, dangerouslySkipPermissions: opts.dangerouslySkipPermissions };
10919
+ } else if (job.agent === "codex") {
10920
+ out.codex = { ...out.codex, dangerouslySkipPermissions: opts.dangerouslySkipPermissions };
10921
+ }
10922
+ }
9732
10923
  return out;
9733
10924
  }
9734
10925
 
9735
10926
  // src/commands/screen.ts
9736
10927
  import { spawnSync as spawnSync3 } from "child_process";
9737
10928
  import { log as log37 } from "@clack/prompts";
9738
- import { Command as Command35 } from "commander";
10929
+ import { Command as Command36 } from "commander";
9739
10930
  var SIGNED_URL_TTL_MIN = 1;
9740
10931
  var SIGNED_URL_TTL_MAX = 86400;
9741
10932
  function parseTtlOrExit(raw) {
@@ -9748,7 +10939,7 @@ function parseTtlOrExit(raw) {
9748
10939
  }
9749
10940
  return n;
9750
10941
  }
9751
- var screenCommand = new Command35("screen").description("Open a box's VNC (noVNC) viewer in the browser (auto-unpause/start)").argument(
10942
+ var screenCommand = new Command36("screen").description("Open a box's VNC (noVNC) viewer in the browser (auto-unpause/start)").argument(
9752
10943
  "[box]",
9753
10944
  "box ref: project index, id, id prefix, name, or container (default: the only box in this project)"
9754
10945
  ).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 +11004,28 @@ var screenCommand = new Command35("screen").description("Open a box's VNC (noVNC
9813
11004
  } else if (state === "missing") {
9814
11005
  throw new Error(`cloud sandbox for ${box.name} is missing; was it deleted?`);
9815
11006
  }
11007
+ const persisted = await readBoxStatus(box);
11008
+ const hasWebService = persisted?.services.some((s) => s.expose) ?? false;
11009
+ if (hasWebService) {
11010
+ try {
11011
+ const webUrl = await p.resolveUrl(box, { kind: "web" });
11012
+ const q = `'${webUrl.replace(/'/g, "'\\''")}'`;
11013
+ const br = await p.exec(box, ["bash", "-lc", `agent-browser open --headed ${q}`], {
11014
+ user: "vscode"
11015
+ });
11016
+ if (br.exitCode === 0) {
11017
+ log37.info(`opened ${webUrl} in the in-box browser (visible in the VNC view)`);
11018
+ } else {
11019
+ log37.warn(
11020
+ `could not open in-box browser (continuing): ${br.stderr.trim() || br.stdout.trim() || `exit ${String(br.exitCode)}`}`
11021
+ );
11022
+ }
11023
+ } catch (err) {
11024
+ log37.warn(
11025
+ `in-box browser skipped: ${err instanceof Error ? err.message : String(err)}`
11026
+ );
11027
+ }
11028
+ }
9816
11029
  const base = await p.resolveUrl(box, { kind: "vnc", ttl });
9817
11030
  url = `${base.replace(/\/$/, "")}/vnc.html?autoconnect=1&password=${encodeURIComponent(box.vncPassword)}`;
9818
11031
  }
@@ -9835,7 +11048,7 @@ var screenCommand = new Command35("screen").description("Open a box's VNC (noVNC
9835
11048
  // src/commands/shell.ts
9836
11049
  import { spawnSync as spawnSync4 } from "child_process";
9837
11050
  import { log as log39 } from "@clack/prompts";
9838
- import { Command as Command36 } from "commander";
11051
+ import { Command as Command37 } from "commander";
9839
11052
 
9840
11053
  // src/commands/_provider-guard.ts
9841
11054
  import { log as log38 } from "@clack/prompts";
@@ -9950,7 +11163,7 @@ async function startOrAttachShell(box, cfg) {
9950
11163
  });
9951
11164
  process.exit(code);
9952
11165
  }
9953
- var shellCommand = new Command36("shell").description(
11166
+ var shellCommand = new Command37("shell").description(
9954
11167
  "Open an interactive shell in a box, in a detachable tmux session (auto-unpause/start)"
9955
11168
  ).argument(
9956
11169
  "[box]",
@@ -10041,7 +11254,7 @@ var shellCommand = new Command36("shell").description(
10041
11254
  handleLifecycleError(err);
10042
11255
  }
10043
11256
  });
10044
- var shellAttachCommand = new Command36("attach").description(
11257
+ var shellAttachCommand = new Command37("attach").description(
10045
11258
  "Attach to a shell tmux session in a box, starting one if none is running (auto-unpause/start)"
10046
11259
  ).argument(
10047
11260
  "[box]",
@@ -10079,7 +11292,7 @@ function renderShellTable(sessions) {
10079
11292
  for (const r of rows) process.stdout.write(`${fmt(r)}
10080
11293
  `);
10081
11294
  }
10082
- var shellLsCommand = new Command36("ls").description("List the shell tmux sessions running in a box").argument(
11295
+ var shellLsCommand = new Command37("ls").description("List the shell tmux sessions running in a box").argument(
10083
11296
  "[box]",
10084
11297
  "box ref: project index, id, id prefix, name, or container (default: the only box in this project)"
10085
11298
  ).action(async (idOrName) => {
@@ -10102,7 +11315,7 @@ var shellLsCommand = new Command36("ls").description("List the shell tmux sessio
10102
11315
  handleLifecycleError(err);
10103
11316
  }
10104
11317
  });
10105
- var shellKillCommand = new Command36("kill").description("Kill a shell tmux session in a box (the shell and anything running in it)").argument(
11318
+ var shellKillCommand = new Command37("kill").description("Kill a shell tmux session in a box (the shell and anything running in it)").argument(
10106
11319
  "[box]",
10107
11320
  "box ref: project index, id, id prefix, name, or container (default: the only box in this project)"
10108
11321
  ).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 +11353,8 @@ shellCommand.addCommand(shellLsCommand);
10140
11353
  shellCommand.addCommand(shellKillCommand);
10141
11354
 
10142
11355
  // src/commands/start.ts
10143
- import { Command as Command37 } from "commander";
10144
- var startCommand = new Command37("start").description(
11356
+ import { Command as Command38 } from "commander";
11357
+ var startCommand = new Command38("start").description(
10145
11358
  "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
11359
  ).argument(
10147
11360
  "[box]",
@@ -10165,7 +11378,7 @@ var startCommand = new Command37("start").description(
10165
11378
 
10166
11379
  // src/commands/status.ts
10167
11380
  import { log as log41 } from "@clack/prompts";
10168
- import { Command as Command38 } from "commander";
11381
+ import { Command as Command39 } from "commander";
10169
11382
 
10170
11383
  // src/endpoints-render.ts
10171
11384
  function renderEndpointLines(endpoints, stream) {
@@ -10377,7 +11590,7 @@ async function runInspect(box, opts) {
10377
11590
 
10378
11591
  // src/commands/status.ts
10379
11592
  var statusCommand2 = withWatchOptions(
10380
- new Command38("status").description("Show service + task status from a box's agentbox-ctl daemon").argument(
11593
+ new Command39("status").description("Show service + task status from a box's agentbox-ctl daemon").argument(
10381
11594
  "[box]",
10382
11595
  "box ref: project index, id, id prefix, name, or container (default: the only box in this project)"
10383
11596
  ).option("-j, --json", "machine-readable JSON output").option("--inspect", "show detailed box info (volumes, limits, paths) instead of service/task status")
@@ -10551,8 +11764,8 @@ function renderPersisted2(s, state) {
10551
11764
  }
10552
11765
 
10553
11766
  // src/commands/stop.ts
10554
- import { Command as Command39 } from "commander";
10555
- var stopCommand = new Command39("stop").description(
11767
+ import { Command as Command40 } from "commander";
11768
+ var stopCommand = new Command40("stop").description(
10556
11769
  "Stop a box (Docker: docker stop; preserves upper + node_modules volumes. Cloud: backend.stop \u2014 sandbox stays in your account, disk preserved)."
10557
11770
  ).argument(
10558
11771
  "[box]",
@@ -10581,7 +11794,7 @@ restart with: agentbox start ${box.name}
10581
11794
  });
10582
11795
 
10583
11796
  // src/commands/top.ts
10584
- import { Command as Command40 } from "commander";
11797
+ import { Command as Command41 } from "commander";
10585
11798
  var COLS = ["BOX", "STATE", "CPU%", "MEM USAGE / LIMIT", "MEM%", "PIDS", "DISK", "NET I/O"];
10586
11799
  function row(name, state, s) {
10587
11800
  const mem = `${fmtBytes(s.memUsedBytes)} / ${fmtBytes(s.memLimitBytes)}`;
@@ -10614,6 +11827,7 @@ async function selectBoxes(idOrName, opts) {
10614
11827
  }
10615
11828
  async function snapshot(idOrName, opts) {
10616
11829
  const boxes = await selectBoxes(idOrName, opts);
11830
+ if (opts.live) await applyLiveCloudStates(boxes);
10617
11831
  const stats = await Promise.all(
10618
11832
  boxes.map((b) => {
10619
11833
  if ((b.provider ?? "docker") !== "docker") return emptyStats(b.provider ?? "cloud");
@@ -10654,10 +11868,10 @@ async function renderProjectFooters() {
10654
11868
 
10655
11869
  SYSTEM: ${parts.join(" - ")}` : "";
10656
11870
  }
10657
- var topCommand = new Command40("top").description("Live resource monitor (cpu/mem/pids/disk) for a box, the project, or every box").argument(
11871
+ var topCommand = new Command41("top").description("Live resource monitor (cpu/mem/pids/disk) for a box, the project, or every box").argument(
10658
11872
  "[box]",
10659
11873
  "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) => {
11874
+ ).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
11875
  try {
10662
11876
  if (opts.json) {
10663
11877
  const { boxes, stats } = await snapshot(idOrName, opts);
@@ -10687,8 +11901,8 @@ var topCommand = new Command40("top").description("Live resource monitor (cpu/me
10687
11901
  });
10688
11902
 
10689
11903
  // src/commands/unpause.ts
10690
- import { Command as Command41 } from "commander";
10691
- var unpauseCommand = new Command41("unpause").description(
11904
+ import { Command as Command42 } from "commander";
11905
+ var unpauseCommand = new Command42("unpause").description(
10692
11906
  "Resume a paused box. Docker: `docker unpause` (sub-second). Cloud: backend.resume (re-hydrates from archive \u2014 slower first time)."
10693
11907
  ).argument(
10694
11908
  "[box]",
@@ -10712,8 +11926,8 @@ var unpauseCommand = new Command41("unpause").description(
10712
11926
 
10713
11927
  // src/commands/update.ts
10714
11928
  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";
11929
+ import { confirm as confirm16, intro as intro8, isCancel as isCancel17, log as log42, outro as outro8, spinner as spinner10 } from "@clack/prompts";
11930
+ import { Command as Command43 } from "commander";
10717
11931
 
10718
11932
  // src/exec-method.ts
10719
11933
  function detectExecutionMethod(input) {
@@ -10757,7 +11971,7 @@ function runInherit(cmd, args) {
10757
11971
  child.on("close", (code) => resolveP(code ?? 0));
10758
11972
  });
10759
11973
  }
10760
- var updateCommand = new Command42("self-update").description(
11974
+ var updateCommand = new Command43("self-update").description(
10761
11975
  "Update agentbox: self-update via npm/pnpm (unless run via npx), wipe the box image so it rebuilds, and reload the relay"
10762
11976
  ).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
11977
  try {
@@ -10780,8 +11994,8 @@ var updateCommand = new Command42("self-update").description(
10780
11994
  return;
10781
11995
  }
10782
11996
  if (!opts.yes) {
10783
- const ok = await confirm15({ message: "Proceed with update?", initialValue: true });
10784
- if (isCancel16(ok) || !ok) {
11997
+ const ok = await confirm16({ message: "Proceed with update?", initialValue: true });
11998
+ if (isCancel17(ok) || !ok) {
10785
11999
  log42.info("cancelled");
10786
12000
  return;
10787
12001
  }
@@ -10803,13 +12017,13 @@ var updateCommand = new Command42("self-update").description(
10803
12017
  log42.success(`updated ${PKG} via ${cmd.cmd}`);
10804
12018
  }
10805
12019
  }
10806
- const s = spinner9();
12020
+ const s = spinner10();
10807
12021
  s.start(`removing image ${DEFAULT_BOX_IMAGE}`);
10808
12022
  const removed = await removeImage(DEFAULT_BOX_IMAGE);
10809
12023
  s.stop(
10810
12024
  removed ? `removed image ${DEFAULT_BOX_IMAGE} (rebuilds on next create/claude)` : `image ${DEFAULT_BOX_IMAGE} not present (nothing to remove)`
10811
12025
  );
10812
- const sr = spinner9();
12026
+ const sr = spinner10();
10813
12027
  sr.start("stopping relay");
10814
12028
  const stop = await stopRelay();
10815
12029
  sr.stop(
@@ -10820,7 +12034,7 @@ var updateCommand = new Command42("self-update").description(
10820
12034
  "relay will restart automatically (with the updated build) on your next `agentbox create` / `agentbox claude`"
10821
12035
  );
10822
12036
  } else {
10823
- const sr2 = spinner9();
12037
+ const sr2 = spinner10();
10824
12038
  sr2.start("restarting relay");
10825
12039
  try {
10826
12040
  const ep = await ensureRelay();
@@ -10841,7 +12055,7 @@ var updateCommand = new Command42("self-update").description(
10841
12055
  // src/commands/url.ts
10842
12056
  import { spawnSync as spawnSync5 } from "child_process";
10843
12057
  import { log as log43 } from "@clack/prompts";
10844
- import { Command as Command43 } from "commander";
12058
+ import { Command as Command44 } from "commander";
10845
12059
  var SIGNED_URL_TTL_MIN2 = 1;
10846
12060
  var SIGNED_URL_TTL_MAX2 = 86400;
10847
12061
  function parseTtlOrExit2(raw) {
@@ -10854,7 +12068,7 @@ function parseTtlOrExit2(raw) {
10854
12068
  }
10855
12069
  return n;
10856
12070
  }
10857
- var urlCommand = new Command43("url").description(
12071
+ var urlCommand = new Command44("url").description(
10858
12072
  "Open a box's web app URL in the browser, even when no service declares `expose:` (auto-unpause/start)"
10859
12073
  ).argument(
10860
12074
  "[box]",
@@ -10933,8 +12147,8 @@ var urlCommand = new Command43("url").description(
10933
12147
 
10934
12148
  // src/commands/wait.ts
10935
12149
  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(
12150
+ import { Command as Command45 } from "commander";
12151
+ var waitCommand = new Command45("wait").description("Block until the box reports all autostart units ready").argument(
10938
12152
  "[box]",
10939
12153
  "box ref: project index, id, id prefix, name, or container (default: the only box in this project)"
10940
12154
  ).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 +12188,14 @@ var SUGARED_COMMANDS = ["create", "claude", "codex", "opencode"];
10974
12188
  function isSugared(name) {
10975
12189
  return SUGARED_COMMANDS.includes(name);
10976
12190
  }
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);
12191
+ function rewriteProviderPrefix(argv2) {
12192
+ if (argv2.length < 4) return [...argv2];
12193
+ const provider = argv2[2];
12194
+ const subcmd = argv2[3];
12195
+ if (typeof provider !== "string" || typeof subcmd !== "string") return [...argv2];
12196
+ if (!isKnownProvider(provider) || !isSugared(subcmd)) return [...argv2];
12197
+ const head = argv2.slice(0, 2);
12198
+ const rest = argv2.slice(4);
10985
12199
  return [...head, subcmd, "--provider", provider, ...rest];
10986
12200
  }
10987
12201
 
@@ -10989,7 +12203,7 @@ function rewriteProviderPrefix(argv) {
10989
12203
  process.env.DOCKER_CLI_HINTS ??= "false";
10990
12204
  process.env.AGENTBOX_CLI_VERSION = AGENTBOX_VERSION;
10991
12205
  process.env.AGENTBOX_CLI_COMMIT = AGENTBOX_COMMIT;
10992
- var program = new Command45();
12206
+ var program = new Command46();
10993
12207
  program.name("agentbox").description("Launch coding agents in isolated sandboxes").version(AGENTBOX_VERSION);
10994
12208
  program.enablePositionalOptions();
10995
12209
  program.addCommand(createCommand);
@@ -11031,10 +12245,43 @@ program.addCommand(vercelCommand);
11031
12245
  program.addCommand(dockerCommand);
11032
12246
  program.addCommand(updateCommand);
11033
12247
  program.addCommand(installCommand);
12248
+ program.addCommand(doctorCommand);
11034
12249
  program.configureHelp({ visibleCommands: () => [] });
11035
12250
  program.addHelpText("after", () => "\n" + buildGroupedHelp(program));
11036
12251
  await applyEngineOverrideAtStartup();
11037
- program.parseAsync(rewriteProviderPrefix(process.argv)).catch((err) => {
12252
+ var argv = rewriteProviderPrefix(process.argv);
12253
+ var FIRST_RUN_EXEMPT = /* @__PURE__ */ new Set([
12254
+ "install",
12255
+ "doctor",
12256
+ "help",
12257
+ "relay",
12258
+ "_run-queued-job",
12259
+ "drive",
12260
+ "screen"
12261
+ ]);
12262
+ function isFirstRunHookEligible(args) {
12263
+ if (!process.stdin.isTTY || !process.stdout.isTTY) return false;
12264
+ const rest = args.slice(2);
12265
+ if (rest.length === 0) return false;
12266
+ for (const a of rest) {
12267
+ if (a === "--help" || a === "-h" || a === "--version" || a === "-V") return false;
12268
+ }
12269
+ const first = rest[0];
12270
+ if (typeof first !== "string" || first.startsWith("-")) return false;
12271
+ if (FIRST_RUN_EXEMPT.has(first)) return false;
12272
+ return true;
12273
+ }
12274
+ if (isFirstRun() && isFirstRunHookEligible(argv)) {
12275
+ try {
12276
+ await runInstallWizard({ fromAutoTrigger: true });
12277
+ } catch (err) {
12278
+ process.stderr.write(
12279
+ `install wizard failed: ${err instanceof Error ? err.message : String(err)}
12280
+ `
12281
+ );
12282
+ }
12283
+ }
12284
+ program.parseAsync(argv).catch((err) => {
11038
12285
  console.error(err);
11039
12286
  process.exit(1);
11040
12287
  });