@madarco/agentbox 0.5.0 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (32) hide show
  1. package/dist/{chunk-7J5AJLWG.js → chunk-BBZMA2K6.js} +3 -3
  2. package/dist/{chunk-RFC5F5HR.js → chunk-HHMWQNLF.js} +8 -8
  3. package/dist/chunk-HHMWQNLF.js.map +1 -0
  4. package/dist/{chunk-PXUBE5KS.js → chunk-HTTKML3C.js} +351 -42
  5. package/dist/chunk-HTTKML3C.js.map +1 -0
  6. package/dist/{chunk-6VTAPD4H.js → chunk-KJNZP6I3.js} +100 -21
  7. package/dist/chunk-KJNZP6I3.js.map +1 -0
  8. package/dist/{chunk-FJNIFTWK.js → chunk-M7I247BK.js} +6 -4
  9. package/dist/chunk-M7I247BK.js.map +1 -0
  10. package/dist/{create-AHZ3GVEZ-TGEDL7UX.js → create-6PWXI6HO-OWAMHBAK.js} +4 -4
  11. package/dist/index.js +310 -102
  12. package/dist/index.js.map +1 -1
  13. package/dist/{lifecycle-LFOL6YFM-TCHDX3J5.js → lifecycle-EMXR46DI-DUVBXNTV.js} +4 -4
  14. package/dist/{stats-Z4BVJODD-HEC4TMUZ.js → stats-SZXOJE3D-N7OODCHW.js} +3 -3
  15. package/package.json +4 -4
  16. package/runtime/docker/Dockerfile.box +23 -11
  17. package/runtime/docker/apps/cli/share/agentbox-setup/SKILL.md +19 -11
  18. package/runtime/docker/packages/ctl/dist/bin.cjs +56 -15
  19. package/runtime/docker/packages/sandbox-docker/scripts/agentbox-checkpoint-cleanup +13 -3
  20. package/runtime/docker/packages/sandbox-docker/scripts/agentbox-dockerd-start +87 -7
  21. package/runtime/docker/packages/sandbox-docker/scripts/agentbox-open +28 -0
  22. package/runtime/docker/packages/sandbox-docker/scripts/custom-system-CLAUDE.md +4 -9
  23. package/runtime/relay/bin.cjs +121 -2
  24. package/share/agentbox-setup/SKILL.md +19 -11
  25. package/dist/chunk-6VTAPD4H.js.map +0 -1
  26. package/dist/chunk-FJNIFTWK.js.map +0 -1
  27. package/dist/chunk-PXUBE5KS.js.map +0 -1
  28. package/dist/chunk-RFC5F5HR.js.map +0 -1
  29. /package/dist/{chunk-7J5AJLWG.js.map → chunk-BBZMA2K6.js.map} +0 -0
  30. /package/dist/{create-AHZ3GVEZ-TGEDL7UX.js.map → create-6PWXI6HO-OWAMHBAK.js.map} +0 -0
  31. /package/dist/{lifecycle-LFOL6YFM-TCHDX3J5.js.map → lifecycle-EMXR46DI-DUVBXNTV.js.map} +0 -0
  32. /package/dist/{stats-Z4BVJODD-HEC4TMUZ.js.map → stats-SZXOJE3D-N7OODCHW.js.map} +0 -0
package/dist/index.js CHANGED
@@ -1,7 +1,9 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
- createBox
4
- } from "./chunk-6VTAPD4H.js";
3
+ createBox,
4
+ hostBackupHasCredentials,
5
+ syncClaudeCredentials
6
+ } from "./chunk-KJNZP6I3.js";
5
7
  import {
6
8
  AmbiguousBoxError,
7
9
  BoxNotFoundError,
@@ -15,16 +17,18 @@ import {
15
17
  startBox,
16
18
  stopBox,
17
19
  unpauseBox
18
- } from "./chunk-FJNIFTWK.js";
20
+ } from "./chunk-M7I247BK.js";
19
21
  import {
20
22
  ClaudeSessionError,
21
23
  DEFAULT_RELAY_PORT,
22
24
  SHARED_CLAUDE_VOLUME,
23
25
  buildClaudeAttachArgv,
24
26
  buildClaudeDashboardAttachArgv,
27
+ buildClaudeLoginRunArgv,
25
28
  buildShellArgv,
26
29
  buildVncUrls,
27
30
  claudeSessionInfo,
31
+ clearRelayNotice,
28
32
  containerHex,
29
33
  ensureAgentboxTasksFile,
30
34
  ensureClaudeVolume,
@@ -38,10 +42,13 @@ import {
38
42
  renderStatusTable,
39
43
  renderTaskTable,
40
44
  resolveClaudeVolume,
45
+ runInteractiveClaudeLogin,
41
46
  seedSetupSkillIntoVolume,
47
+ setRelayNotice,
42
48
  startClaudeSession,
43
- stopRelay
44
- } from "./chunk-PXUBE5KS.js";
49
+ stopRelay,
50
+ warmUpClaudeCredentials
51
+ } from "./chunk-HTTKML3C.js";
45
52
  import {
46
53
  STATE_DIR,
47
54
  readState,
@@ -52,7 +59,7 @@ import {
52
59
  allCheckpointImagesBytes,
53
60
  boxResourceStats,
54
61
  projectCheckpointImageBytes
55
- } from "./chunk-7J5AJLWG.js";
62
+ } from "./chunk-BBZMA2K6.js";
56
63
  import {
57
64
  DEFAULT_BOX_IMAGE,
58
65
  DEFAULT_ENV_PATTERNS,
@@ -62,6 +69,7 @@ import {
62
69
  configPathFor,
63
70
  createCheckpoint,
64
71
  detectEngine,
72
+ ensureImage,
65
73
  execInBox,
66
74
  findProjectRoot,
67
75
  listCheckpoints,
@@ -78,7 +86,7 @@ import {
78
86
  setConfigValue,
79
87
  setEngineOverride,
80
88
  unsetConfigValue
81
- } from "./chunk-RFC5F5HR.js";
89
+ } from "./chunk-HHMWQNLF.js";
82
90
 
83
91
  // src/index.ts
84
92
  import { Command as Command29 } from "commander";
@@ -302,13 +310,12 @@ var browserCommand = new Command("browser").description(
302
310
  });
