@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
@@ -8,12 +8,14 @@ import {
8
8
  removeContainer,
9
9
  sanitizeMnemonic,
10
10
  volumeExists
11
- } from "./chunk-RFC5F5HR.js";
11
+ } from "./chunk-HHMWQNLF.js";
12
12
 
13
- // ../../packages/sandbox-docker/dist/chunk-LGNJND37.js
13
+ // ../../packages/sandbox-docker/dist/chunk-SRQIM7LG.js
14
+ import { spawnSync } from "child_process";
14
15
  import { mkdir, mkdtemp, readdir, readFile, rm, stat, writeFile } from "fs/promises";
15
16
  import { homedir, tmpdir } from "os";
16
17
  import { join, relative } from "path";
18
+ import { setTimeout as delay } from "timers/promises";
17
19
  import { execa } from "execa";
18
20
  import { randomBytes } from "crypto";
19
21
  import { execa as execa2 } from "execa";
@@ -25,6 +27,7 @@ import { mkdir as mkdir2, readdir as readdir3, rm as rm2, stat as stat3 } from "
25
27
  import { homedir as homedir2, platform } from "os";
26
28
  import { join as join3, resolve } from "path";
27
29
  import { stat as stat4 } from "fs/promises";
30
+ import { execa as execa5 } from "execa";
28
31
  import { spawn } from "child_process";
29
32
  import { randomBytes as randomBytes2 } from "crypto";
30
33
  import { existsSync, openSync } from "fs";
