@madarco/agentbox 0.4.1 → 0.5.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 (38) hide show
  1. package/dist/{chunk-WR5FFGE5.js → chunk-6VTAPD4H.js} +123 -112
  2. package/dist/chunk-6VTAPD4H.js.map +1 -0
  3. package/dist/{chunk-J35IH7W5.js → chunk-7J5AJLWG.js} +61 -23
  4. package/dist/chunk-7J5AJLWG.js.map +1 -0
  5. package/dist/{chunk-FQD6ZWYW.js → chunk-FJNIFTWK.js} +66 -65
  6. package/dist/chunk-FJNIFTWK.js.map +1 -0
  7. package/dist/{chunk-IDR4HVIC.js → chunk-HPZMD5DE.js} +2 -2
  8. package/dist/chunk-HPZMD5DE.js.map +1 -0
  9. package/dist/{chunk-NSIECUCS.js → chunk-PXUBE5KS.js} +365 -258
  10. package/dist/chunk-PXUBE5KS.js.map +1 -0
  11. package/dist/{chunk-SOMIKEN2.js → chunk-RFC5F5HR.js} +272 -214
  12. package/dist/chunk-RFC5F5HR.js.map +1 -0
  13. package/dist/create-AHZ3GVEZ-TGEDL7UX.js +15 -0
  14. package/dist/index.js +2757 -1854
  15. package/dist/index.js.map +1 -1
  16. package/dist/{lifecycle-LURNDNYO-UWQYPNPX.js → lifecycle-LFOL6YFM-TCHDX3J5.js} +5 -5
  17. package/dist/{state-ZSP3ORXW-WI6KOIG3.js → state-KD7M46ZP-KHFTHFUS.js} +2 -2
  18. package/dist/stats-Z4BVJODD-HEC4TMUZ.js +19 -0
  19. package/package.json +5 -4
  20. package/runtime/docker/Dockerfile.box +47 -19
  21. package/runtime/docker/apps/cli/share/agentbox-setup/SKILL.md +39 -50
  22. package/runtime/docker/packages/ctl/dist/bin.cjs +219 -148
  23. package/runtime/docker/packages/sandbox-docker/scripts/agentbox-checkpoint-cleanup +42 -0
  24. package/runtime/docker/packages/sandbox-docker/scripts/custom-system-CLAUDE.md +26 -15
  25. package/runtime/relay/bin.cjs +288 -12
  26. package/share/agentbox-setup/SKILL.md +39 -50
  27. package/dist/chunk-FQD6ZWYW.js.map +0 -1
  28. package/dist/chunk-IDR4HVIC.js.map +0 -1
  29. package/dist/chunk-J35IH7W5.js.map +0 -1
  30. package/dist/chunk-NSIECUCS.js.map +0 -1
  31. package/dist/chunk-SOMIKEN2.js.map +0 -1
  32. package/dist/chunk-WR5FFGE5.js.map +0 -1
  33. package/dist/create-4BQY2UYU-CGSW3RGE.js +0 -15
  34. package/dist/stats-GZFLPYTU-DBJ2DVBJ.js +0 -19
  35. /package/dist/{create-4BQY2UYU-CGSW3RGE.js.map → create-AHZ3GVEZ-TGEDL7UX.js.map} +0 -0
  36. /package/dist/{lifecycle-LURNDNYO-UWQYPNPX.js.map → lifecycle-LFOL6YFM-TCHDX3J5.js.map} +0 -0
  37. /package/dist/{state-ZSP3ORXW-WI6KOIG3.js.map → state-KD7M46ZP-KHFTHFUS.js.map} +0 -0
  38. /package/dist/{stats-GZFLPYTU-DBJ2DVBJ.js.map → stats-Z4BVJODD-HEC4TMUZ.js.map} +0 -0
@@ -6,11 +6,11 @@ import {
6
6
  execInBox,
7
7
  orbstackVolumePath,
8
8
  removeContainer,
9
+ sanitizeMnemonic,
9
10
  volumeExists
10
- } from "./chunk-SOMIKEN2.js";
11
+ } from "./chunk-RFC5F5HR.js";
11
12
 
12
- // ../../packages/sandbox-docker/dist/chunk-NY7PB7KY.js
13
- import { spawnSync } from "child_process";
13
+ // ../../packages/sandbox-docker/dist/chunk-LGNJND37.js
14
14
  import { mkdir, mkdtemp, readdir, readFile, rm, stat, writeFile } from "fs/promises";
