@madarco/agentbox 0.4.1 → 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 (40) hide show
  1. package/dist/{chunk-J35IH7W5.js → chunk-BBZMA2K6.js} +61 -23
  2. package/dist/chunk-BBZMA2K6.js.map +1 -0
  3. package/dist/{chunk-SOMIKEN2.js → chunk-HHMWQNLF.js} +272 -214
  4. package/dist/chunk-HHMWQNLF.js.map +1 -0
  5. package/dist/{chunk-IDR4HVIC.js → chunk-HPZMD5DE.js} +2 -2
  6. package/dist/chunk-HPZMD5DE.js.map +1 -0
  7. package/dist/{chunk-NSIECUCS.js → chunk-HTTKML3C.js} +705 -289
  8. package/dist/chunk-HTTKML3C.js.map +1 -0
  9. package/dist/{chunk-WR5FFGE5.js → chunk-KJNZP6I3.js} +218 -128
  10. package/dist/chunk-KJNZP6I3.js.map +1 -0
  11. package/dist/{chunk-FQD6ZWYW.js → chunk-M7I247BK.js} +68 -65
  12. package/dist/chunk-M7I247BK.js.map +1 -0
  13. package/dist/create-6PWXI6HO-OWAMHBAK.js +15 -0
  14. package/dist/index.js +2394 -1283
  15. package/dist/index.js.map +1 -1
  16. package/dist/{lifecycle-LURNDNYO-UWQYPNPX.js → lifecycle-EMXR46DI-DUVBXNTV.js} +5 -5
  17. package/dist/{state-ZSP3ORXW-WI6KOIG3.js → state-KD7M46ZP-KHFTHFUS.js} +2 -2
  18. package/dist/stats-SZXOJE3D-N7OODCHW.js +19 -0
  19. package/package.json +3 -2
  20. package/runtime/docker/Dockerfile.box +65 -25
  21. package/runtime/docker/apps/cli/share/agentbox-setup/SKILL.md +52 -55
  22. package/runtime/docker/packages/ctl/dist/bin.cjs +272 -160
  23. package/runtime/docker/packages/sandbox-docker/scripts/agentbox-checkpoint-cleanup +52 -0
  24. package/runtime/docker/packages/sandbox-docker/scripts/agentbox-dockerd-start +87 -7
  25. package/runtime/docker/packages/sandbox-docker/scripts/agentbox-open +28 -0
  26. package/runtime/docker/packages/sandbox-docker/scripts/custom-system-CLAUDE.md +21 -15
  27. package/runtime/relay/bin.cjs +407 -12
  28. package/share/agentbox-setup/SKILL.md +52 -55
  29. package/dist/chunk-FQD6ZWYW.js.map +0 -1
  30. package/dist/chunk-IDR4HVIC.js.map +0 -1
  31. package/dist/chunk-J35IH7W5.js.map +0 -1
  32. package/dist/chunk-NSIECUCS.js.map +0 -1
  33. package/dist/chunk-SOMIKEN2.js.map +0 -1
  34. package/dist/chunk-WR5FFGE5.js.map +0 -1
  35. package/dist/create-4BQY2UYU-CGSW3RGE.js +0 -15
  36. package/dist/stats-GZFLPYTU-DBJ2DVBJ.js +0 -19
  37. /package/dist/{create-4BQY2UYU-CGSW3RGE.js.map → create-6PWXI6HO-OWAMHBAK.js.map} +0 -0
  38. /package/dist/{lifecycle-LURNDNYO-UWQYPNPX.js.map → lifecycle-EMXR46DI-DUVBXNTV.js.map} +0 -0
  39. /package/dist/{state-ZSP3ORXW-WI6KOIG3.js.map → state-KD7M46ZP-KHFTHFUS.js.map} +0 -0
  40. /package/dist/{stats-GZFLPYTU-DBJ2DVBJ.js.map → stats-SZXOJE3D-N7OODCHW.js.map} +0 -0
@@ -6,24 +6,28 @@ import {
6
6
  execInBox,
7
7
  orbstackVolumePath,
8
8
  removeContainer,
9
+ sanitizeMnemonic,
9
10
  volumeExists
10
- } from "./chunk-SOMIKEN2.js";
11
+ } from "./chunk-HHMWQNLF.js";
11
12
 
12
- // ../../packages/sandbox-docker/dist/chunk-NY7PB7KY.js
13
+ // ../../packages/sandbox-docker/dist/chunk-SRQIM7LG.js
13
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";
20
22
  import { readdir as readdir2, stat as stat2 } from "fs/promises";
21
23
  import { join as join2 } from "path";
22
24
  import { execa as execa3 } from "execa";
25
+ import { execa as execa4 } from "execa";
23
26
  import { mkdir as mkdir2, readdir as readdir3, rm as rm2, stat as stat3 } from "fs/promises";
24
27
  import { homedir as homedir2, platform } from "os";
25
28
  import { join as join3, resolve } from "path";
26
29
  import { stat as stat4 } from "fs/promises";
30
+ import { execa as execa5 } from "execa";
27
31
  import { spawn } from "child_process";
28
32
  import { randomBytes as randomBytes2 } from "crypto";
29
33
  import { existsSync, openSync } from "fs";
