@madarco/agentbox 0.4.0 → 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-3NCUES35.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-3JKQNOXP.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-MOC54XL6.js → chunk-PXUBE5KS.js} +376 -245
  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 +2760 -1857
  15. package/dist/index.js.map +1 -1
  16. package/dist/{lifecycle-YTMZYKOE-TD5S5FTS.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 +3 -2
  20. package/runtime/docker/Dockerfile.box +53 -20
  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-3JKQNOXP.js.map +0 -1
  28. package/dist/chunk-3NCUES35.js.map +0 -1
  29. package/dist/chunk-IDR4HVIC.js.map +0 -1
  30. package/dist/chunk-J35IH7W5.js.map +0 -1
  31. package/dist/chunk-MOC54XL6.js.map +0 -1
  32. package/dist/chunk-SOMIKEN2.js.map +0 -1
  33. package/dist/create-SE6H4B5U-IWAZHJHV.js +0 -15
  34. package/dist/stats-GZFLPYTU-DBJ2DVBJ.js +0 -19
  35. /package/dist/{create-SE6H4B5U-IWAZHJHV.js.map → create-AHZ3GVEZ-TGEDL7UX.js.map} +0 -0
  36. /package/dist/{lifecycle-YTMZYKOE-TD5S5FTS.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-DVEY57YJ.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-DVEY57YJ.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,64 +684,48 @@ 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 [
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.
667
696
  ";",
668
697
  "set",
669
- "-t",
670
- s,
671
- "status-interval",
672
- "60",
698
+ "-g",
699
+ "prefix",
700
+ "C-a",
673
701
  ";",
674
702
  "set",
675
- "-t",
676
- s,
677
- "status-justify",
678
- "left",
703
+ "-g",
704
+ "prefix2",
705
+ "C-b",
679
706
  ";",
680
- "set",
681
- "-t",
682
- s,
683
- "status-style",
684
- "bg=colour236,fg=colour250",
685
- ";",
686
- "set",
687
- "-t",
688
- s,
689
- "status-left-length",
690
- "60",
707
+ "bind-key",
708
+ "C-a",
709
+ "send-prefix",
691
710
  ";",
692
- "set",
693
- "-t",
694
- s,
695
- "status-left",
696
- `#[fg=colour16,bg=colour39,bold] agentbox \u25B8 ${name} #[default] `,
711
+ "bind-key",
712
+ "C-b",
713
+ "send-prefix",
714
+ "-2",
697
715
  ";",
698
- "set",
699
- "-t",
700
- s,
701
- "status-right-length",
702
- "30",
716
+ "bind-key",
717
+ "q",
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.
703
723
  ";",
704
724
  "set",
705
725
  "-t",
706
726
  s,
707
- "status-right",
708
- "#[fg=colour245]Ctrl-b d detach ",
709
- ";",
710
- "set",
711
- "-t",
712
- s,
713
- "window-status-format",
714
- "",
715
- ";",
716
- "set",
717
- "-t",
718
- s,
719
- "window-status-current-format",
720
- ""
727
+ "status",
728
+ "off"
721
729
  ];
722
730
  }
723
731
  function buildShellArgv(container) {
@@ -727,16 +735,6 @@ function buildShellArgv(container) {
727
735
  function formatDetachNotice(ref) {
728
736
  return `Session detached. Reattach with: agentbox claude attach ${ref}`;
729
737
  }
730
- function attachClaudeSession(container, sessionName, reattachRef) {
731
- const child = spawnSync("docker", buildClaudeAttachArgv(container, sessionName), {
732
- stdio: "inherit"
733
- });
734
- const code = child.status ?? 0;
735
- if (reattachRef && code === 0) {
736
- process.stdout.write("\x1B[1A\x1B[2K\r" + formatDetachNotice(reattachRef) + "\n");
737
- }
738
- process.exit(code);
739
- }
740
738
  async function claudeSessionInfo(container, sessionName) {
741
739
  const name = sessionName ?? DEFAULT_CLAUDE_SESSION;
742
740
  const has = await execa(
@@ -1029,211 +1027,281 @@ async function isGitDir(path) {
1029
1027
  return false;
1030
1028
  }
1031
1029
  }
1032
- async function createBoxWorktree(args) {
1033
- const log = args.onLog ?? (() => {
1034
- });
1035
- const stash = await execa2("git", ["-C", args.hostMainRepo, "stash", "create"], {
1036
- reject: false
1037
- });
1038
- const stashSha = stash.exitCode === 0 ? stash.stdout.trim() || null : null;
1039
- const untracked = await execa2(
1040
- "git",
1041
- ["-C", args.hostMainRepo, "ls-files", "--others", "--exclude-standard", "-z"],
1042
- { reject: false }
1043
- );
1044
- const untrackedList = untracked.exitCode === 0 && untracked.stdout.length > 0 ? untracked.stdout.split("\0").filter((s) => s.length > 0) : [];
1045
- const branchName = await pickFreshBranch(args.hostMainRepo, args.branchName);
1046
- 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(
1047
1041
  "git",
1048
- ["-C", args.hostMainRepo, "worktree", "add", "-b", branchName, args.worktreeDir, "HEAD"],
1042
+ ["-C", hostMainRepo, "show-ref", "--verify", "--quiet", `refs/heads/${name}`],
1049
1043
  { reject: false }
1050
1044
  );
1051
- if (wadd.exitCode !== 0) {
1052
- throw new GitWorktreeError(
1053
- `git worktree add failed for ${args.hostMainRepo}: ${wadd.stderr || wadd.stdout}`
1054
- );
1045
+ return result.exitCode === 0;
1046
+ }
1047
+ var GitWorktreeError = class extends Error {
1048
+ constructor(message) {
1049
+ super(message);
1050
+ this.name = "GitWorktreeError";
1055
1051
  }
1056
- log(`created worktree ${args.worktreeDir} on branch ${branchName}`);
1057
- 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(
1058
1064
  "git",
1059
- ["-C", args.hostMainRepo, "config", "extensions.worktreeConfig", "true"],
1065
+ ["-C", repo.hostMainRepo, "ls-files", "--others", "--exclude-standard", "-z"],
1060
1066
  { reject: false }
1061
1067
  );
1062
- await execa2(
1063
- "git",
1064
- ["-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],
1065
1083
  { reject: false }
1066
1084
  );
1067
- if (stashSha) {
1068
- const withIndex = await execa2(
1069
- "git",
1070
- ["-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
+ ],
1071
1171
  { reject: false }
1072
1172
  );
1073
- if (withIndex.exitCode !== 0) {
1074
- const noIndex = await execa2(
1173
+ await execa3(
1174
+ "docker",
1175
+ [
1176
+ "exec",
1177
+ "--user",
1178
+ "vscode",
1179
+ opts.container,
1075
1180
  "git",
1076
- ["-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
+ ],
1077
1218
  { reject: false }
1078
1219
  );
1079
- if (noIndex.exitCode !== 0) {
1080
- log(
1081
- `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 }
1082
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
+ }
1083
1244
  } else {
1084
- log(`applied tracked changes (without index \u2014 staged state lost)`);
1245
+ log(`applied tracked changes from host main into ${ct}`);
1085
1246
  }
1086
- } else {
1087
- log(`applied tracked changes from host main`);
1088
1247
  }
1089
- }
1090
- if (untrackedList.length > 0) {
1091
- const tarOut = await execa2(
1092
- "tar",
1093
- ["-C", args.hostMainRepo, "--null", "-T", "-", "-cf", "-"],
1094
- {
1095
- 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$/, ""),
1096
1251
  encoding: "buffer",
1097
1252
  reject: false
1098
- }
1099
- );
1100
- if (tarOut.exitCode === 0) {
1101
- const tarIn = await execa2("tar", ["-C", args.worktreeDir, "-xf", "-"], {
1102
- input: tarOut.stdout,
1103
- reject: false
1104
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
+ );
1105
1263
  if (tarIn.exitCode !== 0) {
1106
- log(`warning: untracked-file copy into worktree failed: ${tarIn.stderr}`);
1264
+ log(`warning: untracked-file copy into ${ct} failed: ${tarIn.stderr}`);
1107
1265
  } else {
1108
- 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}`);
1109
1268
  }
1110
- } else {
1111
- log(`warning: tar of untracked files failed: ${tarOut.stderr}`);
1112
1269
  }
1113
1270
  }
1114
- return { branchName, stashSha, untrackedCount: untrackedList.length };
1115
1271
  }
1116
- async function pickFreshBranch(hostMainRepo, base) {
1117
- let candidate = base;
1118
- let suffix = 2;
1119
- while (await branchExists(hostMainRepo, candidate)) {
1120
- candidate = `${base}-${String(suffix++)}`;
1121
- 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}`);
1122
1281
  }
1123
- return candidate;
1124
- }
1125
- async function branchExists(hostMainRepo, name) {
1126
- const result = await execa2(
1127
- "git",
1128
- ["-C", hostMainRepo, "show-ref", "--verify", "--quiet", `refs/heads/${name}`],
1129
- { 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 }
1130
1286
  );
1131
- 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}`);
1132
1291
  }
1133
- async function removeBoxWorktree(args) {
1134
- const remove = await execa2(
1292
+ async function removeInBoxWorktree(args) {
1293
+ const remove = await execa3(
1135
1294
  "git",
1136
- ["-C", args.hostMainRepo, "worktree", "remove", "--force", args.worktreeDir],
1295
+ ["-C", args.hostMainRepo, "worktree", "remove", "--force", args.gitWorktreePath],
1137
1296
  { reject: false }
1138
1297
  );
1139
1298
  if (remove.exitCode === 0) return;
1140
- await execa2("rm", ["-rf", args.worktreeDir], { reject: false });
1141
- await execa2("git", ["-C", args.hostMainRepo, "worktree", "prune"], { reject: false });
1142
- }
1143
- var GitWorktreeError = class extends Error {
1144
- constructor(message) {
1145
- super(message);
1146
- this.name = "GitWorktreeError";
1147
- }
1148
- };
1149
- var DEFAULT_LOWER_DIRS = ["/host-src"];
1150
- var BOX_USER_UID = 1e3;
1151
- var BOX_USER_GID = 1e3;
1152
- async function mountOverlay(container, opts = {}) {
1153
- const lowerDirs = opts.lowerDirs && opts.lowerDirs.length > 0 ? opts.lowerDirs : ["/host-src"];
1154
- const mountOpts = [
1155
- `lowerdir=${lowerDirs.join(":")}`,
1156
- "upperdir=/upper/upper",
1157
- "workdir=/upper/work",
1158
- `squash_to_uid=${String(BOX_USER_UID)}`,
1159
- `squash_to_gid=${String(BOX_USER_GID)}`
1160
- ].join(",");
1161
- const lines = [
1162
- "set -euo pipefail",
1163
- "mkdir -p /upper/upper /upper/work /workspace",
1164
- // Idempotent — if a previous attempt left a stale overlay, unmount first.
1165
- "mountpoint -q /workspace && fusermount3 -u /workspace || true",
1166
- `fuse-overlayfs -o ${mountOpts} /workspace`,
1167
- "mountpoint -q /workspace"
1168
- ];
1169
- for (const w of opts.nestedWorktrees ?? []) {
1170
- lines.push(
1171
- `mkdir -p ${shellQuote(w.containerPath)}`,
1172
- `mountpoint -q ${shellQuote(w.containerPath)} && umount ${shellQuote(w.containerPath)} || true`,
1173
- `mount --bind ${shellQuote(w.mountFromPath)} ${shellQuote(w.containerPath)}`,
1174
- `mountpoint -q ${shellQuote(w.containerPath)}`
1175
- );
1176
- }
1177
- const result = await execInBox(container, ["bash", "-lc", lines.join("\n")], { user: "root" });
1178
- if (result.exitCode !== 0) {
1179
- throw new OverlayError(
1180
- `failed to mount FUSE overlay in ${container}`,
1181
- result.stdout,
1182
- result.stderr
1183
- );
1184
- }
1185
- return { upperWritePath: "/upper/upper" };
1299
+ await execa3("git", ["-C", args.hostMainRepo, "worktree", "prune"], { reject: false });
1186
1300
  }
1187
- function shellQuote(s) {
1188
- return `'${s.replace(/'/g, `'\\''`)}'`;
1189
- }
1190
- async function verifyOverlay(container, lowerDirs = DEFAULT_LOWER_DIRS) {
1191
- const sentinel = ".agentbox-overlay-check";
1192
- const checks = [];
1193
- const ls = await execInBox(container, ["bash", "-lc", `ls -A /workspace | head -1`], {
1194
- user: "root"
1195
- });
1196
- checks.push({
1197
- name: "workspace lists lower contents",
1198
- ok: ls.exitCode === 0,
1199
- detail: ls.exitCode === 0 ? `first entry: ${ls.stdout.trim() || "(empty)"}` : ls.stderr.trim()
1200
- });
1201
- const write = await execInBox(container, ["bash", "-lc", `touch /workspace/${sentinel}`], {
1202
- user: "root"
1203
- });
1204
- checks.push({
1205
- name: "write through overlay succeeds",
1206
- ok: write.exitCode === 0,
1207
- detail: write.exitCode === 0 ? `created /workspace/${sentinel}` : write.stderr.trim()
1208
- });
1209
- const upper = await execInBox(container, ["bash", "-lc", `test -f /upper/upper/${sentinel}`], {
1210
- user: "root"
1211
- });
1212
- checks.push({
1213
- name: "write lands in /upper (cow target)",
1214
- ok: upper.exitCode === 0,
1215
- detail: upper.exitCode === 0 ? `/upper/upper/${sentinel} exists` : `expected /upper/upper/${sentinel} to exist`
1216
- });
1217
- const lowerProbe = lowerDirs.map((d) => `test ! -e ${shellQuote(d)}/${sentinel}`).join(" && ");
1218
- const lower = await execInBox(container, ["bash", "-lc", lowerProbe], { user: "root" });
1219
- checks.push({
1220
- name: `lower untouched (${lowerDirs.join(", ")})`,
1221
- ok: lower.exitCode === 0,
1222
- detail: lower.exitCode === 0 ? `${sentinel} absent from every lower` : `${sentinel} leaked into a lower layer`
1223
- });
1224
- await execInBox(container, ["bash", "-lc", `rm -f /workspace/${sentinel}`], { user: "root" });
1225
- return checks;
1301
+ function ctParent(p) {
1302
+ const i = p.lastIndexOf("/");
1303
+ return i <= 0 ? "/" : p.slice(0, i);
1226
1304
  }
1227
- var OverlayError = class extends Error {
1228
- constructor(message, stdout, stderr) {
1229
- super(message);
1230
- this.stdout = stdout;
1231
- this.stderr = stderr;
1232
- this.name = "OverlayError";
1233
- }
1234
- stdout;
1235
- stderr;
1236
- };
1237
1305
  var EXCLUDE_DIRS = /* @__PURE__ */ new Set([
1238
1306
  "node_modules",
1239
1307
  ".next",
@@ -1250,8 +1318,11 @@ var EXCLUDE_DIRS = /* @__PURE__ */ new Set([
1250
1318
  ".parcel-cache"
1251
1319
  ]);
1252
1320
  var SNAPSHOTS_ROOT = join3(homedir2(), ".agentbox", "snapshots");
1253
- function snapshotPathFor(boxId) {
1254
- 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);
1255
1326
  }
1256
1327
  async function findExcludedDirs(root, excluded = EXCLUDE_DIRS) {
1257
1328
  const matches = [];
@@ -1281,7 +1352,7 @@ async function createSnapshot(opts) {
1281
1352
  const excluded = opts.excluded ?? EXCLUDE_DIRS;
1282
1353
  await mkdir2(SNAPSHOTS_ROOT, { recursive: true });
1283
1354
  const cpArgs = platform() === "darwin" ? ["-cR"] : ["-R"];
1284
- await execa3("cp", [...cpArgs, `${source}/`, destination]);
1355
+ await execa4("cp", [...cpArgs, `${source}/`, destination]);
1285
1356
  const toPrune = await findExcludedDirs(destination, excluded);
1286
1357
  await Promise.all(toPrune.map((p) => rm2(p, { recursive: true, force: true })));
1287
1358
  return { destination, prunedPaths: toPrune };
@@ -1442,6 +1513,21 @@ async function stopRelay() {
1442
1513
  });
1443
1514
  return { stopped: true, pid };
1444
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
+ }
1445
1531
  function pingHealthz(timeoutMs) {
1446
1532
  return new Promise((resolveP) => {
1447
1533
  const req = httpRequest(
@@ -1460,6 +1546,42 @@ function pingHealthz(timeoutMs) {
1460
1546
  req.end();
1461
1547
  });
1462
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
+ }
1463
1585
  async function readPidFile() {
1464
1586
  try {
1465
1587
  const text = await readFile2(PID_FILE, "utf8");
@@ -1483,7 +1605,7 @@ function generateRelayToken() {
1483
1605
  async function registerBoxWithRelay(args) {
1484
1606
  const worktrees = (args.worktrees ?? []).map((w) => ({
1485
1607
  containerPath: w.containerPath,
1486
- hostWorktreeDir: w.hostWorktreeDir,
1608
+ hostMainRepo: w.hostMainRepo,
1487
1609
  branch: w.branch
1488
1610
  }));
1489
1611
  await adminPost("/admin/register-box", {
@@ -1492,6 +1614,7 @@ async function registerBoxWithRelay(args) {
1492
1614
  name: args.name,
1493
1615
  containerName: args.containerName,
1494
1616
  createdAt: args.createdAt,
1617
+ projectIndex: args.projectIndex,
1495
1618
  worktrees
1496
1619
  });
1497
1620
  }
@@ -1549,6 +1672,7 @@ async function rehydrateRelayRegistry(boxes) {
1549
1672
  name: b.name,
1550
1673
  containerName: b.container,
1551
1674
  createdAt: b.createdAt,
1675
+ projectIndex: b.projectIndex,
1552
1676
  worktrees: b.gitWorktrees
1553
1677
  });
1554
1678
  } catch {
@@ -1658,10 +1782,10 @@ async function ensureAgentboxTasksFile(container, services, opts = {}) {
1658
1782
  return { status: "wrote" };
1659
1783
  }
1660
1784
  async function writeFileInBox(container, path, content) {
1661
- const { execa: execa4 } = await import("execa");
1662
- const result = await execa4(
1785
+ const { execa: execa5 } = await import("execa");
1786
+ const result = await execa5(
1663
1787
  "docker",
1664
- ["exec", "-i", "--user", "vscode", container, "sh", "-c", `cat > ${shellQuote2(path)}`],
1788
+ ["exec", "-i", "--user", "vscode", container, "sh", "-c", `cat > ${shellQuote(path)}`],
1665
1789
  { input: content, reject: false }
1666
1790
  );
1667
1791
  return {
@@ -1670,7 +1794,7 @@ async function writeFileInBox(container, path, content) {
1670
1794
  stderr: result.stderr ?? ""
1671
1795
  };
1672
1796
  }
1673
- function shellQuote2(s) {
1797
+ function shellQuote(s) {
1674
1798
  return `'${s.replace(/'/g, `'\\''`)}'`;
1675
1799
  }
1676
1800
 
@@ -2157,19 +2281,22 @@ async function loadConfig(path) {
2157
2281
  }
2158
2282
 
2159
2283
  export {
2284
+ DEFAULT_RELAY_PORT,
2160
2285
  RELAY_CONTAINER_NAME,
2161
2286
  RELAY_NETWORK_NAME,
2162
2287
  RELAY_IMAGE_REF,
2163
2288
  SHARED_CLAUDE_VOLUME,
2164
2289
  resolveClaudeVolume,
2165
2290
  ensureClaudeVolume,
2291
+ seedSetupSkillIntoVolume,
2166
2292
  buildClaudeMounts,
2167
2293
  rebuildPluginNativeDeps,
2168
2294
  ClaudeSessionError,
2169
2295
  startClaudeSession,
2296
+ buildClaudeAttachArgv,
2170
2297
  buildClaudeDashboardAttachArgv,
2171
2298
  buildShellArgv,
2172
- attachClaudeSession,
2299
+ formatDetachNotice,
2173
2300
  claudeSessionInfo,
2174
2301
  pullClaudeExtras,
2175
2302
  SHARED_DOCKER_CACHE_VOLUME,
@@ -2181,17 +2308,21 @@ export {
2181
2308
  buildVncUrls,
2182
2309
  WEB_CONTAINER_PORT,
2183
2310
  detectGitRepos,
2184
- createBoxWorktree,
2185
- removeBoxWorktree,
2186
- DEFAULT_LOWER_DIRS,
2187
- mountOverlay,
2188
- verifyOverlay,
2311
+ pickFreshBranch,
2312
+ gitWorktreePathFor,
2313
+ collectRepoCarryOver,
2314
+ chownGitBindParents,
2315
+ bindWorktrees,
2316
+ seedWorkspace,
2317
+ seedWorkspaceFromDir,
2318
+ removeInBoxWorktree,
2189
2319
  SNAPSHOTS_ROOT,
2190
2320
  snapshotPathFor,
2191
2321
  createSnapshot,
2192
2322
  launchCtlDaemon,
2193
2323
  ensureRelay,
2194
2324
  stopRelay,
2325
+ getRelayStatus,
2195
2326
  generateRelayToken,
2196
2327
  registerBoxWithRelay,
2197
2328
  forgetBoxFromRelay,
@@ -2212,4 +2343,4 @@ export {
2212
2343
  ConfigError,
2213
2344
  loadConfig
2214
2345
  };
2215
- //# sourceMappingURL=chunk-MOC54XL6.js.map
2346
+ //# sourceMappingURL=chunk-PXUBE5KS.js.map