15
15
  import { homedir, tmpdir } from "os";
16
16
  import { join, relative } from "path";
@@ -20,6 +20,7 @@ import { execa as execa2 } from "execa";
20
20
  import { readdir as readdir2, stat as stat2 } from "fs/promises";
21
21
  import { join as join2 } from "path";
22
22
  import { execa as execa3 } from "execa";
23
+ import { execa as execa4 } from "execa";
23
24
  import { mkdir as mkdir2, readdir as readdir3, rm as rm2, stat as stat3 } from "fs/promises";
24
25
  import { homedir as homedir2, platform } from "os";
25
26
  import { join as join3, resolve } from "path";
@@ -41,7 +42,7 @@ var RELAY_NETWORK_NAME = "agentbox-net";
41
42
  var RELAY_IMAGE_REF = "agentbox/relay:dev";
42
43
  var MAX_BODY_BYTES = 1024 * 1024;
43
44
 
44
- // ../../packages/sandbox-docker/dist/chunk-NY7PB7KY.js
45
+ // ../../packages/sandbox-docker/dist/chunk-LGNJND37.js
45
46
  function isHostPathHookCommand(command, hostHome) {
46
47
  if (typeof command !== "string" || command.length === 0) return false;
47
48
  if (hostHome.length === 0) return false;
@@ -193,6 +194,8 @@ var DEFAULT_CLAUDE_SESSION = "claude";
193
194
  var CONTAINER_CLAUDE_DIR = "/home/vscode/.claude";
194
195
  var CONTAINER_USER = "vscode";
195
196
  var CONTAINER_WORKSPACE = "/workspace";
197
+ var IN_BOX_SETUP_GUIDE_PATH = "/usr/local/share/agentbox/setup-guide.md";
198
+ var SETUP_SKILL_DST = "/dst/skills/agentbox-setup/SKILL.md";
196
199
  function resolveClaudeVolume(opts) {
197
200
  if (opts.isolate) {
198
201
  return { volume: `${SHARED_CLAUDE_VOLUME}-${opts.boxId}` };
@@ -334,6 +337,28 @@ async function ensureClaudeVolume(spec, opts) {
334
337
  }
335
338
  return { created, synced: true, filteredHookCount, clearedInstallMethod, aliasedProjectKey };
336
339
  }
340
+ async function seedSetupSkillIntoVolume(volume, image) {
341
+ try {
342
+ const { stdout } = await execa("docker", [
343
+ "run",
344
+ "--rm",
345
+ "--user",
346
+ "0",
347
+ "-v",
348
+ `${volume}:/dst`,
349
+ image,
350
+ "sh",
351
+ "-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`
356
+ ]);
357
+ return { seeded: stdout.includes("SEEDED") };
358
+ } catch {
359
+ return { seeded: false };
360
+ }
361
+ }
337
362
  async function maybeFilterTo(src, dest, hostHome, opts = {}) {
338
363
  let parsed;
339
364
  try {
@@ -564,7 +589,6 @@ function shQuote(arg) {
564
589
  }
565
590
  async function startClaudeSession(opts) {
566
591
  const sessionName = opts.sessionName ?? DEFAULT_CLAUDE_SESSION;
567
- const boxName = opts.boxName ?? opts.container.replace(/^agentbox-/, "");
568
592
  const cmd = ["claude", ...opts.claudeArgs].map(shQuote).join(" ");
569
593
  const term = process.env["TERM"] ?? "xterm-256color";
570
594
  const envFlags = ["-e", `TERM=${term}`];
@@ -586,7 +610,7 @@ async function startClaudeSession(opts) {
586
610
  "-s",
587
611
  sessionName,
588
612
  cmd,
589
- ...buildClaudeStatusBarArgs(sessionName, boxName)
613
+ ...buildClaudeStatusBarArgs(sessionName)
590
614
  ],
591
615
  { reject: false }
592
616
  );
@@ -660,13 +684,15 @@ function buildClaudeDashboardAttachArgv(container, sessionName) {
660
684
  dash
661
685
  ];
662
686
  }
663
- function buildClaudeStatusBarArgs(sessionName, boxName) {
687
+ function buildClaudeStatusBarArgs(sessionName) {
664
688
  const s = sessionName;
665
- const name = boxName;
666
689
  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.
690
+ // Server-global (no -t): primary prefix Ctrl+a (dashboard parity), keep
691
+ // tmux's default Ctrl+b as a secondary prefix so users with existing
692
+ // muscle memory / integrations aren't broken. `q` is the same key under
693
+ // both prefixes (single key table) -> Ctrl+a q AND Ctrl+b q both detach.
694
+ // `send-prefix` / `send-prefix -2` let a double-tap of either prefix
695
+ // reach Claude as that literal key.
670
696
  ";",
671
697
  "set",
672
698
  "-g",
@@ -676,9 +702,6 @@ function buildClaudeStatusBarArgs(sessionName, boxName) {
676
702
  "set",
677
703
  "-g",
678
704
  "prefix2",
679
- "None",
680
- ";",
681
- "unbind-key",
682
705
  "C-b",
683
706
  ";",
684
707
  "bind-key",
@@ -686,62 +709,23 @@ function buildClaudeStatusBarArgs(sessionName, boxName) {
686
709
  "send-prefix",
687
710
  ";",
688
711
  "bind-key",
712
+ "C-b",
713
+ "send-prefix",
714
+ "-2",
715
+ ";",
716
+ "bind-key",
689
717
  "q",
690
718
  "detach-client",
719
+ // Hide the inner tmux status bar — the wrapped-pty footer (for
720
+ // `agentbox claude` / `agentbox shell`) and the dashboard's own status
721
+ // row already show the box name + detach hint; without `status off`
722
+ // they double up.
691
723
  ";",
692
724
  "set",
693
725
  "-t",
694
726
  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
- ""
727
+ "status",
728
+ "off"
745
729
  ];
746
730
  }
747
731
  function buildShellArgv(container) {
@@ -751,16 +735,6 @@ function buildShellArgv(container) {
751
735
  function formatDetachNotice(ref) {
752
736
  return `Session detached. Reattach with: agentbox claude attach ${ref}`;
753
737
  }
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");
761
- }
762
- process.exit(code);
763
- }
764
738
  async function claudeSessionInfo(container, sessionName) {
765
739
  const name = sessionName ?? DEFAULT_CLAUDE_SESSION;
766
740
  const has = await execa(
@@ -1053,211 +1027,281 @@ async function isGitDir(path) {
1053
1027
  return false;
1054
1028
  }
1055
1029
  }
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(
1030
+ async function pickFreshBranch(hostMainRepo, base) {
1031
+ let candidate = base;
1032
+ let suffix = 2;
1033
+ while (await branchExists(hostMainRepo, candidate)) {
1034
+ candidate = `${base}-${String(suffix++)}`;
1035
+ if (suffix > 100) throw new GitWorktreeError(`could not find a free branch name near ${base}`);
1036
+ }
1037
+ return candidate;
1038
+ }
1039
+ async function branchExists(hostMainRepo, name) {
1040
+ const result = await execa2(
1071
1041
  "git",
1072
- ["-C", args.hostMainRepo, "worktree", "add", "-b", branchName, args.worktreeDir, "HEAD"],
1042
+ ["-C", hostMainRepo, "show-ref", "--verify", "--quiet", `refs/heads/${name}`],
1073
1043
  { reject: false }
1074
1044
  );
1075
- if (wadd.exitCode !== 0) {
1076
- throw new GitWorktreeError(
1077
- `git worktree add failed for ${args.hostMainRepo}: ${wadd.stderr || wadd.stdout}`
1078
- );
1045
+ return result.exitCode === 0;
1046
+ }
1047
+ var GitWorktreeError = class extends Error {
1048
+ constructor(message) {
1049
+ super(message);
1050
+ this.name = "GitWorktreeError";
1079
1051
  }
1080
- log(`created worktree ${args.worktreeDir} on branch ${branchName}`);
1081
- await execa2(
1052
+ };
1053
+ var WORKTREE_ROOT = "/home/vscode/.agentbox-worktrees";
1054
+ function fsSafeBranch(branch) {
1055
+ return branch.replace(/[^A-Za-z0-9._-]+/g, "_");
1056
+ }
1057
+ function gitWorktreePathFor(branch) {
1058
+ return `${WORKTREE_ROOT}/${fsSafeBranch(branch)}`;
1059
+ }
1060
+ async function collectRepoCarryOver(repo, branch, containerPath, gitWorktreePath) {
1061
+ const stash = await execa3("git", ["-C", repo.hostMainRepo, "stash", "create"], { reject: false });
1062
+ const stashSha = stash.exitCode === 0 ? stash.stdout.trim() || null : null;
1063
+ const untracked = await execa3(
1082
1064
  "git",
1083
- ["-C", args.hostMainRepo, "config", "extensions.worktreeConfig", "true"],
1065
+ ["-C", repo.hostMainRepo, "ls-files", "--others", "--exclude-standard", "-z"],
1084
1066
  { reject: false }
1085
1067
  );
1086
- await execa2(
1087
- "git",
1088
- ["-C", args.worktreeDir, "config", "--worktree", "commit.gpgsign", "false"],
1068
+ const untrackedNul = untracked.exitCode === 0 ? untracked.stdout : "";
1069
+ return {
1070
+ repo,
1071
+ containerPath,
1072
+ gitWorktreePath,
1073
+ branch,
1074
+ stashSha,
1075
+ untrackedNul,
1076
+ hostSource: repo.hostMainRepo
1077
+ };
1078
+ }
1079
+ async function dexec(container, argv, user = "vscode", cwd = "/") {
1080
+ const r = await execa3(
1081
+ "docker",
1082
+ ["exec", "-w", cwd, "--user", user, container, ...argv],
1089
1083
  { reject: false }
1090
1084
  );
1091
- if (stashSha) {
1092
- const withIndex = await execa2(
1093
- "git",
1094
- ["-C", args.worktreeDir, "stash", "apply", "--index", stashSha],
1085
+ if (r.exitCode !== 0) {
1086
+ throw new GitWorktreeError(`${argv.join(" ")} failed: ${r.stderr || r.stdout}`);
1087
+ }
1088
+ }
1089
+ async function chownGitBindParents(args) {
1090
+ const log = args.onLog ?? (() => {
1091
+ });
1092
+ const repos = Array.from(new Set(args.hostMainRepos));
1093
+ for (const repo of repos) {
1094
+ const result = await execInBox(args.container, ["chown", "vscode:vscode", repo], {
1095
+ user: "root"
1096
+ });
1097
+ if (result.exitCode === 0) {
1098
+ log(`chowned ${repo} to vscode:vscode (parent of bind-mounted .git)`);
1099
+ } else {
1100
+ const msg = (result.stderr || result.stdout || `exit ${result.exitCode}`).trim();
1101
+ log(`chown ${repo} failed (best-effort, ignoring): ${msg}`);
1102
+ }
1103
+ }
1104
+ }
1105
+ async function bindWorktrees(container, binds, onLog) {
1106
+ const log = onLog ?? (() => {
1107
+ });
1108
+ const ordered = [...binds].sort(
1109
+ (a, b) => a.kind === "root" && b.kind !== "root" ? -1 : a.kind !== "root" && b.kind === "root" ? 1 : 0
1110
+ );
1111
+ for (const b of ordered) {
1112
+ await execa3(
1113
+ "docker",
1114
+ ["exec", "-w", "/", "--user", "root", container, "sh", "-c", `mountpoint -q ${b.containerPath} && umount ${b.containerPath} || true`],
1115
+ { reject: false }
1116
+ );
1117
+ if (b.kind === "nested") {
1118
+ await dexec(container, ["mkdir", "-p", ctParent(b.containerPath)], "root");
1119
+ await dexec(container, ["mkdir", "-p", b.containerPath], "root");
1120
+ }
1121
+ await dexec(container, ["mount", "--bind", b.gitWorktreePath, b.containerPath], "root");
1122
+ log(`bind-mounted ${b.containerPath} <- ${b.gitWorktreePath}`);
1123
+ }
1124
+ }
1125
+ async function seedWorkspace(opts) {
1126
+ const log = opts.onLog ?? (() => {
1127
+ });
1128
+ await dexec(opts.container, ["mkdir", "-p", WORKTREE_ROOT]);
1129
+ for (const r of opts.repos) {
1130
+ const main = r.repo.hostMainRepo;
1131
+ const wt = r.gitWorktreePath;
1132
+ const add = await execa3(
1133
+ "docker",
1134
+ [
1135
+ "exec",
1136
+ "--user",
1137
+ "vscode",
1138
+ opts.container,
1139
+ "git",
1140
+ "-C",
1141
+ main,
1142
+ "worktree",
1143
+ "add",
1144
+ "-b",
1145
+ r.branch,
1146
+ wt,
1147
+ "HEAD"
1148
+ ],
1149
+ { reject: false }
1150
+ );
1151
+ if (add.exitCode !== 0) {
1152
+ throw new GitWorktreeError(
1153
+ `git worktree add ${wt} (branch ${r.branch}) failed: ${add.stderr || add.stdout}`
1154
+ );
1155
+ }
1156
+ log(`worktree ${wt} on branch ${r.branch} (host main ${main})`);
1157
+ await execa3(
1158
+ "docker",
1159
+ [
1160
+ "exec",
1161
+ "--user",
1162
+ "vscode",
1163
+ opts.container,
1164
+ "git",
1165
+ "-C",
1166
+ main,
1167
+ "config",
1168
+ "extensions.worktreeConfig",
1169
+ "true"
1170
+ ],
1095
1171
  { reject: false }
1096
1172
  );
1097
- if (withIndex.exitCode !== 0) {
1098
- const noIndex = await execa2(
1173
+ await execa3(
1174
+ "docker",
1175
+ [
1176
+ "exec",
1177
+ "--user",
1178
+ "vscode",
1179
+ opts.container,
1099
1180
  "git",
1100
- ["-C", args.worktreeDir, "stash", "apply", stashSha],
1181
+ "-C",
1182
+ wt,
1183
+ "config",
1184
+ "--worktree",
1185
+ "commit.gpgsign",
1186
+ "false"
1187
+ ],
1188
+ { reject: false }
1189
+ );
1190
+ }
1191
+ await bindWorktrees(
1192
+ opts.container,
1193
+ opts.repos.map((r) => ({
1194
+ kind: r.repo.kind,
1195
+ containerPath: r.containerPath,
1196
+ gitWorktreePath: r.gitWorktreePath
1197
+ })),
1198
+ log
1199
+ );
1200
+ for (const r of opts.repos) {
1201
+ const ct = r.containerPath;
1202
+ if (r.stashSha) {
1203
+ const withIndex = await execa3(
1204
+ "docker",
1205
+ [
1206
+ "exec",
1207
+ "--user",
1208
+ "vscode",
1209
+ opts.container,
1210
+ "git",
1211
+ "-C",
1212
+ ct,
1213
+ "stash",
1214
+ "apply",
1215
+ "--index",
1216
+ r.stashSha
1217
+ ],
1101
1218
  { reject: false }
1102
1219
  );
1103
- if (noIndex.exitCode !== 0) {
1104
- log(
1105
- `warning: stash apply failed in worktree (${withIndex.stderr || withIndex.stdout || "no message"})`
1220
+ if (withIndex.exitCode !== 0) {
1221
+ const noIndex = await execa3(
1222
+ "docker",
1223
+ [
1224
+ "exec",
1225
+ "--user",
1226
+ "vscode",
1227
+ opts.container,
1228
+ "git",
1229
+ "-C",
1230
+ ct,
1231
+ "stash",
1232
+ "apply",
1233
+ r.stashSha
1234
+ ],
1235
+ { reject: false }
1106
1236
  );
1237
+ if (noIndex.exitCode !== 0) {
1238
+ log(
1239
+ `warning: stash apply failed in ${ct} (${withIndex.stderr || withIndex.stdout || "no message"})`
1240
+ );
1241
+ } else {
1242
+ log(`applied tracked changes (without index \u2014 staged state lost) in ${ct}`);
1243
+ }
1107
1244
  } else {
1108
- log(`applied tracked changes (without index \u2014 staged state lost)`);
1245
+ log(`applied tracked changes from host main into ${ct}`);
1109
1246
  }
1110
- } else {
1111
- log(`applied tracked changes from host main`);
1112
1247
  }
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"),
1248
+ if (r.untrackedNul.length > 0) {
1249
+ const tarOut = await execa3("tar", ["-C", r.hostSource, "--null", "-T", "-", "-cf", "-"], {
1250
+ input: r.untrackedNul.replace(/\0$/, ""),
1120
1251
  encoding: "buffer",
1121
1252
  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
1253
  });
1254
+ if (tarOut.exitCode !== 0) {
1255
+ log(`warning: tar of untracked files for ${r.repo.hostMainRepo} failed: ${tarOut.stderr}`);
1256
+ continue;
1257
+ }
1258
+ const tarIn = await execa3(
1259
+ "docker",
1260
+ ["exec", "-i", "--user", "vscode", opts.container, "tar", "-C", ct, "-xf", "-"],
1261
+ { input: tarOut.stdout, reject: false }
1262
+ );
1129
1263
  if (tarIn.exitCode !== 0) {
1130
- log(`warning: untracked-file copy into worktree failed: ${tarIn.stderr}`);
1264
+ log(`warning: untracked-file copy into ${ct} failed: ${tarIn.stderr}`);
1131
1265
  } else {
1132
- log(`copied ${String(untrackedList.length)} untracked file(s) into worktree`);
1266
+ const count = r.untrackedNul.split("\0").filter((s) => s.length > 0).length;
1267
+ log(`copied ${String(count)} untracked file(s) into ${ct}`);
1133
1268
  }
1134
- } else {
1135
- log(`warning: tar of untracked files failed: ${tarOut.stderr}`);
1136
1269
  }
1137
1270
  }
1138
- return { branchName, stashSha, untrackedCount: untrackedList.length };
1139
1271
  }
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}`);
1272
+ async function seedWorkspaceFromDir(opts) {
1273
+ const log = opts.onLog ?? (() => {
1274
+ });
1275
+ const tarOut = await execa3("tar", ["-C", opts.hostSource, "-cf", "-", "."], {
1276
+ encoding: "buffer",
1277
+ reject: false
1278
+ });
1279
+ if (tarOut.exitCode !== 0) {
1280
+ throw new GitWorktreeError(`tar of ${opts.hostSource} failed: ${tarOut.stderr}`);
1146
1281
  }
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 }
1282
+ const tarIn = await execa3(
1283
+ "docker",
1284
+ ["exec", "-i", "--user", "1000:1000", opts.container, "tar", "-C", "/workspace", "-xf", "-"],
1285
+ { input: tarOut.stdout, reject: false }
1154
1286
  );
1155
- return result.exitCode === 0;
1287
+ if (tarIn.exitCode !== 0) {
1288
+ throw new GitWorktreeError(`tar extract into /workspace failed: ${tarIn.stderr}`);
1289
+ }
1290
+ log(`seeded /workspace from ${opts.hostSource}`);
1156
1291
  }
1157
- async function removeBoxWorktree(args) {
1158
- const remove = await execa2(
1292
+ async function removeInBoxWorktree(args) {
1293
+ const remove = await execa3(
1159
1294
  "git",
1160
- ["-C", args.hostMainRepo, "worktree", "remove", "--force", args.worktreeDir],
1295
+ ["-C", args.hostMainRepo, "worktree", "remove", "--force", args.gitWorktreePath],
1161
1296
  { reject: false }
1162
1297
  );
1163
1298
  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 });
1166
- }
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" };
1299
+ await execa3("git", ["-C", args.hostMainRepo, "worktree", "prune"], { reject: false });
1210
1300
  }
1211
- function shellQuote(s) {
1212
- return `'${s.replace(/'/g, `'\\''`)}'`;
1301
+ function ctParent(p) {
1302
+ const i = p.lastIndexOf("/");
1303
+ return i <= 0 ? "/" : p.slice(0, i);
1213
1304
  }
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
1305
  var EXCLUDE_DIRS = /* @__PURE__ */ new Set([
1262
1306
  "node_modules",
1263
1307
  ".next",
@@ -1274,8 +1318,11 @@ var EXCLUDE_DIRS = /* @__PURE__ */ new Set([
1274
1318
  ".parcel-cache"
1275
1319
  ]);
1276
1320
  var SNAPSHOTS_ROOT = join3(homedir2(), ".agentbox", "snapshots");
1277
- function snapshotPathFor(boxId) {
1278
- return join3(SNAPSHOTS_ROOT, boxId);
1321
+ function snapshotPathFor(box) {
1322
+ const mnemonic = sanitizeMnemonic(box.name);
1323
+ const n = box.projectIndex;
1324
+ const segment = typeof n === "number" && Number.isFinite(n) && n > 0 ? `${box.id}-${String(n)}-${mnemonic}` : `${box.id}-${mnemonic}`;
1325
+ return join3(SNAPSHOTS_ROOT, segment);
1279
1326
  }
1280
1327
  async function findExcludedDirs(root, excluded = EXCLUDE_DIRS) {
1281
1328
  const matches = [];
@@ -1305,7 +1352,7 @@ async function createSnapshot(opts) {
1305
1352
  const excluded = opts.excluded ?? EXCLUDE_DIRS;
1306
1353
  await mkdir2(SNAPSHOTS_ROOT, { recursive: true });
1307
1354
  const cpArgs = platform() === "darwin" ? ["-cR"] : ["-R"];
1308
- await execa3("cp", [...cpArgs, `${source}/`, destination]);
1355
+ await execa4("cp", [...cpArgs, `${source}/`, destination]);
1309
1356
  const toPrune = await findExcludedDirs(destination, excluded);
1310
1357
  await Promise.all(toPrune.map((p) => rm2(p, { recursive: true, force: true })));
1311
1358
  return { destination, prunedPaths: toPrune };
@@ -1466,6 +1513,21 @@ async function stopRelay() {
1466
1513
  });
1467
1514
  return { stopped: true, pid };
1468
1515
  }
1516
+ async function getRelayStatus() {
1517
+ const pid = await readPidFile();
1518
+ const pidAlive = pid !== null && await processAlive(pid);
1519
+ const health = await fetchHealthz(300);
1520
+ return {
1521
+ running: health !== null,
1522
+ pid,
1523
+ pidAlive,
1524
+ port: PORT,
1525
+ endpoint: ENDPOINT,
1526
+ health: health === null ? null : { boxes: health.boxes, events: health.events },
1527
+ pidFile: PID_FILE,
1528
+ logFile: LOG_FILE
1529
+ };
1530
+ }
1469
1531
  function pingHealthz(timeoutMs) {
1470
1532
  return new Promise((resolveP) => {
1471
1533
  const req = httpRequest(
@@ -1484,6 +1546,42 @@ function pingHealthz(timeoutMs) {
1484
1546
  req.end();
1485
1547
  });
1486
1548
  }
1549
+ function fetchHealthz(timeoutMs) {
1550
+ return new Promise((resolveP) => {
1551
+ const req = httpRequest(
1552
+ { host: "127.0.0.1", port: PORT, method: "GET", path: "/healthz", timeout: timeoutMs },
1553
+ (res) => {
1554
+ const status = res.statusCode ?? 0;
1555
+ if (status < 200 || status >= 300) {
1556
+ res.resume();
1557
+ resolveP(null);
1558
+ return;
1559
+ }
1560
+ const chunks = [];
1561
+ res.on("data", (c) => chunks.push(c));
1562
+ res.on("end", () => {
1563
+ try {
1564
+ const parsed = JSON.parse(Buffer.concat(chunks).toString("utf8"));
1565
+ if (typeof parsed.ok === "boolean" && typeof parsed.boxes === "number" && typeof parsed.events === "number") {
1566
+ resolveP({ ok: parsed.ok, boxes: parsed.boxes, events: parsed.events });
1567
+ } else {
1568
+ resolveP(null);
1569
+ }
1570
+ } catch {
1571
+ resolveP(null);
1572
+ }
1573
+ });
1574
+ res.on("error", () => resolveP(null));
1575
+ }
1576
+ );
1577
+ req.on("error", () => resolveP(null));
1578
+ req.on("timeout", () => {
1579
+ req.destroy();
1580
+ resolveP(null);
1581
+ });
1582
+ req.end();
1583
+ });
1584
+ }
1487
1585
  async function readPidFile() {
1488
1586
  try {
1489
1587
  const text = await readFile2(PID_FILE, "utf8");
@@ -1507,7 +1605,7 @@ function generateRelayToken() {
1507
1605
  async function registerBoxWithRelay(args) {
1508
1606
  const worktrees = (args.worktrees ?? []).map((w) => ({
1509
1607
  containerPath: w.containerPath,
1510
- hostWorktreeDir: w.hostWorktreeDir,
1608
+ hostMainRepo: w.hostMainRepo,
1511
1609
  branch: w.branch
1512
1610
  }));
1513
1611
  await adminPost("/admin/register-box", {
@@ -1516,6 +1614,7 @@ async function registerBoxWithRelay(args) {
1516
1614
  name: args.name,
1517
1615
  containerName: args.containerName,
1518
1616
  createdAt: args.createdAt,
1617
+ projectIndex: args.projectIndex,
1519
1618
  worktrees
1520
1619
  });
1521
1620
  }
@@ -1573,6 +1672,7 @@ async function rehydrateRelayRegistry(boxes) {
1573
1672
  name: b.name,
1574
1673
  containerName: b.container,
1575
1674
  createdAt: b.createdAt,
1675
+ projectIndex: b.projectIndex,
1576
1676
  worktrees: b.gitWorktrees
1577
1677
  });
1578
1678
  } catch {
@@ -1682,10 +1782,10 @@ async function ensureAgentboxTasksFile(container, services, opts = {}) {
1682
1782
  return { status: "wrote" };
1683
1783
  }
1684
1784
  async function writeFileInBox(container, path, content) {
1685
- const { execa: execa4 } = await import("execa");
1686
- const result = await execa4(
1785
+ const { execa: execa5 } = await import("execa");
1786
+ const result = await execa5(
1687
1787
  "docker",
1688
- ["exec", "-i", "--user", "vscode", container, "sh", "-c", `cat > ${shellQuote2(path)}`],
1788
+ ["exec", "-i", "--user", "vscode", container, "sh", "-c", `cat > ${shellQuote(path)}`],
1689
1789
  { input: content, reject: false }
1690
1790
  );
1691
1791
  return {
@@ -1694,7 +1794,7 @@ async function writeFileInBox(container, path, content) {
1694
1794
  stderr: result.stderr ?? ""
1695
1795
  };
1696
1796
  }
1697
- function shellQuote2(s) {
1797
+ function shellQuote(s) {
1698
1798
  return `'${s.replace(/'/g, `'\\''`)}'`;
1699
1799
  }
1700
1800
 
@@ -2181,19 +2281,22 @@ async function loadConfig(path) {
2181
2281
  }
2182
2282
 
2183
2283
  export {
2284
+ DEFAULT_RELAY_PORT,
2184
2285
  RELAY_CONTAINER_NAME,
2185
2286
  RELAY_NETWORK_NAME,
2186
2287
  RELAY_IMAGE_REF,
2187
2288
  SHARED_CLAUDE_VOLUME,
2188
2289
  resolveClaudeVolume,
2189
2290
  ensureClaudeVolume,
2291
+ seedSetupSkillIntoVolume,
2190
2292
  buildClaudeMounts,
2191
2293
  rebuildPluginNativeDeps,
2192
2294
  ClaudeSessionError,
2193
2295
  startClaudeSession,
2296
+ buildClaudeAttachArgv,
2194
2297
  buildClaudeDashboardAttachArgv,
2195
2298
  buildShellArgv,
2196
- attachClaudeSession,
2299
+ formatDetachNotice,
2197
2300
  claudeSessionInfo,
2198
2301
  pullClaudeExtras,
2199
2302
  SHARED_DOCKER_CACHE_VOLUME,
@@ -2205,17 +2308,21 @@ export {
2205
2308
  buildVncUrls,
2206
2309
  WEB_CONTAINER_PORT,
2207
2310
  detectGitRepos,
2208
- createBoxWorktree,
2209
- removeBoxWorktree,
2210
- DEFAULT_LOWER_DIRS,
2211
- mountOverlay,
2212
- verifyOverlay,
2311
+ pickFreshBranch,
2312
+ gitWorktreePathFor,
2313
+ collectRepoCarryOver,
2314
+ chownGitBindParents,
2315
+ bindWorktrees,
2316
+ seedWorkspace,
2317
+ seedWorkspaceFromDir,
2318
+ removeInBoxWorktree,
2213
2319
  SNAPSHOTS_ROOT,
2214
2320
  snapshotPathFor,
2215
2321
  createSnapshot,
2216
2322
  launchCtlDaemon,
2217
2323
  ensureRelay,
2218
2324
  stopRelay,
2325
+ getRelayStatus,
2219
2326
  generateRelayToken,
2220
2327
  registerBoxWithRelay,
2221
2328
  forgetBoxFromRelay,
@@ -2236,4 +2343,4 @@ export {
2236
2343
  ConfigError,
2237
2344
  loadConfig
2238
2345
  };
2239
- //# sourceMappingURL=chunk-NSIECUCS.js.map
2346
+ //# sourceMappingURL=chunk-PXUBE5KS.js.map