303
311
 
304
312
  // src/commands/claude.ts
305
- import { confirm as confirm2, intro, isCancel as isCancel2, log as log5, outro, password, spinner } from "@clack/prompts";
313
+ import { confirm as confirm2, intro, isCancel as isCancel2, log as log5, outro, spinner } from "@clack/prompts";
306
314
  import { Command as Command2 } from "commander";
307
315
 
308
316
  // src/auth.ts
309
- import { spawnSync as spawnSync2 } from "child_process";
310
- import { mkdir, readFile, writeFile } from "fs/promises";
311
- import { dirname, join } from "path";
317
+ import { readFile } from "fs/promises";
318
+ import { join } from "path";
312
319
  var AUTH_FILE = join(STATE_DIR, "auth.json");
313
320
  async function resolveClaudeAuth(processEnv, opts = {}) {
314
321
  const env = {};
@@ -340,22 +347,6 @@ async function readAuthFile(path2 = AUTH_FILE) {
340
347
  return {};
341
348
  }
342
349
  }
343
- async function writeAuthFile(next, path2 = AUTH_FILE) {
344
- await mkdir(dirname(path2), { recursive: true });
345
- await writeFile(path2, JSON.stringify(next, null, 2) + "\n", { mode: 384, flag: "w" });
346
- }
347
- function hostClaudeAvailable() {
348
- const r = spawnSync2("which", ["claude"], { stdio: ["ignore", "pipe", "ignore"] });
349
- return r.status === 0 && (r.stdout?.toString().trim().length ?? 0) > 0;
350
- }
351
- function runHostSetupToken() {
352
- const child = spawnSync2("claude", ["setup-token"], { stdio: "inherit" });
353
- return { exitCode: child.status ?? -1 };
354
- }
355
- function isPlausibleOauthToken(s) {
356
- const t = s.trim();
357
- return t.startsWith("sk-ant-oat") && t.length >= 40;
358
- }
359
350
 
360
351
  // ../../packages/core/dist/index.js