@@ -31,7 +35,7 @@ import { mkdir as mkdir3, readFile as readFile2, unlink, writeFile as writeFile2
31
35
  import { request as httpRequest } from "http";
32
36
  import { homedir as homedir3 } from "os";
33
37
  import { dirname, join as join4, resolve as resolve2 } from "path";
34
- import { setTimeout as delay } from "timers/promises";
38
+ import { setTimeout as delay2 } from "timers/promises";
35
39
  import { fileURLToPath } from "url";
36
40
 
37
41
  // ../../packages/relay/dist/index.js
@@ -41,7 +45,7 @@ var RELAY_NETWORK_NAME = "agentbox-net";
41
45
  var RELAY_IMAGE_REF = "agentbox/relay:dev";
42
46
  var MAX_BODY_BYTES = 1024 * 1024;
43
47
 
44
- // ../../packages/sandbox-docker/dist/chunk-NY7PB7KY.js
48
+ // ../../packages/sandbox-docker/dist/chunk-SRQIM7LG.js
45
49
  function isHostPathHookCommand(command, hostHome) {
46
50
  if (typeof command !== "string" || command.length === 0) return false;
47
51
  if (hostHome.length === 0) return false;
@@ -78,6 +82,27 @@ function filterHostHooks(data, hostHome) {
78
82
  }
79
83
  return { data: clone, removedCommands };
80
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
+ }
81
106
  function addProjectAlias(data, fromPath, toPath) {
82
107
  const clone = structuredClone(data);
83
108
  if (clone === null || typeof clone !== "object" || Array.isArray(clone)) {
@@ -104,17 +129,17 @@ function addProjectAlias(data, fromPath, toPath) {
104
129
  }
105
130
  return { data: clone, aliased: true };
106
131
  }
107
- function clearInstallMethod(data) {
132
+ function setInstallMethodNative(data) {
108
133
  const clone = structuredClone(data);
109
134
  if (clone === null || typeof clone !== "object" || Array.isArray(clone)) {
110
- return { data: clone, cleared: false };
135
+ return { data: clone, applied: false };
111
136
  }
112
137
  const obj = clone;
113
- if (Object.prototype.hasOwnProperty.call(obj, "installMethod")) {
114
- delete obj.installMethod;
115
- return { data: clone, cleared: true };
116
- }
117
- 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 };
118
143
  }
119
144
  var SKILL_EXCLUDE_PREFIXES = ["agentbox-"];
120
145
  var CONTAINER_PLUGINS_PREFIX = "/home/vscode/.claude/plugins/";
@@ -188,11 +213,31 @@ function mergeInstalledPlugins(hostJson, boxJson, opts) {
188
213
  (host, merged) => ({ ...host, plugins: merged })
189
214
  );
190
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
+ }
191
234
  var SHARED_CLAUDE_VOLUME = "agentbox-claude-config";
192
235
  var DEFAULT_CLAUDE_SESSION = "claude";
193
236
  var CONTAINER_CLAUDE_DIR = "/home/vscode/.claude";
194
237
  var CONTAINER_USER = "vscode";
195
238
  var CONTAINER_WORKSPACE = "/workspace";
239
+ var IN_BOX_SETUP_GUIDE_PATH = "/usr/local/share/agentbox/setup-guide.md";
240
+ var SETUP_SKILL_DST = "/dst/skills/agentbox-setup/SKILL.md";
196
241
  function resolveClaudeVolume(opts) {
197
242
  if (opts.isolate) {
198
243
  return { volume: `${SHARED_CLAUDE_VOLUME}-${opts.boxId}` };
@@ -207,6 +252,14 @@ async function pathExists(p) {
207
252
  return false;
208
253
  }
209
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
+ }
210
263
  async function findBrokenSymlinks(root) {
211
264
  const broken = [];
212
265
  async function walk(dir) {
@@ -241,6 +294,7 @@ async function ensureClaudeVolume(spec, opts) {
241
294
  if (!await pathExists(hostClaude)) return { created, synced: false };
242
295
  const hostClaudeJson = join(homedir(), ".claude.json");
243
296
  const hasJson = await pathExists(hostClaudeJson);
297
+ const seedClaudeJson = !await volumeHasClaudeJson(spec.volume, opts.image);
244
298
  const hostHome = homedir();
245
299
  const hostAgents = join(homedir(), ".agents");
246
300
  const hasAgents = await pathExists(hostAgents);
@@ -258,12 +312,13 @@ async function ensureClaudeVolume(spec, opts) {
258
312
  "-v",
259
313
  `${hostClaude}:/src-claude:ro`
260
314
  ];
261
- if (hasJson) args.push("-v", `${hostClaudeJson}:/src-claude-json:ro`);
315
+ if (hasJson && seedClaudeJson) args.push("-v", `${hostClaudeJson}:/src-claude-json:ro`);
262
316
  if (hasAgents) args.push("-v", `${hostAgents}:/.agents:ro`);
263
317
  const filterDir = await mkdtemp(join(tmpdir(), "agentbox-claude-filter-"));
264
318
  let filteredHookCount = 0;
265
- let clearedInstallMethod = false;
319
+ let installMethodFixed = false;
266
320
  let aliasedProjectKey = false;
321
+ let workspaceTrusted = false;
267
322
  try {
268
323
  const settingsResult = await maybeFilterTo(
269
324
  join(hostClaude, "settings.json"),
@@ -271,21 +326,40 @@ async function ensureClaudeVolume(spec, opts) {
271
326
  hostHome
272
327
  );
273
328
  filteredHookCount += settingsResult.removedHooks;
274
- if (hasJson) {
329
+ if (!seedClaudeJson) {
330
+ } else if (hasJson) {
275
331
  const jsonResult = await maybeFilterTo(
276
332
  hostClaudeJson,
277
333
  join(filterDir, "_claude.json"),
278
334
  hostHome,
279
335
  {
280
- clearInstallMethod: true,
281
- 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
282
339
  }
283
340
  );
284
341
  filteredHookCount += jsonResult.removedHooks;
285
- clearedInstallMethod = jsonResult.clearedInstallMethod;
342
+ installMethodFixed = jsonResult.installMethodFixed;
286
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;
287
361
  }
288
- if (filteredHookCount > 0 || clearedInstallMethod || aliasedProjectKey) {
362
+ if (filteredHookCount > 0 || installMethodFixed || aliasedProjectKey || workspaceTrusted) {
289
363
  args.push("-v", `${filterDir}:/src-filter:ro`);
290
364
  }
291
365
  const brokenSymlinks = await findBrokenSymlinks(hostClaude);
@@ -332,22 +406,57 @@ async function ensureClaudeVolume(spec, opts) {
332
406
  } finally {
333
407
  await rm(filterDir, { recursive: true, force: true });
334
408
  }
335
- return { created, synced: true, filteredHookCount, clearedInstallMethod, aliasedProjectKey };
409
+ return {
410
+ created,
411
+ synced: true,
412
+ filteredHookCount,
413
+ installMethodFixed,
414
+ aliasedProjectKey,
415
+ workspaceTrusted
416
+ };
417
+ }
418
+ async function seedSetupSkillIntoVolume(volume, image) {
419
+ try {
420
+ const { stdout } = await execa("docker", [
421
+ "run",
422
+ "--rm",
423
+ "--user",
424
+ "0",
425
+ "-v",
426
+ `${volume}:/dst`,
427
+ image,
428
+ "sh",
429
+ "-c",
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`
434
+ ]);
435
+ return { seeded: stdout.includes("SEEDED") };
436
+ } catch {
437
+ return { seeded: false };
438
+ }
336
439
  }
337
440
  async function maybeFilterTo(src, dest, hostHome, opts = {}) {
441
+ const zero = {
442
+ removedHooks: 0,
443
+ installMethodFixed: false,
444
+ aliasedProjectKey: false,
445
+ workspaceTrusted: false
446
+ };
338
447
  let parsed;
339
448
  try {
340
449
  parsed = JSON.parse(await readFile(src, "utf8"));
341
450
  } catch {
342
- return { removedHooks: 0, clearedInstallMethod: false, aliasedProjectKey: false };
451
+ return zero;
343
452
  }
344
453
  const filtered = filterHostHooks(parsed, hostHome);
345
454
  let working = filtered.data;
346
- let cleared = false;
347
- if (opts.clearInstallMethod) {
348
- const r = clearInstallMethod(working);
455
+ let installFixed = false;
456
+ if (opts.setInstallMethodNative) {
457
+ const r = setInstallMethodNative(working);
349
458
  working = r.data;
350
- cleared = r.cleared;
459
+ installFixed = r.applied;
351
460
  }
352
461
  let aliased = false;
353
462
  if (opts.aliasProject) {
@@ -355,14 +464,21 @@ async function maybeFilterTo(src, dest, hostHome, opts = {}) {
355
464
  working = r.data;
356
465
  aliased = r.aliased;
357
466
  }
358
- if (filtered.removedCommands.length === 0 && !cleared && !aliased) {
359
- 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;
360
475
  }
361
476
  await writeFile(dest, JSON.stringify(working, null, 2));
362
477
  return {
363
478
  removedHooks: filtered.removedCommands.length,
364
- clearedInstallMethod: cleared,
365
- aliasedProjectKey: aliased
479
+ installMethodFixed: installFixed,
480
+ aliasedProjectKey: aliased,
481
+ workspaceTrusted: trusted
366
482
  };
367
483
  }
368
484
  var FORWARDED_ENV_KEYS = [
@@ -410,7 +526,18 @@ async function isDir(p) {
410
526
  return false;
411
527
  }
412
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
+ }
413
537
  async function scanPluginCacheForRebuild(cacheRoot) {
538
+ const referenced = await readReferencedPluginKeys(
539
+ join(cacheRoot, "..", "installed_plugins.json")
540
+ );
414
541
  let marketplaces;
415
542
  try {
416
543
  marketplaces = await readdir(cacheRoot, { withFileTypes: true });
@@ -437,6 +564,7 @@ async function scanPluginCacheForRebuild(cacheRoot) {
437
564
  }
438
565
  for (const v of versions) {
439
566
  if (!v.isDirectory()) continue;
567
+ if (referenced.size > 0 && !referenced.has(`${m.name}/${p.name}/${v.name}`)) continue;
440
568
  const vPath = join(pPath, v.name);
441
569
  if (!await isFile(join(vPath, "package.json"))) continue;
442
570
  if (await isFile(join(vPath, PLUGIN_INSTALLED_MARKER))) continue;
@@ -452,13 +580,38 @@ async function resolveClaudeCacheLiveOnHost(volume) {
452
580
  if (!await isDir(orbstackVolumePath(volume))) return null;
453
581
  return orbstackVolumePath(volume, "plugins", "cache");
454
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
+ }
455
603
  async function rebuildPluginNativeDeps(container, opts = {}) {
456
604
  if (opts.volume) {
457
605
  const cacheRoot = await resolveClaudeCacheLiveOnHost(opts.volume);
458
606
  if (cacheRoot && !await scanPluginCacheForRebuild(cacheRoot)) {
459
- return { rebuilt: [], failed: [], skipped: true };
607
+ return { rebuilt: [], failed: [], pruned: [], prunedBytes: 0, skipped: true };
460
608
  }
461
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
+ ` : "";
462
615
  const script = `set -u
463
616
  PLUGINS_DIR=/home/vscode/.claude/plugins/cache
464
617
  MARKER=${PLUGIN_INSTALLED_MARKER}
@@ -469,7 +622,12 @@ MAX=4
469
622
  [ -d "$PLUGINS_DIR" ] || exit 0
470
623
  mkdir -p "$NPM_CACHE"
471
624
  WORK=$(mktemp -d)
472
- 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
+ }
473
631
  # Run one plugin's install. $1 is frozen by value at call time, so it's safe
474
632
  # to read from the backgrounded subshell; the rest are set-once constants.
475
633
  do_one() {
@@ -489,10 +647,29 @@ do_one() {
489
647
  printf 'FAIL\\n' > "$WORK/$key.res"
490
648
  fi
491
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
492
667
  n=0
493
668
  for dir in "$PLUGINS_DIR"/*/*/*/; do
494
669
  [ -d "$dir" ] || continue
495
670
  [ -f "$dir/package.json" ] || continue
671
+ rel=\${dir%/}; rel=\${rel#$PLUGINS_DIR/}
672
+ is_referenced "$rel" || continue
496
673
  [ -f "$dir/$MARKER" ] && continue
497
674
  [ -n "$(find "$dir" -maxdepth 1 -name "$FAILMARKER" -mmin -$BACKOFF_MIN 2>/dev/null)" ] && continue
498
675
  echo "REBUILD_START \${dir#$PLUGINS_DIR/}"
@@ -529,6 +706,8 @@ rm -rf "$WORK"
529
706
  );
530
707
  const rebuilt = [];
531
708
  const failed = [];
709
+ const pruned = [];
710
+ let prunedBytes = 0;
532
711
  const lines = (result.stdout ?? "").split("\n");
533
712
  let collectingFail = null;
534
713
  for (const line of lines) {
@@ -547,9 +726,19 @@ rm -rf "$WORK"
547
726
  rebuilt.push(line.slice("REBUILD_OK ".length));
548
727
  } else if (line.startsWith("REBUILD_FAIL ")) {
549
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
+ }
550
739
  }
551
740
  }
552
- return { rebuilt, failed, skipped: false };
741
+ return { rebuilt, failed, pruned, prunedBytes, skipped: false };
553
742
  }
554
743
  var ClaudeSessionError = class extends Error {
555
744
  constructor(message) {
@@ -564,7 +753,6 @@ function shQuote(arg) {
564
753
  }
565
754
  async function startClaudeSession(opts) {
566
755
  const sessionName = opts.sessionName ?? DEFAULT_CLAUDE_SESSION;
567
- const boxName = opts.boxName ?? opts.container.replace(/^agentbox-/, "");
568
756
  const cmd = ["claude", ...opts.claudeArgs].map(shQuote).join(" ");
569
757
  const term = process.env["TERM"] ?? "xterm-256color";
570
758
  const envFlags = ["-e", `TERM=${term}`];
@@ -586,7 +774,7 @@ async function startClaudeSession(opts) {
586
774
  "-s",
587
775
  sessionName,
588
776
  cmd,
589
- ...buildClaudeStatusBarArgs(sessionName, boxName)
777
+ ...buildClaudeStatusBarArgs(sessionName)
590
778
  ],
591
779
  { reject: false }
592
780
  );
@@ -660,13 +848,15 @@ function buildClaudeDashboardAttachArgv(container, sessionName) {
660
848
  dash
661
849
  ];
662
850
  }
663
- function buildClaudeStatusBarArgs(sessionName, boxName) {
851
+ function buildClaudeStatusBarArgs(sessionName) {
664
852
  const s = sessionName;
665
- const name = boxName;
666
853
  return [
667
- // Server-global (no -t): remap prefix Ctrl-b -> Ctrl+a, bind `q` to detach
668
- // so Ctrl+a q matches the dashboard's quit chord. `send-prefix` makes a
669
- // double Ctrl+a reach Claude as a literal Ctrl+a.
854
+ // Server-global (no -t): primary prefix Ctrl+a (dashboard parity), keep
855
+ // tmux's default Ctrl+b as a secondary prefix so users with existing
856
+ // muscle memory / integrations aren't broken. `q` is the same key under
857
+ // both prefixes (single key table) -> Ctrl+a q AND Ctrl+b q both detach.
858
+ // `send-prefix` / `send-prefix -2` let a double-tap of either prefix
859
+ // reach Claude as that literal key.
670
860
  ";",
671
861
  "set",
672
862
  "-g",
@@ -676,9 +866,6 @@ function buildClaudeStatusBarArgs(sessionName, boxName) {
676
866
  "set",
677
867
  "-g",
678
868
  "prefix2",
679
- "None",
680
- ";",
681
- "unbind-key",
682
869
  "C-b",
683
870
  ";",
684
871
  "bind-key",
@@ -686,80 +873,88 @@ function buildClaudeStatusBarArgs(sessionName, boxName) {
686
873
  "send-prefix",
687
874
  ";",
688
875
  "bind-key",
876
+ "C-b",
877
+ "send-prefix",
878
+ "-2",
879
+ ";",
880
+ "bind-key",
689
881
  "q",
690
882
  "detach-client",
883
+ // Hide the inner tmux status bar — the wrapped-pty footer (for
884
+ // `agentbox claude` / `agentbox shell`) and the dashboard's own status
885
+ // row already show the box name + detach hint; without `status off`
886
+ // they double up.
691
887
  ";",
692
888
  "set",
693
889
  "-t",
694
890
  s,
695
- "status-interval",
696
- "60",
697
- ";",
698
- "set",
699
- "-t",
700
- s,
701
- "status-justify",
702
- "left",
703
- ";",
704
- "set",
705
- "-t",
706
- s,
707
- "status-style",
708
- "bg=colour236,fg=colour250",
709
- ";",
710
- "set",
711
- "-t",
712
- s,
713
- "status-left-length",
714
- "60",
715
- ";",
716
- "set",
717
- "-t",
718
- s,
719
- "status-left",
720
- `#[fg=colour16,bg=colour39,bold] agentbox \u25B8 ${name} #[default] `,
721
- ";",
722
- "set",
723
- "-t",
724
- s,
725
- "status-right-length",
726
- "30",
727
- ";",
728
- "set",
729
- "-t",
730
- s,
731
- "status-right",
732
- "#[fg=colour255]Control+a q#[fg=colour245]: detach ",
733
- ";",
734
- "set",
735
- "-t",
736
- s,
737
- "window-status-format",
738
- "",
739
- ";",
740
- "set",
741
- "-t",
742
- s,
743
- "window-status-current-format",
744
- ""
891
+ "status",
892
+ "off"
745
893
  ];
746
894
  }
747
895
  function buildShellArgv(container) {
748
896
  const term = process.env["TERM"] ?? "xterm-256color";
749
897
  return ["exec", "-it", "-e", `TERM=${term}`, "--user", CONTAINER_USER, container, "bash", "-l"];
750
898
  }
751
- function formatDetachNotice(ref) {
752
- return `Session detached. Reattach with: agentbox claude attach ${ref}`;
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
+ ];
753
919
  }
754
- function attachClaudeSession(container, sessionName, reattachRef) {
755
- const child = spawnSync("docker", buildClaudeAttachArgv(container, sessionName), {
756
- stdio: "inherit"
757
- });
758
- const code = child.status ?? 0;
759
- if (reattachRef && code === 0) {
760
- process.stdout.write("\x1B[1A\x1B[2K\r" + formatDetachNotice(reattachRef) + "\n");
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);
761
953
  }
762
- process.exit(code);
954
+ return { warmed: false, attempts: MAX_ATTEMPTS };
955
+ }
956
+ function formatDetachNotice(ref) {
957
+ return `Session detached. Reattach with: agentbox claude attach ${ref}`;
763
958
  }
764
959
  async function claudeSessionInfo(container, sessionName) {
765
960
  const name = sessionName ?? DEFAULT_CLAUDE_SESSION;
@@ -1053,211 +1248,281 @@ async function isGitDir(path) {
1053
1248
  return false;
1054
1249
  }
1055
1250
  }
1056
- async function createBoxWorktree(args) {
1057
- const log = args.onLog ?? (() => {
1058
- });
1059
- const stash = await execa2("git", ["-C", args.hostMainRepo, "stash", "create"], {
1060
- reject: false
1061
- });
1062
- const stashSha = stash.exitCode === 0 ? stash.stdout.trim() || null : null;
1063
- const untracked = await execa2(
1064
- "git",
1065
- ["-C", args.hostMainRepo, "ls-files", "--others", "--exclude-standard", "-z"],
1066
- { reject: false }
1067
- );
1068
- const untrackedList = untracked.exitCode === 0 && untracked.stdout.length > 0 ? untracked.stdout.split("\0").filter((s) => s.length > 0) : [];
1069
- const branchName = await pickFreshBranch(args.hostMainRepo, args.branchName);
1070
- const wadd = await execa2(
1251
+ async function pickFreshBranch(hostMainRepo, base) {
1252
+ let candidate = base;
1253
+ let suffix = 2;
1254
+ while (await branchExists(hostMainRepo, candidate)) {
1255
+ candidate = `${base}-${String(suffix++)}`;
1256
+ if (suffix > 100) throw new GitWorktreeError(`could not find a free branch name near ${base}`);
1257
+ }
1258
+ return candidate;
1259
+ }
1260
+ async function branchExists(hostMainRepo, name) {
1261
+ const result = await execa2(
1071
1262
  "git",
1072
- ["-C", args.hostMainRepo, "worktree", "add", "-b", branchName, args.worktreeDir, "HEAD"],
1263
+ ["-C", hostMainRepo, "show-ref", "--verify", "--quiet", `refs/heads/${name}`],
1073
1264
  { reject: false }
1074
1265
  );
1075
- if (wadd.exitCode !== 0) {
1076
- throw new GitWorktreeError(
1077
- `git worktree add failed for ${args.hostMainRepo}: ${wadd.stderr || wadd.stdout}`
1078
- );
1266
+ return result.exitCode === 0;
1267
+ }
1268
+ var GitWorktreeError = class extends Error {
1269
+ constructor(message) {
1270
+ super(message);
1271
+ this.name = "GitWorktreeError";
1079
1272
  }
1080
- log(`created worktree ${args.worktreeDir} on branch ${branchName}`);
1081
- await execa2(
1273
+ };
1274
+ var WORKTREE_ROOT = "/home/vscode/.agentbox-worktrees";
1275
+ function fsSafeBranch(branch) {
1276
+ return branch.replace(/[^A-Za-z0-9._-]+/g, "_");
1277
+ }
1278
+ function gitWorktreePathFor(branch) {
1279
+ return `${WORKTREE_ROOT}/${fsSafeBranch(branch)}`;
1280
+ }
1281
+ async function collectRepoCarryOver(repo, branch, containerPath, gitWorktreePath) {
1282
+ const stash = await execa3("git", ["-C", repo.hostMainRepo, "stash", "create"], { reject: false });
1283
+ const stashSha = stash.exitCode === 0 ? stash.stdout.trim() || null : null;
1284
+ const untracked = await execa3(
1082
1285
  "git",
1083
- ["-C", args.hostMainRepo, "config", "extensions.worktreeConfig", "true"],
1286
+ ["-C", repo.hostMainRepo, "ls-files", "--others", "--exclude-standard", "-z"],
1084
1287
  { reject: false }
1085
1288
  );
1086
- await execa2(
1087
- "git",
1088
- ["-C", args.worktreeDir, "config", "--worktree", "commit.gpgsign", "false"],
1289
+ const untrackedNul = untracked.exitCode === 0 ? untracked.stdout : "";
1290
+ return {
1291
+ repo,
1292
+ containerPath,
1293
+ gitWorktreePath,
1294
+ branch,
1295
+ stashSha,
1296
+ untrackedNul,
1297
+ hostSource: repo.hostMainRepo
1298
+ };
1299
+ }
1300
+ async function dexec(container, argv, user = "vscode", cwd = "/") {
1301
+ const r = await execa3(
1302
+ "docker",
1303
+ ["exec", "-w", cwd, "--user", user, container, ...argv],
1089
1304
  { reject: false }
1090
1305
  );
1091
- if (stashSha) {
1092
- const withIndex = await execa2(
1093
- "git",
1094
- ["-C", args.worktreeDir, "stash", "apply", "--index", stashSha],
1306
+ if (r.exitCode !== 0) {
1307
+ throw new GitWorktreeError(`${argv.join(" ")} failed: ${r.stderr || r.stdout}`);
1308
+ }
1309
+ }
1310
+ async function chownGitBindParents(args) {
1311
+ const log = args.onLog ?? (() => {
1312
+ });
1313
+ const repos = Array.from(new Set(args.hostMainRepos));
1314
+ for (const repo of repos) {
1315
+ const result = await execInBox(args.container, ["chown", "vscode:vscode", repo], {
1316
+ user: "root"
1317
+ });
1318
+ if (result.exitCode === 0) {
1319
+ log(`chowned ${repo} to vscode:vscode (parent of bind-mounted .git)`);
1320
+ } else {
1321
+ const msg = (result.stderr || result.stdout || `exit ${result.exitCode}`).trim();
1322
+ log(`chown ${repo} failed (best-effort, ignoring): ${msg}`);
1323
+ }
1324
+ }
1325
+ }
1326
+ async function bindWorktrees(container, binds, onLog) {
1327
+ const log = onLog ?? (() => {
1328
+ });
1329
+ const ordered = [...binds].sort(
1330
+ (a, b) => a.kind === "root" && b.kind !== "root" ? -1 : a.kind !== "root" && b.kind === "root" ? 1 : 0
1331
+ );
1332
+ for (const b of ordered) {
1333
+ await execa3(
1334
+ "docker",
1335
+ ["exec", "-w", "/", "--user", "root", container, "sh", "-c", `mountpoint -q ${b.containerPath} && umount ${b.containerPath} || true`],
1336
+ { reject: false }
1337
+ );
1338
+ if (b.kind === "nested") {
1339
+ await dexec(container, ["mkdir", "-p", ctParent(b.containerPath)], "root");
1340
+ await dexec(container, ["mkdir", "-p", b.containerPath], "root");
1341
+ }
1342
+ await dexec(container, ["mount", "--bind", b.gitWorktreePath, b.containerPath], "root");
1343
+ log(`bind-mounted ${b.containerPath} <- ${b.gitWorktreePath}`);
1344
+ }
1345
+ }
1346
+ async function seedWorkspace(opts) {
1347
+ const log = opts.onLog ?? (() => {
1348
+ });
1349
+ await dexec(opts.container, ["mkdir", "-p", WORKTREE_ROOT]);
1350
+ for (const r of opts.repos) {
1351
+ const main = r.repo.hostMainRepo;
1352
+ const wt = r.gitWorktreePath;
1353
+ const add = await execa3(
1354
+ "docker",
1355
+ [
1356
+ "exec",
1357
+ "--user",
1358
+ "vscode",
1359
+ opts.container,
1360
+ "git",
1361
+ "-C",
1362
+ main,
1363
+ "worktree",
1364
+ "add",
1365
+ "-b",
1366
+ r.branch,
1367
+ wt,
1368
+ "HEAD"
1369
+ ],
1370
+ { reject: false }
1371
+ );
1372
+ if (add.exitCode !== 0) {
1373
+ throw new GitWorktreeError(
1374
+ `git worktree add ${wt} (branch ${r.branch}) failed: ${add.stderr || add.stdout}`
1375
+ );
1376
+ }
1377
+ log(`worktree ${wt} on branch ${r.branch} (host main ${main})`);
1378
+ await execa3(
1379
+ "docker",
1380
+ [
1381
+ "exec",
1382
+ "--user",
1383
+ "vscode",
1384
+ opts.container,
1385
+ "git",
1386
+ "-C",
1387
+ main,
1388
+ "config",
1389
+ "extensions.worktreeConfig",
1390
+ "true"
1391
+ ],
1095
1392
  { reject: false }
1096
1393
  );
1097
- if (withIndex.exitCode !== 0) {
1098
- const noIndex = await execa2(
1394
+ await execa3(
1395
+ "docker",
1396
+ [
1397
+ "exec",
1398
+ "--user",
1399
+ "vscode",
1400
+ opts.container,
1099
1401
  "git",
1100
- ["-C", args.worktreeDir, "stash", "apply", stashSha],
1402
+ "-C",
1403
+ wt,
1404
+ "config",
1405
+ "--worktree",
1406
+ "commit.gpgsign",
1407
+ "false"
1408
+ ],
1409
+ { reject: false }
1410
+ );
1411
+ }
1412
+ await bindWorktrees(
1413
+ opts.container,
1414
+ opts.repos.map((r) => ({
1415
+ kind: r.repo.kind,
1416
+ containerPath: r.containerPath,
1417
+ gitWorktreePath: r.gitWorktreePath
1418
+ })),
1419
+ log
1420
+ );
1421
+ for (const r of opts.repos) {
1422
+ const ct = r.containerPath;
1423
+ if (r.stashSha) {
1424
+ const withIndex = await execa3(
1425
+ "docker",
1426
+ [
1427
+ "exec",
1428
+ "--user",
1429
+ "vscode",
1430
+ opts.container,
1431
+ "git",
1432
+ "-C",
1433
+ ct,
1434
+ "stash",
1435
+ "apply",
1436
+ "--index",
1437
+ r.stashSha
1438
+ ],
1101
1439
  { reject: false }
1102
1440
  );
1103
- if (noIndex.exitCode !== 0) {
1104
- log(
1105
- `warning: stash apply failed in worktree (${withIndex.stderr || withIndex.stdout || "no message"})`
1441
+ if (withIndex.exitCode !== 0) {
1442
+ const noIndex = await execa3(
1443
+ "docker",
1444
+ [
1445
+ "exec",
1446
+ "--user",
1447
+ "vscode",
1448
+ opts.container,
1449
+ "git",
1450
+ "-C",
1451
+ ct,
1452
+ "stash",
1453
+ "apply",
1454
+ r.stashSha
1455
+ ],
1456
+ { reject: false }
1106
1457
  );
1458
+ if (noIndex.exitCode !== 0) {
1459
+ log(
1460
+ `warning: stash apply failed in ${ct} (${withIndex.stderr || withIndex.stdout || "no message"})`
1461
+ );
1462
+ } else {
1463
+ log(`applied tracked changes (without index \u2014 staged state lost) in ${ct}`);
1464
+ }
1107
1465
  } else {
1108
- log(`applied tracked changes (without index \u2014 staged state lost)`);
1466
+ log(`applied tracked changes from host main into ${ct}`);
1109
1467
  }
1110
- } else {
1111
- log(`applied tracked changes from host main`);
1112
1468
  }
1113
- }
1114
- if (untrackedList.length > 0) {
1115
- const tarOut = await execa2(
1116
- "tar",
1117
- ["-C", args.hostMainRepo, "--null", "-T", "-", "-cf", "-"],
1118
- {
1119
- input: untrackedList.join("\0"),
1469
+ if (r.untrackedNul.length > 0) {
1470
+ const tarOut = await execa3("tar", ["-C", r.hostSource, "--null", "-T", "-", "-cf", "-"], {
1471
+ input: r.untrackedNul.replace(/\0$/, ""),
1120
1472
  encoding: "buffer",
1121
1473
  reject: false
1122
- }
1123
- );
1124
- if (tarOut.exitCode === 0) {
1125
- const tarIn = await execa2("tar", ["-C", args.worktreeDir, "-xf", "-"], {
1126
- input: tarOut.stdout,
1127
- reject: false
1128
1474
  });
1475
+ if (tarOut.exitCode !== 0) {
1476
+ log(`warning: tar of untracked files for ${r.repo.hostMainRepo} failed: ${tarOut.stderr}`);
1477
+ continue;
1478
+ }
1479
+ const tarIn = await execa3(
1480
+ "docker",
1481
+ ["exec", "-i", "--user", "vscode", opts.container, "tar", "-C", ct, "-xf", "-"],
1482
+ { input: tarOut.stdout, reject: false }
1483
+ );
1129
1484
  if (tarIn.exitCode !== 0) {
1130
- log(`warning: untracked-file copy into worktree failed: ${tarIn.stderr}`);
1485
+ log(`warning: untracked-file copy into ${ct} failed: ${tarIn.stderr}`);
1131
1486
  } else {
1132
- log(`copied ${String(untrackedList.length)} untracked file(s) into worktree`);
1487
+ const count = r.untrackedNul.split("\0").filter((s) => s.length > 0).length;
1488
+ log(`copied ${String(count)} untracked file(s) into ${ct}`);
1133
1489
  }
1134
- } else {
1135
- log(`warning: tar of untracked files failed: ${tarOut.stderr}`);
1136
1490
  }
1137
1491
  }
1138
- return { branchName, stashSha, untrackedCount: untrackedList.length };
1139
1492
  }
1140
- async function pickFreshBranch(hostMainRepo, base) {
1141
- let candidate = base;
1142
- let suffix = 2;
1143
- while (await branchExists(hostMainRepo, candidate)) {
1144
- candidate = `${base}-${String(suffix++)}`;
1145
- if (suffix > 100) throw new GitWorktreeError(`could not find a free branch name near ${base}`);
1493
+ async function seedWorkspaceFromDir(opts) {
1494
+ const log = opts.onLog ?? (() => {
1495
+ });
1496
+ const tarOut = await execa3("tar", ["-C", opts.hostSource, "-cf", "-", "."], {
1497
+ encoding: "buffer",
1498
+ reject: false
1499
+ });
1500
+ if (tarOut.exitCode !== 0) {
1501
+ throw new GitWorktreeError(`tar of ${opts.hostSource} failed: ${tarOut.stderr}`);
1146
1502
  }
1147
- return candidate;
1148
- }
1149
- async function branchExists(hostMainRepo, name) {
1150
- const result = await execa2(
1151
- "git",
1152
- ["-C", hostMainRepo, "show-ref", "--verify", "--quiet", `refs/heads/${name}`],
1153
- { reject: false }
1503
+ const tarIn = await execa3(
1504
+ "docker",
1505
+ ["exec", "-i", "--user", "1000:1000", opts.container, "tar", "-C", "/workspace", "-xf", "-"],
1506
+ { input: tarOut.stdout, reject: false }
1154
1507
  );
1155
- return result.exitCode === 0;
1508
+ if (tarIn.exitCode !== 0) {
1509
+ throw new GitWorktreeError(`tar extract into /workspace failed: ${tarIn.stderr}`);
1510
+ }
1511
+ log(`seeded /workspace from ${opts.hostSource}`);
1156
1512
  }
1157
- async function removeBoxWorktree(args) {
1158
- const remove = await execa2(
1513
+ async function removeInBoxWorktree(args) {
1514
+ const remove = await execa3(
1159
1515
  "git",
1160
- ["-C", args.hostMainRepo, "worktree", "remove", "--force", args.worktreeDir],
1516
+ ["-C", args.hostMainRepo, "worktree", "remove", "--force", args.gitWorktreePath],
1161
1517
  { reject: false }
1162
1518
  );
1163
1519
  if (remove.exitCode === 0) return;
1164
- await execa2("rm", ["-rf", args.worktreeDir], { reject: false });
1165
- await execa2("git", ["-C", args.hostMainRepo, "worktree", "prune"], { reject: false });
1520
+ await execa3("git", ["-C", args.hostMainRepo, "worktree", "prune"], { reject: false });
1166
1521
  }
1167
- var GitWorktreeError = class extends Error {
1168
- constructor(message) {
1169
- super(message);
1170
- this.name = "GitWorktreeError";
1171
- }
1172
- };
1173
- var DEFAULT_LOWER_DIRS = ["/host-src"];
1174
- var BOX_USER_UID = 1e3;
1175
- var BOX_USER_GID = 1e3;
1176
- async function mountOverlay(container, opts = {}) {
1177
- const lowerDirs = opts.lowerDirs && opts.lowerDirs.length > 0 ? opts.lowerDirs : ["/host-src"];
1178
- const mountOpts = [
1179
- `lowerdir=${lowerDirs.join(":")}`,
1180
- "upperdir=/upper/upper",
1181
- "workdir=/upper/work",
1182
- `squash_to_uid=${String(BOX_USER_UID)}`,
1183
- `squash_to_gid=${String(BOX_USER_GID)}`
1184
- ].join(",");
1185
- const lines = [
1186
- "set -euo pipefail",
1187
- "mkdir -p /upper/upper /upper/work /workspace",
1188
- // Idempotent — if a previous attempt left a stale overlay, unmount first.
1189
- "mountpoint -q /workspace && fusermount3 -u /workspace || true",
1190
- `fuse-overlayfs -o ${mountOpts} /workspace`,
1191
- "mountpoint -q /workspace"
1192
- ];
1193
- for (const w of opts.nestedWorktrees ?? []) {
1194
- lines.push(
1195
- `mkdir -p ${shellQuote(w.containerPath)}`,
1196
- `mountpoint -q ${shellQuote(w.containerPath)} && umount ${shellQuote(w.containerPath)} || true`,
1197
- `mount --bind ${shellQuote(w.mountFromPath)} ${shellQuote(w.containerPath)}`,
1198
- `mountpoint -q ${shellQuote(w.containerPath)}`
1199
- );
1200
- }
1201
- const result = await execInBox(container, ["bash", "-lc", lines.join("\n")], { user: "root" });
1202
- if (result.exitCode !== 0) {
1203
- throw new OverlayError(
1204
- `failed to mount FUSE overlay in ${container}`,
1205
- result.stdout,
1206
- result.stderr
1207
- );
1208
- }
1209
- return { upperWritePath: "/upper/upper" };
1210
- }
1211
- function shellQuote(s) {
1212
- return `'${s.replace(/'/g, `'\\''`)}'`;
1522
+ function ctParent(p) {
1523
+ const i = p.lastIndexOf("/");
1524
+ return i <= 0 ? "/" : p.slice(0, i);
1213
1525
  }
1214
- async function verifyOverlay(container, lowerDirs = DEFAULT_LOWER_DIRS) {
1215
- const sentinel = ".agentbox-overlay-check";
1216
- const checks = [];
1217
- const ls = await execInBox(container, ["bash", "-lc", `ls -A /workspace | head -1`], {
1218
- user: "root"
1219
- });
1220
- checks.push({
1221
- name: "workspace lists lower contents",
1222
- ok: ls.exitCode === 0,
1223
- detail: ls.exitCode === 0 ? `first entry: ${ls.stdout.trim() || "(empty)"}` : ls.stderr.trim()
1224
- });
1225
- const write = await execInBox(container, ["bash", "-lc", `touch /workspace/${sentinel}`], {
1226
- user: "root"
1227
- });
1228
- checks.push({
1229
- name: "write through overlay succeeds",
1230
- ok: write.exitCode === 0,
1231
- detail: write.exitCode === 0 ? `created /workspace/${sentinel}` : write.stderr.trim()
1232
- });
1233
- const upper = await execInBox(container, ["bash", "-lc", `test -f /upper/upper/${sentinel}`], {
1234
- user: "root"
1235
- });
1236
- checks.push({
1237
- name: "write lands in /upper (cow target)",
1238
- ok: upper.exitCode === 0,
1239
- detail: upper.exitCode === 0 ? `/upper/upper/${sentinel} exists` : `expected /upper/upper/${sentinel} to exist`
1240
- });
1241
- const lowerProbe = lowerDirs.map((d) => `test ! -e ${shellQuote(d)}/${sentinel}`).join(" && ");
1242
- const lower = await execInBox(container, ["bash", "-lc", lowerProbe], { user: "root" });
1243
- checks.push({
1244
- name: `lower untouched (${lowerDirs.join(", ")})`,
1245
- ok: lower.exitCode === 0,
1246
- detail: lower.exitCode === 0 ? `${sentinel} absent from every lower` : `${sentinel} leaked into a lower layer`
1247
- });
1248
- await execInBox(container, ["bash", "-lc", `rm -f /workspace/${sentinel}`], { user: "root" });
1249
- return checks;
1250
- }
1251
- var OverlayError = class extends Error {
1252
- constructor(message, stdout, stderr) {
1253
- super(message);
1254
- this.stdout = stdout;
1255
- this.stderr = stderr;
1256
- this.name = "OverlayError";
1257
- }
1258
- stdout;
1259
- stderr;
1260
- };
1261
1526
  var EXCLUDE_DIRS = /* @__PURE__ */ new Set([
1262
1527
  "node_modules",
1263
1528
  ".next",
@@ -1274,8 +1539,11 @@ var EXCLUDE_DIRS = /* @__PURE__ */ new Set([
1274
1539
  ".parcel-cache"
1275
1540
  ]);
1276
1541
  var SNAPSHOTS_ROOT = join3(homedir2(), ".agentbox", "snapshots");
1277
- function snapshotPathFor(boxId) {
1278
- return join3(SNAPSHOTS_ROOT, boxId);
1542
+ function snapshotPathFor(box) {
1543
+ const mnemonic = sanitizeMnemonic(box.name);
1544
+ const n = box.projectIndex;
1545
+ const segment = typeof n === "number" && Number.isFinite(n) && n > 0 ? `${box.id}-${String(n)}-${mnemonic}` : `${box.id}-${mnemonic}`;
1546
+ return join3(SNAPSHOTS_ROOT, segment);
1279
1547
  }
1280
1548
  async function findExcludedDirs(root, excluded = EXCLUDE_DIRS) {
1281
1549
  const matches = [];
@@ -1305,13 +1573,15 @@ async function createSnapshot(opts) {
1305
1573
  const excluded = opts.excluded ?? EXCLUDE_DIRS;
1306
1574
  await mkdir2(SNAPSHOTS_ROOT, { recursive: true });
1307
1575
  const cpArgs = platform() === "darwin" ? ["-cR"] : ["-R"];
1308
- await execa3("cp", [...cpArgs, `${source}/`, destination]);
1576
+ await execa4("cp", [...cpArgs, `${source}/`, destination]);
1309
1577
  const toPrune = await findExcludedDirs(destination, excluded);
1310
1578
  await Promise.all(toPrune.map((p) => rm2(p, { recursive: true, force: true })));
1311
1579
  return { destination, prunedPaths: toPrune };
1312
1580
  }
1581
+ var CTL_DAEMON_LOG = "/var/log/agentbox/ctl-daemon.log";
1313
1582
  async function launchCtlDaemon(container, hostSocketPath, timeoutMs = 3e3) {
1314
- 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], {
1315
1585
  user: "vscode",
1316
1586
  detach: true
1317
1587
  });
@@ -1336,6 +1606,23 @@ async function pathExists2(p) {
1336
1606
  return false;
1337
1607
  }
1338
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
+ }
1339
1626
  var STATE_DIR = join4(homedir3(), ".agentbox");
1340
1627
  var PID_FILE = join4(STATE_DIR, "relay.pid");
1341
1628
  var LOG_FILE = join4(STATE_DIR, "relay.log");
@@ -1364,7 +1651,7 @@ async function ensureRelay(opts = {}) {
1364
1651
  if (existingPid !== null && await processAlive(existingPid)) {
1365
1652
  for (let i = 0; i < 10; i++) {
1366
1653
  if (await pingHealthz(300)) return ENDPOINT;
1367
- await delay(200);
1654
+ await delay2(200);
1368
1655
  }
1369
1656
  log(`relay pid ${String(existingPid)} alive but /healthz unresponsive \u2014 proceeding anyway`);
1370
1657
  return ENDPOINT;
@@ -1398,7 +1685,7 @@ async function ensureRelay(opts = {}) {
1398
1685
  log(`relay reachable on ${ENDPOINT.hostUrl}`);
1399
1686
  return ENDPOINT;
1400
1687
  }
1401
- await delay(200);
1688
+ await delay2(200);
1402
1689
  }
1403
1690
  throw new Error(
1404
1691
  `relay did not become reachable on ${ENDPOINT.hostUrl} within 5s; see ${LOG_FILE}`
@@ -1454,7 +1741,7 @@ async function stopRelay() {
1454
1741
  }
1455
1742
  for (let i = 0; i < 20; i++) {
1456
1743
  if (!await processAlive(pid)) break;
1457
- await delay(100);
1744
+ await delay2(100);
1458
1745
  }
1459
1746
  if (await processAlive(pid)) {
1460
1747
  try {
@@ -1466,6 +1753,21 @@ async function stopRelay() {
1466
1753
  });
1467
1754
  return { stopped: true, pid };
1468
1755
  }
1756
+ async function getRelayStatus() {
1757
+ const pid = await readPidFile();
1758
+ const pidAlive = pid !== null && await processAlive(pid);
1759
+ const health = await fetchHealthz(300);
1760
+ return {
1761
+ running: health !== null,
1762
+ pid,
1763
+ pidAlive,
1764
+ port: PORT,
1765
+ endpoint: ENDPOINT,
1766
+ health: health === null ? null : { boxes: health.boxes, events: health.events },
1767
+ pidFile: PID_FILE,
1768
+ logFile: LOG_FILE
1769
+ };
1770
+ }
1469
1771
  function pingHealthz(timeoutMs) {
1470
1772
  return new Promise((resolveP) => {
1471
1773
  const req = httpRequest(
@@ -1484,6 +1786,42 @@ function pingHealthz(timeoutMs) {
1484
1786
  req.end();
1485
1787
  });
1486
1788
  }
1789
+ function fetchHealthz(timeoutMs) {
1790
+ return new Promise((resolveP) => {
1791
+ const req = httpRequest(
1792
+ { host: "127.0.0.1", port: PORT, method: "GET", path: "/healthz", timeout: timeoutMs },
1793
+ (res) => {
1794
+ const status = res.statusCode ?? 0;
1795
+ if (status < 200 || status >= 300) {
1796
+ res.resume();
1797
+ resolveP(null);
1798
+ return;
1799
+ }
1800
+ const chunks = [];
1801
+ res.on("data", (c) => chunks.push(c));
1802
+ res.on("end", () => {
1803
+ try {
1804
+ const parsed = JSON.parse(Buffer.concat(chunks).toString("utf8"));
1805
+ if (typeof parsed.ok === "boolean" && typeof parsed.boxes === "number" && typeof parsed.events === "number") {
1806
+ resolveP({ ok: parsed.ok, boxes: parsed.boxes, events: parsed.events });
1807
+ } else {
1808
+ resolveP(null);
1809
+ }
1810
+ } catch {
1811
+ resolveP(null);
1812
+ }
1813
+ });
1814
+ res.on("error", () => resolveP(null));
1815
+ }
1816
+ );
1817
+ req.on("error", () => resolveP(null));
1818
+ req.on("timeout", () => {
1819
+ req.destroy();
1820
+ resolveP(null);
1821
+ });
1822
+ req.end();
1823
+ });
1824
+ }
1487
1825
  async function readPidFile() {
1488
1826
  try {
1489
1827
  const text = await readFile2(PID_FILE, "utf8");
@@ -1507,7 +1845,7 @@ function generateRelayToken() {
1507
1845
  async function registerBoxWithRelay(args) {
1508
1846
  const worktrees = (args.worktrees ?? []).map((w) => ({
1509
1847
  containerPath: w.containerPath,
1510
- hostWorktreeDir: w.hostWorktreeDir,
1848
+ hostMainRepo: w.hostMainRepo,
1511
1849
  branch: w.branch
1512
1850
  }));
1513
1851
  await adminPost("/admin/register-box", {
@@ -1516,6 +1854,7 @@ async function registerBoxWithRelay(args) {
1516
1854
  name: args.name,
1517
1855
  containerName: args.containerName,
1518
1856
  createdAt: args.createdAt,
1857
+ projectIndex: args.projectIndex,
1519
1858
  worktrees
1520
1859
  });
1521
1860
  }
@@ -1525,6 +1864,26 @@ async function forgetBoxFromRelay(boxId) {
1525
1864
  } catch {
1526
1865
  }
1527
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
+ }
1528
1887
  async function adminPost(path, body) {
1529
1888
  const json = JSON.stringify(body);
1530
1889
  await new Promise((resolveP, rejectP) => {
@@ -1563,6 +1922,49 @@ async function adminPost(path, body) {
1563
1922
  req.end();
1564
1923
  });
1565
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
+ }
1566
1968
  async function rehydrateRelayRegistry(boxes) {
1567
1969
  for (const b of boxes) {
1568
1970
  if (!b.relayToken) continue;
@@ -1573,6 +1975,7 @@ async function rehydrateRelayRegistry(boxes) {
1573
1975
  name: b.name,
1574
1976
  containerName: b.container,
1575
1977
  createdAt: b.createdAt,
1978
+ projectIndex: b.projectIndex,
1576
1979
  worktrees: b.gitWorktrees
1577
1980
  });
1578
1981
  } catch {
@@ -1682,10 +2085,10 @@ async function ensureAgentboxTasksFile(container, services, opts = {}) {
1682
2085
  return { status: "wrote" };
1683
2086
  }
1684
2087
  async function writeFileInBox(container, path, content) {
1685
- const { execa: execa4 } = await import("execa");
1686
- const result = await execa4(
2088
+ const { execa: execa6 } = await import("execa");
2089
+ const result = await execa6(
1687
2090
  "docker",
1688
- ["exec", "-i", "--user", "vscode", container, "sh", "-c", `cat > ${shellQuote2(path)}`],
2091
+ ["exec", "-i", "--user", "vscode", container, "sh", "-c", `cat > ${shellQuote(path)}`],
1689
2092
  { input: content, reject: false }
1690
2093
  );
1691
2094
  return {
@@ -1694,7 +2097,7 @@ async function writeFileInBox(container, path, content) {
1694
2097
  stderr: result.stderr ?? ""
1695
2098
  };
1696
2099
  }
1697
- function shellQuote2(s) {
2100
+ function shellQuote(s) {
1698
2101
  return `'${s.replace(/'/g, `'\\''`)}'`;
1699
2102
  }
1700
2103
 
@@ -2181,19 +2584,25 @@ async function loadConfig(path) {
2181
2584
  }
2182
2585
 
2183
2586
  export {
2587
+ DEFAULT_RELAY_PORT,
2184
2588
  RELAY_CONTAINER_NAME,
2185
2589
  RELAY_NETWORK_NAME,
2186
2590
  RELAY_IMAGE_REF,
2187
2591
  SHARED_CLAUDE_VOLUME,
2188
2592
  resolveClaudeVolume,
2189
2593
  ensureClaudeVolume,
2594
+ seedSetupSkillIntoVolume,
2190
2595
  buildClaudeMounts,
2191
2596
  rebuildPluginNativeDeps,
2192
2597
  ClaudeSessionError,
2193
2598
  startClaudeSession,
2599
+ buildClaudeAttachArgv,
2194
2600
  buildClaudeDashboardAttachArgv,
2195
2601
  buildShellArgv,
2196
- attachClaudeSession,
2602
+ buildClaudeLoginRunArgv,
2603
+ runInteractiveClaudeLogin,
2604
+ warmUpClaudeCredentials,
2605
+ formatDetachNotice,
2197
2606
  claudeSessionInfo,
2198
2607
  pullClaudeExtras,
2199
2608
  SHARED_DOCKER_CACHE_VOLUME,
@@ -2205,20 +2614,27 @@ export {
2205
2614
  buildVncUrls,
2206
2615
  WEB_CONTAINER_PORT,
2207
2616
  detectGitRepos,
2208
- createBoxWorktree,
2209
- removeBoxWorktree,
2210
- DEFAULT_LOWER_DIRS,
2211
- mountOverlay,
2212
- verifyOverlay,
2617
+ pickFreshBranch,
2618
+ gitWorktreePathFor,
2619
+ collectRepoCarryOver,
2620
+ chownGitBindParents,
2621
+ bindWorktrees,
2622
+ seedWorkspace,
2623
+ seedWorkspaceFromDir,
2624
+ removeInBoxWorktree,
2213
2625
  SNAPSHOTS_ROOT,
2214
2626
  snapshotPathFor,
2215
2627
  createSnapshot,
2216
2628
  launchCtlDaemon,
2629
+ ensureHomeOwnedByVscode,
2217
2630
  ensureRelay,
2218
2631
  stopRelay,
2632
+ getRelayStatus,
2219
2633
  generateRelayToken,
2220
2634
  registerBoxWithRelay,
2221
2635
  forgetBoxFromRelay,
2636
+ setRelayNotice,
2637
+ clearRelayNotice,
2222
2638
  rehydrateRelayRegistry,
2223
2639
  ideProfile,
2224
2640
  SHARED_VSCODE_EXTENSIONS_VOLUME,
@@ -2236,4 +2652,4 @@ export {
2236
2652
  ConfigError,
2237
2653
  loadConfig
2238
2654
  };
2239
- //# sourceMappingURL=chunk-NSIECUCS.js.map
2655
+ //# sourceMappingURL=chunk-HTTKML3C.js.map