@@ -32,7 +35,7 @@ import { mkdir as mkdir3, readFile as readFile2, unlink, writeFile as writeFile2
32
35
  import { request as httpRequest } from "http";
33
36
  import { homedir as homedir3 } from "os";
34
37
  import { dirname, join as join4, resolve as resolve2 } from "path";
35
- import { setTimeout as delay } from "timers/promises";
38
+ import { setTimeout as delay2 } from "timers/promises";
36
39
  import { fileURLToPath } from "url";
37
40
 
38
41
  // ../../packages/relay/dist/index.js
@@ -42,7 +45,7 @@ var RELAY_NETWORK_NAME = "agentbox-net";
42
45
  var RELAY_IMAGE_REF = "agentbox/relay:dev";
43
46
  var MAX_BODY_BYTES = 1024 * 1024;
44
47
 
45
- // ../../packages/sandbox-docker/dist/chunk-LGNJND37.js
48
+ // ../../packages/sandbox-docker/dist/chunk-SRQIM7LG.js
46
49
  function isHostPathHookCommand(command, hostHome) {
47
50
  if (typeof command !== "string" || command.length === 0) return false;
48
51
  if (hostHome.length === 0) return false;
@@ -79,6 +82,27 @@ function filterHostHooks(data, hostHome) {
79
82
  }
80
83
  return { data: clone, removedCommands };
81
84
  }
85
+ function trustWorkspace(data, workspacePath) {
86
+ const clone = structuredClone(data);
87
+ if (clone === null || typeof clone !== "object" || Array.isArray(clone)) {
88
+ return { data: clone, trusted: false };
89
+ }
90
+ if (workspacePath.length === 0) return { data: clone, trusted: false };
91
+ const obj = clone;
92
+ if (obj.projects === null || typeof obj.projects !== "object" || Array.isArray(obj.projects)) {
93
+ obj.projects = {};
94
+ }
95
+ const projects = obj.projects;
96
+ const existing = projects[workspacePath];
97
+ const entry = existing !== null && typeof existing === "object" && !Array.isArray(existing) ? existing : {};
98
+ if (entry.hasTrustDialogAccepted === true) {
99
+ projects[workspacePath] = entry;
100
+ return { data: clone, trusted: false };
101
+ }
102
+ entry.hasTrustDialogAccepted = true;
103
+ projects[workspacePath] = entry;
104
+ return { data: clone, trusted: true };
105
+ }
82
106
  function addProjectAlias(data, fromPath, toPath) {
83
107
  const clone = structuredClone(data);
84
108
  if (clone === null || typeof clone !== "object" || Array.isArray(clone)) {
@@ -105,17 +129,17 @@ function addProjectAlias(data, fromPath, toPath) {
105
129
  }
106
130
  return { data: clone, aliased: true };
107
131
  }
108
- function clearInstallMethod(data) {
132
+ function setInstallMethodNative(data) {
109
133
  const clone = structuredClone(data);
110
134
  if (clone === null || typeof clone !== "object" || Array.isArray(clone)) {
111
- return { data: clone, cleared: false };
135
+ return { data: clone, applied: false };
112
136
  }
113
137
  const obj = clone;
114
- if (Object.prototype.hasOwnProperty.call(obj, "installMethod")) {
115
- delete obj.installMethod;
116
- return { data: clone, cleared: true };
117
- }
118
- return { data: clone, cleared: false };
138
+ const changed = obj.installMethod !== "native" || obj.autoUpdates !== false || obj.autoUpdatesProtectedForNative !== true;
139
+ obj.installMethod = "native";
140
+ obj.autoUpdates = false;
141
+ obj.autoUpdatesProtectedForNative = true;
142
+ return { data: clone, applied: changed };
119
143
  }
120
144
  var SKILL_EXCLUDE_PREFIXES = ["agentbox-"];
121
145
  var CONTAINER_PLUGINS_PREFIX = "/home/vscode/.claude/plugins/";
@@ -189,6 +213,24 @@ function mergeInstalledPlugins(hostJson, boxJson, opts) {
189
213
  (host, merged) => ({ ...host, plugins: merged })
190
214
  );
191
215
  }
216
+ function referencedPluginVersionKeys(installedPluginsJson) {
217
+ const keys = /* @__PURE__ */ new Set();
218
+ if (!isPlainObject(installedPluginsJson)) return keys;
219
+ const plugins = installedPluginsJson["plugins"];
220
+ if (!isPlainObject(plugins)) return keys;
221
+ for (const entries of Object.values(plugins)) {
222
+ if (!Array.isArray(entries)) continue;
223
+ for (const entry of entries) {
224
+ if (!isPlainObject(entry)) continue;
225
+ const installPath = entry["installPath"];
226
+ if (typeof installPath !== "string") continue;
227
+ const segments = installPath.split("/").filter((s) => s.length > 0);
228
+ if (segments.length < 3) continue;
229
+ keys.add(segments.slice(-3).join("/"));
230
+ }
231
+ }
232
+ return keys;
233
+ }
192
234
  var SHARED_CLAUDE_VOLUME = "agentbox-claude-config";
193
235
  var DEFAULT_CLAUDE_SESSION = "claude";
194
236
  var CONTAINER_CLAUDE_DIR = "/home/vscode/.claude";
@@ -210,6 +252,14 @@ async function pathExists(p) {
210
252
  return false;
211
253
  }
212
254
  }
255
+ async function volumeHasClaudeJson(volume, image) {
256
+ const res = await execa(
257
+ "docker",
258
+ ["run", "--rm", "-v", `${volume}:/dst`, image, "sh", "-c", "test -e /dst/_claude.json"],
259
+ { reject: false }
260
+ );
261
+ return res.exitCode === 0;
262
+ }
213
263
  async function findBrokenSymlinks(root) {
214
264
  const broken = [];
215
265
  async function walk(dir) {
@@ -244,6 +294,7 @@ async function ensureClaudeVolume(spec, opts) {
244
294
  if (!await pathExists(hostClaude)) return { created, synced: false };
245
295
  const hostClaudeJson = join(homedir(), ".claude.json");
246
296
  const hasJson = await pathExists(hostClaudeJson);
297
+ const seedClaudeJson = !await volumeHasClaudeJson(spec.volume, opts.image);
247
298
  const hostHome = homedir();
248
299
  const hostAgents = join(homedir(), ".agents");
249
300
  const hasAgents = await pathExists(hostAgents);
@@ -261,12 +312,13 @@ async function ensureClaudeVolume(spec, opts) {
261
312
  "-v",
262
313
  `${hostClaude}:/src-claude:ro`
263
314
  ];
264
- if (hasJson) args.push("-v", `${hostClaudeJson}:/src-claude-json:ro`);
315
+ if (hasJson && seedClaudeJson) args.push("-v", `${hostClaudeJson}:/src-claude-json:ro`);
265
316
  if (hasAgents) args.push("-v", `${hostAgents}:/.agents:ro`);
266
317
  const filterDir = await mkdtemp(join(tmpdir(), "agentbox-claude-filter-"));
267
318
  let filteredHookCount = 0;
268
- let clearedInstallMethod = false;
319
+ let installMethodFixed = false;
269
320
  let aliasedProjectKey = false;
321
+ let workspaceTrusted = false;
270
322
  try {
271
323
  const settingsResult = await maybeFilterTo(
272
324
  join(hostClaude, "settings.json"),
@@ -274,21 +326,40 @@ async function ensureClaudeVolume(spec, opts) {
274
326
  hostHome
275
327
  );
276
328
  filteredHookCount += settingsResult.removedHooks;
277
- if (hasJson) {
329
+ if (!seedClaudeJson) {
330
+ } else if (hasJson) {
278
331
  const jsonResult = await maybeFilterTo(
279
332
  hostClaudeJson,
280
333
  join(filterDir, "_claude.json"),
281
334
  hostHome,
282
335
  {
283
- clearInstallMethod: true,
284
- aliasProject: opts.hostWorkspace ? { from: opts.hostWorkspace, to: CONTAINER_WORKSPACE } : void 0
336
+ setInstallMethodNative: true,
337
+ aliasProject: opts.hostWorkspace ? { from: opts.hostWorkspace, to: CONTAINER_WORKSPACE } : void 0,
338
+ trustWorkspacePath: CONTAINER_WORKSPACE
285
339
  }
286
340
  );
287
341
  filteredHookCount += jsonResult.removedHooks;
288
- clearedInstallMethod = jsonResult.clearedInstallMethod;
342
+ installMethodFixed = jsonResult.installMethodFixed;
289
343
  aliasedProjectKey = jsonResult.aliasedProjectKey;
344
+ workspaceTrusted = jsonResult.workspaceTrusted;
345
+ } else {
346
+ await writeFile(
347
+ join(filterDir, "_claude.json"),
348
+ JSON.stringify(
349
+ {
350
+ installMethod: "native",
351
+ autoUpdates: false,
352
+ autoUpdatesProtectedForNative: true,
353
+ projects: { [CONTAINER_WORKSPACE]: { hasTrustDialogAccepted: true } }
354
+ },
355
+ null,
356
+ 2
357
+ )
358
+ );
359
+ installMethodFixed = true;
360
+ workspaceTrusted = true;
290
361
  }
291
- if (filteredHookCount > 0 || clearedInstallMethod || aliasedProjectKey) {
362
+ if (filteredHookCount > 0 || installMethodFixed || aliasedProjectKey || workspaceTrusted) {
292
363
  args.push("-v", `${filterDir}:/src-filter:ro`);
293
364
  }
294
365
  const brokenSymlinks = await findBrokenSymlinks(hostClaude);
@@ -335,7 +406,14 @@ async function ensureClaudeVolume(spec, opts) {
335
406
  } finally {
336
407
  await rm(filterDir, { recursive: true, force: true });
337
408
  }
338
- return { created, synced: true, filteredHookCount, clearedInstallMethod, aliasedProjectKey };
409
+ return {
410
+ created,
411
+ synced: true,
412
+ filteredHookCount,
413
+ installMethodFixed,
414
+ aliasedProjectKey,
415
+ workspaceTrusted
416
+ };
339
417
  }
340
418
  async function seedSetupSkillIntoVolume(volume, image) {
341
419
  try {
@@ -349,10 +427,10 @@ async function seedSetupSkillIntoVolume(volume, image) {
349
427
  image,
350
428
  "sh",
351
429
  "-c",
352
- // Prints SEEDED only when it actually copies, so the caller can log
353
- // accurately. The whole thing is `|| true` so an already-present skill
354
- // (or missing image asset) is a clean no-op, never a non-zero exit.
355
- `{ [ ! -e /dst/skills/agentbox-setup ] && [ -f ${IN_BOX_SETUP_GUIDE_PATH} ] && mkdir -p /dst/skills/agentbox-setup && cp -a ${IN_BOX_SETUP_GUIDE_PATH} ${SETUP_SKILL_DST} && chown -R 1000:1000 /dst/skills/agentbox-setup && echo SEEDED; } || true`
430
+ // Always overwrite from the image so an image upgrade propagates. Prints
431
+ // SEEDED on success; the whole thing is `|| true` so a missing image
432
+ // asset is a clean no-op, never a non-zero exit.
433
+ `{ [ -f ${IN_BOX_SETUP_GUIDE_PATH} ] && rm -rf /dst/skills/agentbox-setup && mkdir -p /dst/skills/agentbox-setup && cp -a ${IN_BOX_SETUP_GUIDE_PATH} ${SETUP_SKILL_DST} && chown -R 1000:1000 /dst/skills/agentbox-setup && echo SEEDED; } || true`
356
434
  ]);
357
435
  return { seeded: stdout.includes("SEEDED") };
358
436
  } catch {
@@ -360,19 +438,25 @@ async function seedSetupSkillIntoVolume(volume, image) {
360
438
  }
361
439
  }
362
440
  async function maybeFilterTo(src, dest, hostHome, opts = {}) {
441
+ const zero = {
442
+ removedHooks: 0,
443
+ installMethodFixed: false,
444
+ aliasedProjectKey: false,
445
+ workspaceTrusted: false
446
+ };
363
447
  let parsed;
364
448
  try {
365
449
  parsed = JSON.parse(await readFile(src, "utf8"));
366
450
  } catch {
367
- return { removedHooks: 0, clearedInstallMethod: false, aliasedProjectKey: false };
451
+ return zero;
368
452
  }
369
453
  const filtered = filterHostHooks(parsed, hostHome);
370
454
  let working = filtered.data;
371
- let cleared = false;
372
- if (opts.clearInstallMethod) {
373
- const r = clearInstallMethod(working);
455
+ let installFixed = false;
456
+ if (opts.setInstallMethodNative) {
457
+ const r = setInstallMethodNative(working);
374
458
  working = r.data;
375
- cleared = r.cleared;
459
+ installFixed = r.applied;
376
460
  }
377
461
  let aliased = false;
378
462
  if (opts.aliasProject) {
@@ -380,14 +464,21 @@ async function maybeFilterTo(src, dest, hostHome, opts = {}) {
380
464
  working = r.data;
381
465
  aliased = r.aliased;
382
466
  }
383
- if (filtered.removedCommands.length === 0 && !cleared && !aliased) {
384
- return { removedHooks: 0, clearedInstallMethod: false, aliasedProjectKey: false };
467
+ let trusted = false;
468
+ if (opts.trustWorkspacePath) {
469
+ const r = trustWorkspace(working, opts.trustWorkspacePath);
470
+ working = r.data;
471
+ trusted = r.trusted;
472
+ }
473
+ if (filtered.removedCommands.length === 0 && !installFixed && !aliased && !trusted) {
474
+ return zero;
385
475
  }
386
476
  await writeFile(dest, JSON.stringify(working, null, 2));
387
477
  return {
388
478
  removedHooks: filtered.removedCommands.length,
389
- clearedInstallMethod: cleared,
390
- aliasedProjectKey: aliased
479
+ installMethodFixed: installFixed,
480
+ aliasedProjectKey: aliased,
481
+ workspaceTrusted: trusted
391
482
  };
392
483
  }
393
484
  var FORWARDED_ENV_KEYS = [
@@ -435,7 +526,18 @@ async function isDir(p) {
435
526
  return false;
436
527
  }
437
528
  }
529
+ async function readReferencedPluginKeys(installedPluginsJsonPath) {
530
+ try {
531
+ const raw = await readFile(installedPluginsJsonPath, "utf8");
532
+ return referencedPluginVersionKeys(JSON.parse(raw));
533
+ } catch {
534
+ return /* @__PURE__ */ new Set();
535
+ }
536
+ }
438
537
  async function scanPluginCacheForRebuild(cacheRoot) {
538
+ const referenced = await readReferencedPluginKeys(
539
+ join(cacheRoot, "..", "installed_plugins.json")
540
+ );
439
541
  let marketplaces;
440
542
  try {
441
543
  marketplaces = await readdir(cacheRoot, { withFileTypes: true });
@@ -462,6 +564,7 @@ async function scanPluginCacheForRebuild(cacheRoot) {
462
564
  }
463
565
  for (const v of versions) {
464
566
  if (!v.isDirectory()) continue;
567
+ if (referenced.size > 0 && !referenced.has(`${m.name}/${p.name}/${v.name}`)) continue;
465
568
  const vPath = join(pPath, v.name);
466
569
  if (!await isFile(join(vPath, "package.json"))) continue;
467
570
  if (await isFile(join(vPath, PLUGIN_INSTALLED_MARKER))) continue;
@@ -477,13 +580,38 @@ async function resolveClaudeCacheLiveOnHost(volume) {
477
580
  if (!await isDir(orbstackVolumePath(volume))) return null;
478
581
  return orbstackVolumePath(volume, "plugins", "cache");
479
582
  }
583
+ async function readBoxReferencedPluginKeys(container) {
584
+ const res = await execa(
585
+ "docker",
586
+ [
587
+ "exec",
588
+ "--user",
589
+ CONTAINER_USER,
590
+ container,
591
+ "cat",
592
+ `${CONTAINER_CLAUDE_DIR}/plugins/installed_plugins.json`
593
+ ],
594
+ { reject: false }
595
+ );
596
+ if (res.exitCode !== 0 || !res.stdout) return /* @__PURE__ */ new Set();
597
+ try {
598
+ return referencedPluginVersionKeys(JSON.parse(res.stdout));
599
+ } catch {
600
+ return /* @__PURE__ */ new Set();
601
+ }
602
+ }
480
603
  async function rebuildPluginNativeDeps(container, opts = {}) {
481
604
  if (opts.volume) {
482
605
  const cacheRoot = await resolveClaudeCacheLiveOnHost(opts.volume);
483
606
  if (cacheRoot && !await scanPluginCacheForRebuild(cacheRoot)) {
484
- return { rebuilt: [], failed: [], skipped: true };
607
+ return { rebuilt: [], failed: [], pruned: [], prunedBytes: 0, skipped: true };
485
608
  }
486
609
  }
610
+ const referenced = await readBoxReferencedPluginKeys(container);
611
+ const refSetup = referenced.size > 0 ? `cat <<'AGENTBOX_REF_EOF' > "$WORK/referenced"
612
+ ${[...referenced].sort().join("\n")}
613
+ AGENTBOX_REF_EOF
614
+ ` : "";
487
615
  const script = `set -u
488
616
  PLUGINS_DIR=/home/vscode/.claude/plugins/cache
489
617
  MARKER=${PLUGIN_INSTALLED_MARKER}
@@ -494,7 +622,12 @@ MAX=4
494
622
  [ -d "$PLUGINS_DIR" ] || exit 0
495
623
  mkdir -p "$NPM_CACHE"
496
624
  WORK=$(mktemp -d)
497
- relkey() { printf '%s' "\${1#$PLUGINS_DIR/}" | tr '/' '_'; }
625
+ ${refSetup}relkey() { printf '%s' "\${1#$PLUGINS_DIR/}" | tr '/' '_'; }
626
+ # True when refs are unknown (no file) or $1 (<m>/<p>/<v>) is referenced.
627
+ is_referenced() {
628
+ [ -s "$WORK/referenced" ] || return 0
629
+ grep -Fxq "$1" "$WORK/referenced"
630
+ }
498
631
  # Run one plugin's install. $1 is frozen by value at call time, so it's safe
499
632
  # to read from the backgrounded subshell; the rest are set-once constants.
500
633
  do_one() {
@@ -514,10 +647,29 @@ do_one() {
514
647
  printf 'FAIL\\n' > "$WORK/$key.res"
515
648
  fi
516
649
  }
650
+ # Prune pass: every unreferenced (stale) version dir loses its node_modules and
651
+ # our markers. Only runs when installed_plugins.json gave us a reference set.
652
+ if [ -s "$WORK/referenced" ]; then
653
+ for dir in "$PLUGINS_DIR"/*/*/*/; do
654
+ [ -d "$dir" ] || continue
655
+ rel=\${dir%/}; rel=\${rel#$PLUGINS_DIR/}
656
+ grep -Fxq "$rel" "$WORK/referenced" && continue
657
+ if [ -d "$dir/node_modules" ]; then
658
+ bytes=$(du -sb "$dir/node_modules" 2>/dev/null | cut -f1)
659
+ [ -n "$bytes" ] || bytes=0
660
+ rm -rf "$dir/node_modules" "$dir/$MARKER" "$dir/$FAILMARKER"
661
+ echo "PRUNE_OK $rel $bytes"
662
+ else
663
+ rm -f "$dir/$MARKER" "$dir/$FAILMARKER"
664
+ fi
665
+ done
666
+ fi
517
667
  n=0
518
668
  for dir in "$PLUGINS_DIR"/*/*/*/; do
519
669
  [ -d "$dir" ] || continue
520
670
  [ -f "$dir/package.json" ] || continue
671
+ rel=\${dir%/}; rel=\${rel#$PLUGINS_DIR/}
672
+ is_referenced "$rel" || continue
521
673
  [ -f "$dir/$MARKER" ] && continue
522
674
  [ -n "$(find "$dir" -maxdepth 1 -name "$FAILMARKER" -mmin -$BACKOFF_MIN 2>/dev/null)" ] && continue
523
675
  echo "REBUILD_START \${dir#$PLUGINS_DIR/}"
@@ -554,6 +706,8 @@ rm -rf "$WORK"
554
706
  );
555
707
  const rebuilt = [];
556
708
  const failed = [];
709
+ const pruned = [];
710
+ let prunedBytes = 0;
557
711
  const lines = (result.stdout ?? "").split("\n");
558
712
  let collectingFail = null;
559
713
  for (const line of lines) {
@@ -572,9 +726,19 @@ rm -rf "$WORK"
572
726
  rebuilt.push(line.slice("REBUILD_OK ".length));
573
727
  } else if (line.startsWith("REBUILD_FAIL ")) {
574
728
  collectingFail = { dir: line.slice("REBUILD_FAIL ".length), stderr: [] };
729
+ } else if (line.startsWith("PRUNE_OK ")) {
730
+ const rest = line.slice("PRUNE_OK ".length);
731
+ const sp = rest.lastIndexOf(" ");
732
+ if (sp > 0) {
733
+ const dir = rest.slice(0, sp);
734
+ const bytes = Number(rest.slice(sp + 1));
735
+ pruned.push(dir);
736
+ if (Number.isFinite(bytes)) prunedBytes += bytes;
737
+ opts.onProgress?.(`pruning stale plugin cache ${dir}`);
738
+ }
575
739
  }
576
740
  }
577
- return { rebuilt, failed, skipped: false };
741
+ return { rebuilt, failed, pruned, prunedBytes, skipped: false };
578
742
  }
579
743
  var ClaudeSessionError = class extends Error {
580
744
  constructor(message) {
@@ -732,6 +896,63 @@ function buildShellArgv(container) {
732
896
  const term = process.env["TERM"] ?? "xterm-256color";
733
897
  return ["exec", "-it", "-e", `TERM=${term}`, "--user", CONTAINER_USER, container, "bash", "-l"];
734
898
  }
899
+ function buildClaudeLoginRunArgv(opts) {
900
+ const term = process.env["TERM"] ?? "xterm-256color";
901
+ return [
902
+ "run",
903
+ "-it",
904
+ "--rm",
905
+ "-e",
906
+ `TERM=${term}`,
907
+ "-e",
908
+ "DISPLAY=",
909
+ "-v",
910
+ `${opts.volume}:${CONTAINER_CLAUDE_DIR}`,
911
+ "--user",
912
+ CONTAINER_USER,
913
+ opts.image,
914
+ "claude",
915
+ "auth",
916
+ "login",
917
+ ...opts.extraArgs
918
+ ];
919
+ }
920
+ function runInteractiveClaudeLogin(dockerArgv) {
921
+ const child = spawnSync("docker", dockerArgv, { stdio: "inherit" });
922
+ return { exitCode: child.status ?? 1 };
923
+ }
924
+ async function warmUpClaudeCredentials(volume, image, opts = {}) {
925
+ const MAX_ATTEMPTS = 6;
926
+ const SLEEP_MS = 5e3;
927
+ for (let attempt = 1; attempt <= MAX_ATTEMPTS; attempt++) {
928
+ opts.onProgress?.(`checking credentials... ${attempt}/${MAX_ATTEMPTS}`);
929
+ const res = await execa(
930
+ "docker",
931
+ [
932
+ "run",
933
+ "--rm",
934
+ "-v",
935
+ `${volume}:${CONTAINER_CLAUDE_DIR}`,
936
+ "--user",
937
+ CONTAINER_USER,
938
+ "-e",
939
+ "DISABLE_AUTOUPDATER=1",
940
+ image,
941
+ "claude",
942
+ "--dangerously-skip-permissions",
943
+ "-p",
944
+ "ok"
945
+ ],
946
+ { reject: false, timeout: 6e4 }
947
+ );
948
+ const out = `${res.stdout ?? ""}
949
+ ${res.stderr ?? ""}`;
950
+ const apiError = /API Error|is not supported on this model|"type":\s*"error"/i.test(out);
951
+ if (res.exitCode === 0 && !apiError) return { warmed: true, attempts: attempt };
952
+ if (attempt < MAX_ATTEMPTS) await delay(SLEEP_MS);
953
+ }
954
+ return { warmed: false, attempts: MAX_ATTEMPTS };
955
+ }
735
956
  function formatDetachNotice(ref) {
736
957
  return `Session detached. Reattach with: agentbox claude attach ${ref}`;
737
958
  }
@@ -1357,8 +1578,10 @@ async function createSnapshot(opts) {
1357
1578
  await Promise.all(toPrune.map((p) => rm2(p, { recursive: true, force: true })));
1358
1579
  return { destination, prunedPaths: toPrune };
1359
1580
  }
1581
+ var CTL_DAEMON_LOG = "/var/log/agentbox/ctl-daemon.log";
1360
1582
  async function launchCtlDaemon(container, hostSocketPath, timeoutMs = 3e3) {
1361
- const result = await execInBox(container, ["agentbox-ctl", "daemon"], {
1583
+ const wrapped = `mkdir -p ${CTL_DAEMON_LOG.replace(/\/[^/]*$/, "")} && exec agentbox-ctl daemon >>${CTL_DAEMON_LOG} 2>&1`;
1584
+ const result = await execInBox(container, ["sh", "-c", wrapped], {
1362
1585
  user: "vscode",
1363
1586
  detach: true
1364
1587
  });
@@ -1383,6 +1606,23 @@ async function pathExists2(p) {
1383
1606
  return false;
1384
1607
  }
1385
1608
  }
1609
+ async function ensureHomeOwnedByVscode(container) {
1610
+ await execa5(
1611
+ "docker",
1612
+ [
1613
+ "exec",
1614
+ "--user",
1615
+ "root",
1616
+ container,
1617
+ "chown",
1618
+ "-R",
1619
+ "--from=root",
1620
+ "vscode:vscode",
1621
+ "/home/vscode"
1622
+ ],
1623
+ { reject: false }
1624
+ );
1625
+ }
1386
1626
  var STATE_DIR = join4(homedir3(), ".agentbox");
1387
1627
  var PID_FILE = join4(STATE_DIR, "relay.pid");
1388
1628
  var LOG_FILE = join4(STATE_DIR, "relay.log");
@@ -1411,7 +1651,7 @@ async function ensureRelay(opts = {}) {
1411
1651
  if (existingPid !== null && await processAlive(existingPid)) {
1412
1652
  for (let i = 0; i < 10; i++) {
1413
1653
  if (await pingHealthz(300)) return ENDPOINT;
1414
- await delay(200);
1654
+ await delay2(200);
1415
1655
  }
1416
1656
  log(`relay pid ${String(existingPid)} alive but /healthz unresponsive \u2014 proceeding anyway`);
1417
1657
  return ENDPOINT;
@@ -1445,7 +1685,7 @@ async function ensureRelay(opts = {}) {
1445
1685
  log(`relay reachable on ${ENDPOINT.hostUrl}`);
1446
1686
  return ENDPOINT;
1447
1687
  }
1448
- await delay(200);
1688
+ await delay2(200);
1449
1689
  }
1450
1690
  throw new Error(
1451
1691
  `relay did not become reachable on ${ENDPOINT.hostUrl} within 5s; see ${LOG_FILE}`
@@ -1501,7 +1741,7 @@ async function stopRelay() {
1501
1741
  }
1502
1742
  for (let i = 0; i < 20; i++) {
1503
1743
  if (!await processAlive(pid)) break;
1504
- await delay(100);
1744
+ await delay2(100);
1505
1745
  }
1506
1746
  if (await processAlive(pid)) {
1507
1747
  try {
@@ -1624,6 +1864,26 @@ async function forgetBoxFromRelay(boxId) {
1624
1864
  } catch {
1625
1865
  }
1626
1866
  }
1867
+ async function setRelayNotice(boxId, kind, message, ttlMs) {
1868
+ try {
1869
+ const body = await adminPostForJson("/admin/notices/set", {
1870
+ boxId,
1871
+ kind,
1872
+ message,
1873
+ ...typeof ttlMs === "number" ? { ttlMs } : {}
1874
+ });
1875
+ const id = body?.id;
1876
+ return typeof id === "string" && id.length > 0 ? id : null;
1877
+ } catch {
1878
+ return null;
1879
+ }
1880
+ }
1881
+ async function clearRelayNotice(boxId, id) {
1882
+ try {
1883
+ await adminPost("/admin/notices/clear", { boxId, id });
1884
+ } catch {
1885
+ }
1886
+ }
1627
1887
  async function adminPost(path, body) {
1628
1888
  const json = JSON.stringify(body);
1629
1889
  await new Promise((resolveP, rejectP) => {
@@ -1662,6 +1922,49 @@ async function adminPost(path, body) {
1662
1922
  req.end();
1663
1923
  });
1664
1924
  }
1925
+ async function adminPostForJson(path, body) {
1926
+ const json = JSON.stringify(body);
1927
+ return new Promise((resolveP, rejectP) => {
1928
+ const req = httpRequest(
1929
+ {
1930
+ host: "127.0.0.1",
1931
+ port: PORT,
1932
+ method: "POST",
1933
+ path,
1934
+ headers: {
1935
+ "Content-Type": "application/json",
1936
+ "Content-Length": Buffer.byteLength(json).toString()
1937
+ },
1938
+ timeout: 3e3
1939
+ },
1940
+ (res) => {
1941
+ const chunks = [];
1942
+ res.on("data", (c) => chunks.push(c));
1943
+ res.on("end", () => {
1944
+ const status = res.statusCode ?? 0;
1945
+ if (status < 200 || status >= 300) {
1946
+ rejectP(new Error(`relay ${path} \u2192 ${String(status)}`));
1947
+ return;
1948
+ }
1949
+ const text = Buffer.concat(chunks).toString("utf8");
1950
+ try {
1951
+ resolveP(text.length > 0 ? JSON.parse(text) : {});
1952
+ } catch (err) {
1953
+ rejectP(err instanceof Error ? err : new Error(String(err)));
1954
+ }
1955
+ });
1956
+ res.on("error", rejectP);
1957
+ }
1958
+ );
1959
+ req.on("error", rejectP);
1960
+ req.on("timeout", () => {
1961
+ req.destroy();
1962
+ rejectP(new Error(`relay ${path} timeout`));
1963
+ });
1964
+ req.write(json);
1965
+ req.end();
1966
+ });
1967
+ }
1665
1968
  async function rehydrateRelayRegistry(boxes) {
1666
1969
  for (const b of boxes) {
1667
1970
  if (!b.relayToken) continue;
@@ -1782,8 +2085,8 @@ async function ensureAgentboxTasksFile(container, services, opts = {}) {
1782
2085
  return { status: "wrote" };
1783
2086
  }
1784
2087
  async function writeFileInBox(container, path, content) {
1785
- const { execa: execa5 } = await import("execa");
1786
- const result = await execa5(
2088
+ const { execa: execa6 } = await import("execa");
2089
+ const result = await execa6(
1787
2090
  "docker",
1788
2091
  ["exec", "-i", "--user", "vscode", container, "sh", "-c", `cat > ${shellQuote(path)}`],
1789
2092
  { input: content, reject: false }
@@ -2296,6 +2599,9 @@ export {
2296
2599
  buildClaudeAttachArgv,
2297
2600
  buildClaudeDashboardAttachArgv,
2298
2601
  buildShellArgv,
2602
+ buildClaudeLoginRunArgv,
2603
+ runInteractiveClaudeLogin,
2604
+ warmUpClaudeCredentials,
2299
2605
  formatDetachNotice,
2300
2606
  claudeSessionInfo,
2301
2607
  pullClaudeExtras,
@@ -2320,12 +2626,15 @@ export {
2320
2626
  snapshotPathFor,
2321
2627
  createSnapshot,
2322
2628
  launchCtlDaemon,
2629
+ ensureHomeOwnedByVscode,
2323
2630
  ensureRelay,
2324
2631
  stopRelay,
2325
2632
  getRelayStatus,
2326
2633
  generateRelayToken,
2327
2634
  registerBoxWithRelay,
2328
2635
  forgetBoxFromRelay,
2636
+ setRelayNotice,
2637
+ clearRelayNotice,
2329
2638
  rehydrateRelayRegistry,
2330
2639
  ideProfile,
2331
2640
  SHARED_VSCODE_EXTENSIONS_VOLUME,
@@ -2343,4 +2652,4 @@ export {
2343
2652
  ConfigError,
2344
2653
  loadConfig
2345
2654
  };
2346
- //# sourceMappingURL=chunk-PXUBE5KS.js.map
2655
+ //# sourceMappingURL=chunk-HTTKML3C.js.map