361
352
  var claudeCodeLauncher = {
@@ -515,7 +506,7 @@ function passthroughFlags(opts) {
515
506
  }
516
507
 
517
508
  // src/wrapped-pty/run.ts
518
- import { spawnSync as spawnSync3 } from "child_process";
509
+ import { spawnSync as spawnSync2 } from "child_process";
519
510
 
520
511
  // src/pty/pty-backend.ts
521
512
  async function loadPtyBackend() {
@@ -644,6 +635,7 @@ function ellipsizeHead(s, max) {
644
635
  }
645
636
  function activityCell(b) {
646
637
  if (b.pendingPrompt) return "\u25B2 prompt";
638
+ if (b.checkpointing) return "\u25C6 checkpoint";
647
639
  if (b.state !== "running") return `[${b.state}]`;
648
640
  switch (b.claudeActivity) {
649
641
  case "working":
@@ -842,10 +834,13 @@ function statusLine(box, w, stateLabel, groups = HINT_GROUPS) {
842
834
  }
843
835
 
844
836
  // src/wrapped-pty/footer.ts
837
+ var SPINNER_FRAMES = ["\u25D0", "\u25D3", "\u25D1", "\u25D2"];
845
838
  var URGENT = "\x1B[38;5;220m\x1B[1m";
846
839
  var TXT = "\x1B[38;5;250m";
847
840
  var SUBTLE = "\x1B[38;5;245m";
848
841
  var RESET = "\x1B[0m";
842
+ var NOTICE_BG = "\x1B[48;5;220m";
843
+ var NOTICE_FG = "\x1B[38;5;16m\x1B[1m";
849
844
  var CLAUDE_IDLE_HINTS = [
850
845
  ["Control+a q", "detach"]
851
846
  ];
@@ -874,6 +869,13 @@ function renderFooter(state, cols) {
874
869
  const stateLabel = state.mode === "shell" ? "shell" : void 0;
875
870
  return statusLine(sidebarBox, cols, stateLabel, hints);
876
871
  }
872
+ if (state.kind === "notice") {
873
+ const spinner5 = SPINNER_FRAMES[state.frame % SPINNER_FRAMES.length];
874
+ const prefix = ` ${spinner5} `;
875
+ const inner2 = Math.max(0, cols - prefix.length);
876
+ const message2 = padTo(state.message, inner2);
877
+ return `${NOTICE_BG}${NOTICE_FG}${prefix}${message2}${RESET}`;
878
+ }
877
879
  const def = state.prompt.defaultAnswer ?? "n";
878
880
  const yn = def === "y" ? "[Y/n]" : "[y/N]";
879
881
  const tag2 = " [!] ";
@@ -962,6 +964,18 @@ function subscribePrompts(opts) {
962
964
  if (payload && typeof payload.id === "string") opts.onResolved(payload.id);
963
965
  } catch {
964
966
  }
967
+ } else if (event === "notice-set" && dataLine.length > 0) {
968
+ try {
969
+ const ev = JSON.parse(dataLine);
970
+ if (ev && typeof ev.id === "string") opts.onNotice?.(ev);
971
+ } catch {
972
+ }
973
+ } else if (event === "notice-clear" && dataLine.length > 0) {
974
+ try {
975
+ const payload = JSON.parse(dataLine);
976
+ if (payload && typeof payload.id === "string") opts.onNoticeCleared?.(payload.id);
977
+ } catch {
978
+ }
965
979
  }
966
980
  }
967
981
  }
@@ -1060,6 +1074,7 @@ function postAnswer(opts) {
1060
1074
  // src/wrapped-pty/run.ts
1061
1075
  var FOOTER_ROWS = 1;
1062
1076
  var STATUS_POLL_INTERVAL_MS = 3e3;
1077
+ var SPINNER_INTERVAL_MS = 120;
1063
1078
  async function runWrappedAttach(opts) {
1064
1079
  if (!process.stdout.isTTY || !process.stdin.isTTY) {
1065
1080
  return runFallback(opts.dockerArgv);
@@ -1090,6 +1105,10 @@ async function runWrappedAttach(opts) {
1090
1105
  let footerState = buildIdle();
1091
1106
  let lastSessionTitle;
1092
1107
  let lastActivity;
1108
+ let capturingPrompt = null;
1109
+ let activeNotice = null;
1110
+ let noticeFrame = 0;
1111
+ let spinnerTimer = null;
1093
1112
  const redrawFooter = () => {
1094
1113
  const cs = process.stdout.columns ?? cols;
1095
1114
  const rs = process.stdout.rows ?? rows;
@@ -1097,6 +1116,32 @@ async function runWrappedAttach(opts) {
1097
1116
  const payload = SYNC_BEGIN + CURSOR_SAVE + cursorMoveTo(rs, 1) + line + CURSOR_RESTORE + SYNC_END;
1098
1117
  process.stdout.write(payload);
1099
1118
  };
1119
+ const recomputeFooter = () => {
1120
+ if (capturingPrompt) {
1121
+ footerState = { kind: "prompt", prompt: capturingPrompt };
1122
+ } else if (activeNotice) {
1123
+ footerState = { kind: "notice", message: activeNotice.message, frame: noticeFrame };
1124
+ } else {
1125
+ footerState = buildIdle(lastSessionTitle, lastActivity);
1126
+ }
1127
+ };
1128
+ const startSpinner = () => {
1129
+ if (spinnerTimer) return;
1130
+ spinnerTimer = setInterval(() => {
1131
+ noticeFrame++;
1132
+ if (footerState.kind === "notice") {
1133
+ recomputeFooter();
1134
+ redrawFooter();
1135
+ }
1136
+ }, SPINNER_INTERVAL_MS);
1137
+ if (typeof spinnerTimer.unref === "function") spinnerTimer.unref();
1138
+ };
1139
+ const stopSpinner = () => {
1140
+ if (spinnerTimer) {
1141
+ clearInterval(spinnerTimer);
1142
+ spinnerTimer = null;
1143
+ }
1144
+ };
1100
1145
  pty.onData((d) => {
1101
1146
  process.stdout.write(d);
1102
1147
  redrawFooter();
@@ -1107,7 +1152,8 @@ async function runWrappedAttach(opts) {
1107
1152
  },
1108
1153
  onAnswer: (body) => {
1109
1154
  void postAnswer({ relayBaseUrl: opts.relayBaseUrl, body });
1110
- footerState = buildIdle(lastSessionTitle, lastActivity);
1155
+ capturingPrompt = null;
1156
+ recomputeFooter();
1111
1157
  redrawFooter();
1112
1158
  }
1113
1159
  });
@@ -1130,15 +1176,31 @@ async function runWrappedAttach(opts) {
1130
1176
  relayBaseUrl: opts.relayBaseUrl,
1131
1177
  boxId: opts.boxId,
1132
1178
  onPrompt: (ev) => {
1133
- footerState = { kind: "prompt", prompt: ev };
1179
+ capturingPrompt = ev;
1180
+ recomputeFooter();
1134
1181
  redrawFooter();
1135
1182
  router.capture(ev).catch(() => {
1136
1183
  });
1137
1184
  },
1138
1185
  onResolved: (id) => {
1139
- if (footerState.kind === "prompt" && footerState.prompt.id === id) {
1186
+ if (capturingPrompt && capturingPrompt.id === id) {
1187
+ capturingPrompt = null;
1140
1188
  router.abort("resolved-elsewhere");
1141
- footerState = buildIdle(lastSessionTitle, lastActivity);
1189
+ recomputeFooter();
1190
+ redrawFooter();
1191
+ }
1192
+ },
1193
+ onNotice: (ev) => {
1194
+ activeNotice = ev;
1195
+ startSpinner();
1196
+ recomputeFooter();
1197
+ redrawFooter();
1198
+ },
1199
+ onNoticeCleared: (id) => {
1200
+ if (activeNotice && activeNotice.id === id) {
1201
+ activeNotice = null;
1202
+ stopSpinner();
1203
+ recomputeFooter();
1142
1204
  redrawFooter();
1143
1205
  }
1144
1206
  }
@@ -1156,7 +1218,7 @@ async function runWrappedAttach(opts) {
1156
1218
  lastSessionTitle = nextTitle;
1157
1219
  lastActivity = nextActivity;
1158
1220
  if (footerState.kind === "idle") {
1159
- footerState = buildIdle(lastSessionTitle, lastActivity);
1221
+ recomputeFooter();
1160
1222
  redrawFooter();
1161
1223
  }
1162
1224
  } catch {
@@ -1178,6 +1240,7 @@ async function runWrappedAttach(opts) {
1178
1240
  process.stdin.off("data", onStdinData);
1179
1241
  process.stdout.off("resize", onResize);
1180
1242
  clearInterval(statusTimer);
1243
+ stopSpinner();
1181
1244
  if (process.stdin.isTTY) process.stdin.setRawMode(false);
1182
1245
  process.stdin.pause();
1183
1246
  stream.close();
@@ -1193,7 +1256,7 @@ async function runWrappedAttach(opts) {
1193
1256
  return exitCode;
1194
1257
  }
1195
1258
  function runFallback(argv) {
1196
- const child = spawnSync3("docker", argv, { stdio: "inherit" });
1259
+ const child = spawnSync2("docker", argv, { stdio: "inherit" });
1197
1260
  return child.status ?? 0;
1198
1261
  }
1199
1262
 
@@ -1201,6 +1264,12 @@ function runFallback(argv) {
1201
1264
  function reattachRef(r) {
1202
1265
  return typeof r.projectIndex === "number" ? String(r.projectIndex) : r.name;
1203
1266
  }
1267
+ function logPrune(rebuild) {
1268
+ if (rebuild.prunedBytes <= 0) return;
1269
+ const mb = Math.round(rebuild.prunedBytes / 1024 / 1024);
1270
+ const n = rebuild.pruned.length;
1271
+ log5.info(`pruned ${String(n)} stale plugin cache${n === 1 ? "" : "s"} (${String(mb)} MB freed)`);
1272
+ }
1204
1273
  var RELAY_HOST_URL = `http://127.0.0.1:${String(DEFAULT_RELAY_PORT)}`;
1205
1274
  async function attachClaudeWrapped(box, sessionName, reattach) {
1206
1275
  const code = await runWrappedAttach({
@@ -1231,39 +1300,46 @@ function buildClaudeCliOverrides(opts) {
1231
1300
  if (Object.keys(claude).length > 0) out.claude = claude;
1232
1301
  return out;
1233
1302
  }
1234
- async function offerSetupToken() {
1235
- log5.info("first time setup: setup token for Claude Code");
1236
- const canRun = hostClaudeAvailable();
1237
- if (canRun) {
1238
- const yes = await confirm2({
1239
- message: "Run `claude setup-token` now to save a token?",
1240
- initialValue: true
1303
+ async function runClaudeLoginContainer(image, extraArgs) {
1304
+ const { exitCode } = runInteractiveClaudeLogin(
1305
+ buildClaudeLoginRunArgv({ volume: SHARED_CLAUDE_VOLUME, image, extraArgs })
1306
+ );
1307
+ if (exitCode === 0) {
1308
+ const s = spinner();
1309
+ s.start("checking credentials");
1310
+ const warm = await warmUpClaudeCredentials(SHARED_CLAUDE_VOLUME, image, {
1311
+ onProgress: (line) => s.message(clampSpinnerLine(line))
1241
1312
  });
1242
- if (isCancel2(yes) || !yes) {
1243
- log5.info("ok, continuing without a saved token; /login inside the box once and it persists in the shared volume.");
1244
- return null;
1245
- }
1246
- const { exitCode } = runHostSetupToken();
1247
- if (exitCode !== 0) {
1248
- log5.warn(`\`claude setup-token\` exited with code ${String(exitCode)}; you can still paste a token below if you have one.`);
1249
- }
1250
- } else {
1251
- log5.warn(
1252
- "Claude Code is not installed on the host, so I cannot run `claude setup-token` for you. Run it on a machine that has Claude Code installed, then paste the token below \u2014 or skip and /login inside the box."
1253
- );
1313
+ s.stop(warm.warmed ? "credentials ready" : "credentials check incomplete \u2014 continuing");
1314
+ await syncClaudeCredentials({ volume: SHARED_CLAUDE_VOLUME }, { image, isolate: false });
1254
1315
  }
1255
- const pasted = await password({ message: "Paste OAuth token (or empty to skip):" });
1256
- if (isCancel2(pasted) || !pasted) {
1257
- log5.info("ok, continuing without a saved token; /login inside the box once and it persists in the shared volume.");
1258
- return null;
1316
+ return exitCode;
1317
+ }
1318
+ async function maybeRunClaudeLogin(args) {
1319
+ if (!process.stdin.isTTY || args.yes) return;
1320
+ if (args.authSource === "host-env") return;
1321
+ if (await hostBackupHasCredentials()) return;
1322
+ const message = args.authSource === "auth-file" ? "You're on a legacy API token (shows as 'Claude API'). Sign in with your Claude subscription instead?" : "Sign in with your Claude subscription? (saved and reused by every box)";
1323
+ const answer = await confirm2({ message, initialValue: true });
1324
+ if (isCancel2(answer) || !answer) {
1325
+ log5.info("Skipped sign-in \u2014 claude will prompt you to /login inside the box.");
1326
+ return;
1259
1327
  }
1260
- const token = pasted.trim();
1261
- if (!isPlausibleOauthToken(token)) {
1262
- log5.warn("That doesn't look like an OAuth token (expected `sk-ant-oat\u2026`); saving anyway \u2014 verify inside the box.");
1328
+ const s = spinner();
1329
+ s.start("preparing sandbox image");
1330
+ await ensureImage(args.image, { onProgress: (line) => s.message(clampSpinnerLine(line)) });
1331
+ s.message("preparing claude config");
1332
+ await ensureClaudeVolume(
1333
+ { volume: SHARED_CLAUDE_VOLUME },
1334
+ { syncFromHost: true, image: args.image, hostWorkspace: args.hostWorkspace }
1335
+ );
1336
+ s.stop("image ready");
1337
+ const exitCode = await runClaudeLoginContainer(args.image, ["--claudeai"]);
1338
+ if (exitCode !== 0) {
1339
+ log5.warn("Claude login did not complete; continuing \u2014 run `agentbox claude login` to retry.");
1340
+ return;
1263
1341
  }
1264
- await writeAuthFile({ claudeCodeOauthToken: token });
1265
- log5.success(`saved to ${AUTH_FILE} (mode 0600)`);
1266
- return { env: { CLAUDE_CODE_OAUTH_TOKEN: token }, source: "auth-file" };
1342
+ log5.success("Signed in with your Claude subscription \u2014 saved for future boxes.");
1267
1343
  }
1268
1344
  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(
1269
1345
  "--snapshot <ref>",
@@ -1287,6 +1363,13 @@ var claudeCommand = new Command2("claude").description("Create a sandboxed box a
1287
1363
  });
1288
1364
  const projectRoot = (await findProjectRoot(opts.workspace)).root;
1289
1365
  const checkpointRef = opts.snapshot && opts.snapshot.length > 0 ? opts.snapshot : cfg.effective.box.defaultCheckpoint.length > 0 ? cfg.effective.box.defaultCheckpoint : void 0;
1366
+ const resolved = await resolveClaudeAuth(process.env);
1367
+ await maybeRunClaudeLogin({
1368
+ image: cfg.effective.box.image,
1369
+ authSource: resolved.source,
1370
+ yes: !!opts.yes,
1371
+ hostWorkspace: opts.workspace
1372
+ });
1290
1373
  const wiz = await maybeRunSetupWizard({
1291
1374
  workspace: opts.workspace,
1292
1375
  yes: !!opts.yes,
@@ -1303,11 +1386,6 @@ var claudeCommand = new Command2("claude").description("Create a sandboxed box a
1303
1386
  }
1304
1387
  const useSnapshot = opts.hostSnapshot === false ? false : opts.hostSnapshot === true ? true : cfg.effective.box.hostSnapshot ?? false;
1305
1388
  const sessionName = cfg.effective.claude.sessionName;
1306
- let resolved = await resolveClaudeAuth(process.env);
1307
- if (resolved.source === "none" && process.stdin.isTTY && !opts.yes) {
1308
- const next = await offerSetupToken();
1309
- if (next) resolved = next;
1310
- }
1311
1389
  const s = spinner();
1312
1390
  s.start("creating box");
1313
1391
  let containerName = "";
@@ -1345,6 +1423,7 @@ var claudeCommand = new Command2("claude").description("Create a sandboxed box a
1345
1423
  });
1346
1424
  const nSuffix = typeof result.record.projectIndex === "number" ? ` \xB7 n ${String(result.record.projectIndex)}` : "";
1347
1425
  s.stop(`box ${result.record.container} ready${nSuffix}`);
1426
+ logPrune(rebuild);
1348
1427
  for (const f of rebuild.failed) {
1349
1428
  log5.warn(`plugin install failed for ${f.dir}; claude may still load it. stderr:
1350
1429
  ${f.stderr.trim()}`);
@@ -1369,6 +1448,7 @@ async function startOrAttachClaude(box, claudeArgs, opts) {
1369
1448
  cliOverrides: opts.sessionName ? { claude: { sessionName: opts.sessionName } } : {}
1370
1449
  });
1371
1450
  const sessionName = cfg.effective.claude.sessionName;
1451
+ const resolved = await resolveClaudeAuth(process.env);
1372
1452
  const insp = await inspectBox(box.id);
1373
1453
  if (insp.state === "missing") {
1374
1454
  throw new Error(`box ${box.name} has no container; was it destroyed?`);
@@ -1379,6 +1459,12 @@ async function startOrAttachClaude(box, claudeArgs, opts) {
1379
1459
  await attachClaudeWrapped(box, sessionName, reattachRef(box));
1380
1460
  return;
1381
1461
  }
1462
+ await maybeRunClaudeLogin({
1463
+ image: box.image,
1464
+ authSource: resolved.source,
1465
+ yes: false,
1466
+ hostWorkspace: box.workspacePath
1467
+ });
1382
1468
  const s = spinner();
1383
1469
  s.start("preparing box");
1384
1470
  if (insp.state === "paused") {
@@ -1401,7 +1487,12 @@ async function startOrAttachClaude(box, claudeArgs, opts) {
1401
1487
  }
1402
1488
  );
1403
1489
  }
1404
- await seedSetupSkillIntoVolume(box.claudeConfigVolume ?? SHARED_CLAUDE_VOLUME, box.image);
1490
+ const claudeVolume = box.claudeConfigVolume ?? SHARED_CLAUDE_VOLUME;
1491
+ await seedSetupSkillIntoVolume(claudeVolume, box.image);
1492
+ await syncClaudeCredentials(
1493
+ { volume: claudeVolume },
1494
+ { image: box.image, isolate: claudeVolume !== SHARED_CLAUDE_VOLUME }
1495
+ );
1405
1496
  s.message("checking plugin native deps");
1406
1497
  const rebuild = await rebuildPluginNativeDeps(box.container, {
1407
1498
  volume: box.claudeConfigVolume ?? SHARED_CLAUDE_VOLUME,
@@ -1415,6 +1506,7 @@ async function startOrAttachClaude(box, claudeArgs, opts) {
1415
1506
  boxName: box.name
1416
1507
  });
1417
1508
  s.stop(`box ${box.container} ready`);
1509
+ logPrune(rebuild);
1418
1510
  for (const f of rebuild.failed) {
1419
1511
  log5.warn(`plugin install failed for ${f.dir}; claude may still load it. stderr:
1420
1512
  ${f.stderr.trim()}`);
@@ -1423,19 +1515,16 @@ ${f.stderr.trim()}`);
1423
1515
  await attachClaudeWrapped(box, sessionName, reattachRef(box));
1424
1516
  }
1425
1517
  var claudeAttachCommand = new Command2("attach").description(
1426
- "Attach to a Claude Code tmux session in a box, starting one if none is running (auto-unpause/start)"
1518
+ "Attach to a Claude Code tmux session in a box, starting one if none is running (auto-unpause/start; never re-syncs ~/.claude \u2014 use `claude start` for that)"
1427
1519
  ).argument(
1428
1520
  "[box]",
1429
1521
  "box ref: project index, id, id prefix, name, or container (default: the only box in this project)"
1430
- ).option("--session-name <name>", "tmux session name (default from config; built-in: claude)").option(
1431
- "--no-sync-config",
1432
- "when starting a fresh session, skip rsyncing the host's ~/.claude into the box's volume (faster)"
1433
- ).action(async function(idOrName) {
1522
+ ).option("--session-name <name>", "tmux session name (default from config; built-in: claude)").action(async function(idOrName) {
1434
1523
  const opts = this.optsWithGlobals();
1435
1524
  intro("Attaching to Claude session...");
1436
1525
  try {
1437
1526
  const box = await resolveBoxOrExit(idOrName);
1438
- await startOrAttachClaude(box, [], opts);
1527
+ await startOrAttachClaude(box, [], { ...opts, syncConfig: false });
1439
1528
  } catch (err) {
1440
1529
  if (err instanceof ClaudeSessionError) {
1441
1530
  log5.error(err.message);
@@ -1470,12 +1559,43 @@ var claudeStartCommand = new Command2("start").description(
1470
1559
  handleLifecycleError(err);
1471
1560
  }
1472
1561
  });
1562
+ var claudeLoginCommand = new Command2("login").description(
1563
+ "Sign in to Claude for use in sandboxes (forwards args to `claude auth login`, e.g. --sso, --console). Runs in a throwaway container against the shared claude-config volume \u2014 usable before the first `agentbox claude`."
1564
+ ).argument(
1565
+ "[args...]",
1566
+ "extra args forwarded to `claude auth login`; place after `--`, e.g. `agentbox claude login -- --sso`"
1567
+ ).action(async (args) => {
1568
+ intro("Signing in to Claude...");
1569
+ if (!process.stdin.isTTY) {
1570
+ log5.error("`agentbox claude login` needs an interactive terminal.");
1571
+ process.exit(1);
1572
+ }
1573
+ try {
1574
+ const cfg = await loadEffectiveConfig(process.cwd());
1575
+ const image = cfg.effective.box.image;
1576
+ const s = spinner();
1577
+ s.start("preparing sandbox image");
1578
+ await ensureImage(image, { onProgress: (line) => s.message(clampSpinnerLine(line)) });
1579
+ s.stop("image ready");
1580
+ const exitCode = await runClaudeLoginContainer(image, args);
1581
+ if (exitCode !== 0) {
1582
+ log5.warn(`\`claude auth login\` exited with code ${String(exitCode)}`);
1583
+ process.exit(exitCode);
1584
+ }
1585
+ outro("signed in \u2014 credentials saved for future boxes");
1586
+ } catch (err) {
1587
+ handleLifecycleError(err);
1588
+ }
1589
+ });
1473
1590
  claudeCommand.addCommand(claudeAttachCommand);
1474
1591
  claudeCommand.addCommand(claudeStartCommand);
1592
+ claudeCommand.addCommand(claudeLoginCommand);
1475
1593
 
1476
1594
  // src/commands/checkpoint.ts
1477
1595
  import { confirm as confirm3, isCancel as isCancel3, log as log6 } from "@clack/prompts";
1478
1596
  import { Command as Command3 } from "commander";
1597
+ var CHECKPOINT_NOTICE = "Checkpoint in progress \u2014 the box will be unresponsive for a moment";
1598
+ var CHECKPOINT_NOTICE_TTL_MS = 66e4;
1479
1599
  async function projectRootFor(cwd, recordRoot) {
1480
1600
  return recordRoot ?? (await findProjectRoot(cwd)).root;
1481
1601
  }
@@ -1500,21 +1620,50 @@ var createSub = new Command3("create").description("Capture a box state as a pro
1500
1620
  }
1501
1621
  const projectRoot = await projectRootFor(box.workspacePath, box.projectRoot);
1502
1622
  const cfg = await loadEffectiveConfig(projectRoot);
1503
- const info = await createCheckpoint({
1504
- box,
1505
- projectRoot,
1506
- name: opts.name,
1507
- merged: opts.merged === true,
1508
- setDefault: opts.setDefault === true,
1509
- replace: opts.replace === true,
1510
- maxLayers: cfg.effective.checkpoint.maxLayers,
1511
- onLog: (line) => log6.info(line)
1512
- });
1513
- log6.success(
1514
- `checkpoint ${info.name} (${info.manifest.type}) -> ${info.dir}` + (opts.setDefault ? " [project default]" : "")
1623
+ const noticeId = await setRelayNotice(
1624
+ box.id,
1625
+ "checkpoint",
1626
+ CHECKPOINT_NOTICE,
1627
+ CHECKPOINT_NOTICE_TTL_MS
1515
1628
  );
1516
- if (!opts.setDefault) {
1517
- log6.info(`make it the default for new boxes: agentbox checkpoint set-default ${info.name}`);
1629
+ let signalled = false;
1630
+ const onSignal = () => {
1631
+ if (signalled) return;
1632
+ signalled = true;
1633
+ void (async () => {
1634
+ if (noticeId) await clearRelayNotice(box.id, noticeId);
1635
+ process.exit(130);
1636
+ })();
1637
+ };
1638
+ if (noticeId) {
1639
+ process.once("SIGINT", onSignal);
1640
+ process.once("SIGTERM", onSignal);
1641
+ }
1642
+ try {
1643
+ const info = await createCheckpoint({
1644
+ box,
1645
+ projectRoot,
1646
+ name: opts.name,
1647
+ merged: opts.merged === true,
1648
+ setDefault: opts.setDefault === true,
1649
+ replace: opts.replace === true,
1650
+ maxLayers: cfg.effective.checkpoint.maxLayers,
1651
+ onLog: (line) => log6.info(line)
1652
+ });
1653
+ log6.success(
1654
+ `checkpoint ${info.name} (${info.manifest.type}) -> ${info.dir}` + (opts.setDefault ? " [project default]" : "")
1655
+ );
1656
+ if (!opts.setDefault) {
1657
+ log6.info(
1658
+ `make it the default for new boxes: agentbox checkpoint set-default ${info.name}`
1659
+ );
1660
+ }
1661
+ } finally {
1662
+ if (noticeId) {
1663
+ await clearRelayNotice(box.id, noticeId);
1664
+ process.removeListener("SIGINT", onSignal);
1665
+ process.removeListener("SIGTERM", onSignal);
1666
+ }
1518
1667
  }
1519
1668
  } catch (err) {
1520
1669
  handleLifecycleError(err);
@@ -1598,7 +1747,7 @@ var rmSub = new Command3("rm").description("Delete a checkpoint").argument("<ref
1598
1747
  handleLifecycleError(err);
1599
1748
  }
1600
1749
  });
1601
- var checkpointCommand = new Command3("checkpoint").description("Capture and manage project checkpoints (warm box state new boxes can start from)").addCommand(createSub, { isDefault: true }).addCommand(lsSub).addCommand(setDefaultSub).addCommand(rmSub);
1750
+ var checkpointCommand = new Command3("checkpoint").alias("checkpoints").description("List and manage project checkpoints (warm box state new boxes can start from)").addCommand(createSub).addCommand(lsSub, { isDefault: true }).addCommand(setDefaultSub).addCommand(rmSub);
1602
1751
 
1603
1752
  // src/commands/code.ts
1604
1753
  import { spawn } from "child_process";
@@ -1761,7 +1910,7 @@ async function fetchServiceNames(container) {
1761
1910
  }
1762
1911
 
1763
1912
  // src/commands/config.ts
1764
- import { spawnSync as spawnSync4 } from "child_process";
1913
+ import { spawnSync as spawnSync3 } from "child_process";
1765
1914
  import { Command as Command5, InvalidArgumentError as InvalidArgumentError2 } from "commander";
1766
1915
  function resolveWriteScope(opts) {
1767
1916
  if (opts.global && opts.project) {
@@ -1986,7 +2135,7 @@ var editCommand = new Command5("edit").description("Open a config file in $EDITO
1986
2135
  const scope = resolveEditScope(opts);
1987
2136
  const path2 = await configPathFor(scope, process.cwd());
1988
2137
  const editor = process.env["EDITOR"] || process.env["VISUAL"] || "vi";
1989
- const child = spawnSync4(editor, [path2], { stdio: "inherit" });
2138
+ const child = spawnSync3(editor, [path2], { stdio: "inherit" });
1990
2139
  process.exit(child.status ?? 0);
1991
2140
  } catch (err) {
1992
2141
  handleError(err);
@@ -2240,7 +2389,7 @@ var cpCommand = new Command6("cp").description("Copy files between host and box
2240
2389
  // src/commands/create.ts
2241
2390
  import { intro as intro2, log as log9, outro as outro2, spinner as spinner2 } from "@clack/prompts";
2242
2391
  import { Command as Command7 } from "commander";
2243
- import { execSync, spawnSync as spawnSync5 } from "child_process";
2392
+ import { execSync, spawnSync as spawnSync4 } from "child_process";
2244
2393
  function buildCliOverrides(opts) {
2245
2394
  const box = {};
2246
2395
  if (opts.hostSnapshot !== void 0) box.hostSnapshot = opts.hostSnapshot;
@@ -2264,7 +2413,7 @@ var RELAY_HOST_URL2 = `http://127.0.0.1:${String(DEFAULT_RELAY_PORT)}`;
2264
2413
  async function attachShell(record) {
2265
2414
  const dockerArgv = ["exec", "-it", record.container, "bash"];
2266
2415
  if (!process.stdout.isTTY || !process.stdin.isTTY) {
2267
- const child = spawnSync5("docker", dockerArgv, { stdio: "inherit" });
2416
+ const child = spawnSync4("docker", dockerArgv, { stdio: "inherit" });
2268
2417
  process.exit(child.status ?? 0);
2269
2418
  }
2270
2419
  const code = await runWrappedAttach({
@@ -2802,6 +2951,7 @@ var POLL_MS = 1e3;
2802
2951
  var FRAME_MS = 16;
2803
2952
  var RESIZE_DEBOUNCE_MS = 120;
2804
2953
  var LEADER_LINGER_MS = 1500;
2954
+ var NOTICE_SPINNER_MS = 120;
2805
2955
  var SYNC_BEGIN2 = "\x1B[?2026h";
2806
2956
  var SYNC_END2 = "\x1B[?2026l";
2807
2957
  function cursorTo2(x, y) {
@@ -2894,6 +3044,16 @@ var Compositor = class {
2894
3044
  * we can dispose them when boxes disappear from the list.
2895
3045
  */
2896
3046
  activePrompts = /* @__PURE__ */ new Map();
3047
+ /**
3048
+ * Per-box active relay notice (currently: a checkpoint freezing the box).
3049
+ * Drives the `◆ checkpoint` sidebar cell and the animated status-bar
3050
+ * warning. Shares the SSE subscriptions in {@link promptStreams}.
3051
+ */
3052
+ activeNotices = /* @__PURE__ */ new Map();
3053
+ /** Monotonic spinner counter for the notice status bar. */
3054
+ noticeFrame = 0;
3055
+ /** Drives the spinner animation while {@link activeNotices} is non-empty. */
3056
+ noticeTimer = null;
2897
3057
  promptStreams = /* @__PURE__ */ new Map();
2898
3058
  activeMode = "claude";
2899
3059
  flashMsg = null;
@@ -2971,7 +3131,10 @@ var Compositor = class {
2971
3131
  if (!wanted.has(boxId)) {
2972
3132
  stream.close();
2973
3133
  this.promptStreams.delete(boxId);
2974
- if (this.activePrompts.delete(boxId)) this.drawChrome();
3134
+ let changed = this.activePrompts.delete(boxId);
3135
+ if (this.activeNotices.delete(boxId)) changed = true;
3136
+ if (this.activeNotices.size === 0) this.stopNoticeSpinner();
3137
+ if (changed) this.drawChrome();
2975
3138
  }
2976
3139
  }
2977
3140
  for (const boxId of wanted) {
@@ -2992,12 +3155,41 @@ var Compositor = class {
2992
3155
  this.drawChrome();
2993
3156
  }
2994
3157
  },
3158
+ onNotice: (ev) => {
3159
+ if (this.tornDown) return;
3160
+ this.activeNotices.set(boxId, ev);
3161
+ this.startNoticeSpinner();
3162
+ this.drawChrome();
3163
+ },
3164
+ onNoticeCleared: (id) => {
3165
+ if (this.tornDown) return;
3166
+ const current = this.activeNotices.get(boxId);
3167
+ if (current && current.id === id) {
3168
+ this.activeNotices.delete(boxId);
3169
+ if (this.activeNotices.size === 0) this.stopNoticeSpinner();
3170
+ this.drawChrome();
3171
+ }
3172
+ },
2995
3173
  onError: () => {
2996
3174
  }
2997
3175
  });
2998
3176
  this.promptStreams.set(boxId, stream);
2999
3177
  }
3000
3178
  }
3179
+ startNoticeSpinner() {
3180
+ if (this.noticeTimer) return;
3181
+ this.noticeTimer = setInterval(() => {
3182
+ this.noticeFrame++;
3183
+ this.drawChrome();
3184
+ }, NOTICE_SPINNER_MS);
3185
+ if (typeof this.noticeTimer.unref === "function") this.noticeTimer.unref();
3186
+ }
3187
+ stopNoticeSpinner() {
3188
+ if (this.noticeTimer) {
3189
+ clearInterval(this.noticeTimer);
3190
+ this.noticeTimer = null;
3191
+ }
3192
+ }
3001
3193
  selectedBox() {
3002
3194
  return this.boxes.find((b) => b.id === this.selectedId);
3003
3195
  }
@@ -3469,9 +3661,12 @@ var Compositor = class {
3469
3661
  drawChrome() {
3470
3662
  if (this.tornDown || this.layout.tooSmall) return;
3471
3663
  const { sidebar, sepX, statusY } = this.layout;
3472
- const boxesWithPrompt = this.activePrompts.size === 0 ? this.boxes : this.boxes.map(
3473
- (b) => this.activePrompts.has(b.id) ? { ...b, pendingPrompt: true } : b
3474
- );
3664
+ const decorate = this.activePrompts.size > 0 || this.activeNotices.size > 0;
3665
+ const boxesWithPrompt = decorate ? this.boxes.map((b) => {
3666
+ const pendingPrompt = this.activePrompts.has(b.id);
3667
+ const checkpointing = this.activeNotices.has(b.id);
3668
+ return pendingPrompt || checkpointing ? { ...b, pendingPrompt, checkpointing } : b;
3669
+ }) : this.boxes;
3475
3670
  const { lines, rowOwner, headerRows } = sidebarLines(
3476
3671
  boxesWithPrompt,
3477
3672
  this.selectedId,
@@ -3505,6 +3700,12 @@ var Compositor = class {
3505
3700
  { kind: "prompt", prompt: activePromptForSelected },
3506
3701
  this.layout.cols
3507
3702
  );
3703
+ } else if (this.activeNotices.has(this.selectedId)) {
3704
+ const notice = this.activeNotices.get(this.selectedId);
3705
+ status = renderFooter(
3706
+ { kind: "notice", message: notice.message, frame: this.noticeFrame },
3707
+ this.layout.cols
3708
+ );
3508
3709
  } else {
3509
3710
  const stateLabel = this.selectedId === NEW_BOX_ID ? "create" : this.menu ? "menu" : this.session && this.activeMode === "shell" ? "shell" : void 0;
3510
3711
  status = statusLine(
@@ -3540,9 +3741,11 @@ var Compositor = class {
3540
3741
  if (this.resizeTimer) clearTimeout(this.resizeTimer);
3541
3742
  if (this.flashTimer) clearTimeout(this.flashTimer);
3542
3743
  if (this.leaderLingerTimer) clearTimeout(this.leaderLingerTimer);
3744
+ if (this.noticeTimer) clearInterval(this.noticeTimer);
3543
3745
  for (const stream of this.promptStreams.values()) stream.close();
3544
3746
  this.promptStreams.clear();
3545
3747
  this.activePrompts.clear();
3748
+ this.activeNotices.clear();
3546
3749
  this.parser.dispose();
3547
3750
  this.disposeSession();
3548
3751
  this.inp.off("data", this.onData);
@@ -3655,6 +3858,11 @@ var dashboardCommand = new Command8("dashboard").description("Box list + the sel
3655
3858
  await rebuildPluginNativeDeps(box.container, {
3656
3859
  volume: box.claudeConfigVolume
3657
3860
  });
3861
+ const claudeVolume = box.claudeConfigVolume ?? SHARED_CLAUDE_VOLUME;
3862
+ await syncClaudeCredentials(
3863
+ { volume: claudeVolume },
3864
+ { image: box.image, isolate: claudeVolume !== SHARED_CLAUDE_VOLUME }
3865
+ );
3658
3866
  await startClaudeSession({ container: box.container, claudeArgs: [], boxName: box.name });
3659
3867
  const info = await claudeSessionInfo(box.container);
3660
3868
  return {
@@ -4609,7 +4817,7 @@ var restartSub = new Command19("restart").description("Stop then start the host
4609
4817
  var relayCommand = new Command19("relay").description("Manage the host relay process (status / stop / start / restart)").addCommand(statusSub, { isDefault: true }).addCommand(stopSub).addCommand(startSub).addCommand(restartSub);
4610
4818
 
4611
4819
  // src/commands/screen.ts
4612
- import { spawnSync as spawnSync6 } from "child_process";
4820
+ import { spawnSync as spawnSync5 } from "child_process";
4613
4821
  import { log as log20 } from "@clack/prompts";
4614
4822
  import { Command as Command20 } from "commander";
4615
4823
  var screenCommand = new Command20("screen").description("Open a box's VNC (noVNC) viewer in the browser (auto-unpause/start)").argument(
@@ -4657,7 +4865,7 @@ var screenCommand = new Command20("screen").description("Open a box's VNC (noVNC
4657
4865
  `);
4658
4866
  return;
4659
4867
  }
4660
- const opened = spawnSync6("open", [url], { stdio: "inherit" });
4868
+ const opened = spawnSync5("open", [url], { stdio: "inherit" });
4661
4869
  if (opened.status !== 0) {
4662
4870
  throw new Error(`open ${url} failed (exit ${String(opened.status ?? "n/a")})`);
4663
4871
  }
@@ -4669,7 +4877,7 @@ var screenCommand = new Command20("screen").description("Open a box's VNC (noVNC
4669
4877
  });
4670
4878
 
4671
4879
  // src/commands/shell.ts
4672
- import { spawnSync as spawnSync7 } from "child_process";
4880
+ import { spawnSync as spawnSync6 } from "child_process";
4673
4881
  import { log as log21 } from "@clack/prompts";
4674
4882
  import { Command as Command21 } from "commander";
4675
4883
  var RELAY_HOST_URL3 = `http://127.0.0.1:${String(DEFAULT_RELAY_PORT)}`;
@@ -4722,7 +4930,7 @@ var shellCommand = new Command21("shell").description("Open an interactive bash
4722
4930
  ...bashArgs
4723
4931
  ];
4724
4932
  if (!isInteractive || effectiveCmd.length > 0) {
4725
- const child = spawnSync7("docker", dockerArgv, { stdio: "inherit" });
4933
+ const child = spawnSync6("docker", dockerArgv, { stdio: "inherit" });
4726
4934
  process.exit(child.status ?? 0);
4727
4935
  }
4728
4936
  const code = await runWrappedAttach({