@sideboard-ai/core 0.1.66 → 0.1.68

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 (35) hide show
  1. package/dist/{agents-EWFD765A.js → agents-JBD7IAGK.js} +10 -6
  2. package/dist/{agents-MD7SCFYX.js → agents-PE3RHQS5.js} +10 -6
  3. package/dist/{chunk-BBEH2CJE.js → chunk-47RBLBRK.js} +22 -7
  4. package/dist/{chunk-IFAU2DEY.js → chunk-6Q7H6SMX.js} +22 -7
  5. package/dist/{chunk-5AG5BUQN.js → chunk-AE5VBOFE.js} +14 -3
  6. package/dist/{chunk-DZYMNW2A.js → chunk-BTL7EMGT.js} +4 -4
  7. package/dist/{chunk-TXQ6K2GR.js → chunk-D4DPQ552.js} +14 -3
  8. package/dist/{chunk-PESJDZTB.js → chunk-DPDGMMGI.js} +2 -2
  9. package/dist/{chunk-R7GUDEPY.js → chunk-EEBDVLDK.js} +22 -20
  10. package/dist/{chunk-LRBRQFBU.js → chunk-N5LMEP65.js} +1 -1
  11. package/dist/{chunk-DNPP7QMK.js → chunk-OICHV4DH.js} +2 -2
  12. package/dist/{chunk-HTOTTJJX.js → chunk-QQC2D4MY.js} +2 -2
  13. package/dist/{chunk-I2KGRAXC.js → chunk-REKGFVUX.js} +22 -20
  14. package/dist/{chunk-YTNLOJPK.js → chunk-ROXEENBT.js} +2 -2
  15. package/dist/{chunk-EVD4KNKR.js → chunk-VROPG6QF.js} +1 -1
  16. package/dist/{chunk-AKPEXIKK.js → chunk-VX6ACNXS.js} +1 -1
  17. package/dist/{connected-teams-CHXO5FFA.js → connected-teams-35EIRJCP.js} +2 -2
  18. package/dist/{connected-teams-GJTESSTX.js → connected-teams-ZRS53OAT.js} +2 -2
  19. package/dist/{coordinator-prompt-ILSULI7C.js → coordinator-prompt-HD6IMCKS.js} +3 -3
  20. package/dist/{coordinator-prompt-E5UGSJES.js → coordinator-prompt-YZYZ5P6P.js} +3 -3
  21. package/dist/{global-workspace-44UG64NA.js → global-workspace-7P5NG3JZ.js} +4 -4
  22. package/dist/{global-workspace-5PM5LA6E.js → global-workspace-Z6GF7D2K.js} +4 -4
  23. package/dist/index.cjs +401 -163
  24. package/dist/index.d.cts +33 -3
  25. package/dist/index.d.ts +33 -3
  26. package/dist/index.js +368 -157
  27. package/dist/mcp/run-stdio.cjs +397 -163
  28. package/dist/mcp/run-stdio.js +363 -156
  29. package/dist/{run-74S2NZWB.js → run-A4RHY7HH.js} +1 -1
  30. package/dist/{run-JLN5JK7Q.js → run-V2WZUMJA.js} +1 -1
  31. package/dist/{workspaces-OVBCSS5Y.js → workspaces-BWJNGEE6.js} +5 -5
  32. package/dist/{workspaces-RXFXQRRF.js → workspaces-O6EZGKQK.js} +5 -5
  33. package/dist/{worktree-S2N63FLV.js → worktree-VDXLUOWR.js} +2 -2
  34. package/dist/{worktree-K2JB53R5.js → worktree-ZPSKEZFE.js} +2 -2
  35. package/package.json +1 -1
@@ -1517,6 +1517,13 @@ function prependPathDir(env, dir) {
1517
1517
  }
1518
1518
  env.PATH = [dir, ...parts].join(import_node_path4.delimiter);
1519
1519
  }
1520
+ function conductorBundledBinDir(home = process.env.HOME || process.env.USERPROFILE || (0, import_node_os3.homedir)()) {
1521
+ return (0, import_node_path4.join)(home, "Library", "Application Support", "com.conductor.app", "bin");
1522
+ }
1523
+ function isConductorBundledCli(filePath) {
1524
+ const p = (filePath ?? "").replace(/\\/g, "/");
1525
+ return p.includes("/com.conductor.app/bin/") || p.endsWith("/com.conductor.app/bin");
1526
+ }
1520
1527
  function ensureAgentPath(env = process.env) {
1521
1528
  const home = env.HOME || env.USERPROFILE || (0, import_node_os3.homedir)();
1522
1529
  const current = env.PATH ?? "";
@@ -1525,7 +1532,9 @@ function ensureAgentPath(env = process.env) {
1525
1532
  const extras = [
1526
1533
  ...EXTRA_BIN_DIRS.map((rel) => (0, import_node_path4.join)(home, rel)),
1527
1534
  "/opt/homebrew/bin",
1528
- "/usr/local/bin"
1535
+ "/usr/local/bin",
1536
+ // Keep after Homebrew/npm so a user-installed CLI still wins.
1537
+ conductorBundledBinDir(home)
1529
1538
  ];
1530
1539
  for (const dir of extras.reverse()) {
1531
1540
  if (!dir || seen.has(dir) || !(0, import_node_fs4.existsSync)(dir)) continue;
@@ -1572,7 +1581,7 @@ function withExportedPath(command, pathValue) {
1572
1581
  const trimmed = command.trim();
1573
1582
  if (!trimmed) return trimmed;
1574
1583
  if (/^(export\s+PATH=|PATH=)/.test(trimmed)) return trimmed;
1575
- return `export PATH=${posixShellSingleQuote(pathValue)}; ${trimmed}`;
1584
+ return `export PATH=${posixShellSingleQuote(pathValue)} && ${trimmed}`;
1576
1585
  }
1577
1586
  var import_node_fs4, import_node_child_process, import_node_os3, import_node_path4, EXTRA_BIN_DIRS;
1578
1587
  var init_path = __esm({
@@ -1640,7 +1649,7 @@ async function git(args, cwd, opts) {
1640
1649
  prefix.push("-c", `${key}=${value}`);
1641
1650
  }
1642
1651
  }
1643
- return run("git", [...prefix, ...args], {
1652
+ return run("git", ["--no-pager", ...prefix, ...args], {
1644
1653
  cwd,
1645
1654
  reject: opts?.reject,
1646
1655
  timeoutMs: opts?.timeoutMs,
@@ -2115,7 +2124,7 @@ async function originGhRepoEnv(cwd) {
2115
2124
  const slug = await resolveGithubRepoSlug(cwd);
2116
2125
  return slug ? { GH_REPO: slug } : {};
2117
2126
  }
2118
- async function resolveDefaultBranch(repoPath) {
2127
+ async function resolveDefaultBranch(repoPath, opts) {
2119
2128
  const viaOrigin = await git(
2120
2129
  ["symbolic-ref", "refs/remotes/origin/HEAD"],
2121
2130
  repoPath,
@@ -2124,22 +2133,24 @@ async function resolveDefaultBranch(repoPath) {
2124
2133
  if (viaOrigin.exitCode === 0 && viaOrigin.stdout.trim()) {
2125
2134
  return viaOrigin.stdout.trim().replace(/^refs\/remotes\/origin\//, "");
2126
2135
  }
2127
- const slug = await resolveGithubRepoSlug(repoPath);
2128
- const viaGh = await gh(
2129
- [
2130
- "repo",
2131
- "view",
2132
- ...slug ? ["--repo", slug] : [],
2133
- "--json",
2134
- "defaultBranchRef",
2135
- "--jq",
2136
- ".defaultBranchRef.name"
2137
- ],
2138
- repoPath,
2139
- { reject: false }
2140
- );
2141
- if (viaGh.exitCode === 0 && viaGh.stdout.trim()) {
2142
- return viaGh.stdout.trim();
2136
+ if (opts?.network !== false) {
2137
+ const slug = await resolveGithubRepoSlug(repoPath);
2138
+ const viaGh = await gh(
2139
+ [
2140
+ "repo",
2141
+ "view",
2142
+ ...slug ? ["--repo", slug] : [],
2143
+ "--json",
2144
+ "defaultBranchRef",
2145
+ "--jq",
2146
+ ".defaultBranchRef.name"
2147
+ ],
2148
+ repoPath,
2149
+ { reject: false, timeoutMs: 8e3 }
2150
+ );
2151
+ if (viaGh.exitCode === 0 && viaGh.stdout.trim()) {
2152
+ return viaGh.stdout.trim();
2153
+ }
2143
2154
  }
2144
2155
  for (const candidate of ["main", "master"]) {
2145
2156
  const check = await git(
@@ -2791,11 +2802,11 @@ async function listWorktrees(repoPath) {
2791
2802
  return entries;
2792
2803
  }
2793
2804
  async function isDirty(worktreePath) {
2794
- const { stdout } = await git(["status", "--porcelain", "-uall"], worktreePath);
2805
+ const { stdout } = await git(["status", "--porcelain"], worktreePath);
2795
2806
  for (const line of stdout.split("\n")) {
2796
2807
  if (!line.trim()) continue;
2797
2808
  const rel = porcelainStatusPath(line);
2798
- if (!rel || isSideboardScratchPath(rel)) continue;
2809
+ if (!rel || isSideboardScratchPath(rel) || rel === ".sideboard") continue;
2799
2810
  return true;
2800
2811
  }
2801
2812
  return false;
@@ -6657,6 +6668,11 @@ async function openInSystemTerminal(command) {
6657
6668
  `No terminal found to run: ${trimmed}. Install gnome-terminal (or similar), or run the command manually.`
6658
6669
  );
6659
6670
  }
6671
+ async function whichCli(bin) {
6672
+ const which = await run("which", [bin], { reject: false, timeoutMs: 3e3 });
6673
+ if (which.exitCode !== 0) return null;
6674
+ return which.stdout.trim().split(/\r?\n/).find(Boolean) ?? null;
6675
+ }
6660
6676
  async function installAgent(agent) {
6661
6677
  const info = getAgentSetupInfo(agent);
6662
6678
  if (info.kind === "bundled-sdk" || info.kind === "api-key") {
@@ -6678,6 +6694,17 @@ async function installAgent(agent) {
6678
6694
  };
6679
6695
  }
6680
6696
  enrichPathWithNpmGlobalBin();
6697
+ const cliBin = agent === "cursor" ? null : CLI_BIN[agent];
6698
+ if (cliBin) {
6699
+ const existing = await whichCli(cliBin);
6700
+ if (existing) {
6701
+ return {
6702
+ ok: true,
6703
+ command: existing,
6704
+ message: isConductorBundledCli(existing) ? `Using Conductor\u2019s ${cliBin} at ${existing}. No extra install \u2014 Log in only if auth is missing.` : `Already on PATH: ${existing}`
6705
+ };
6706
+ }
6707
+ }
6681
6708
  const result = await run("npm", ["install", "-g", info.npmPackage], { reject: false });
6682
6709
  const ok = result.exitCode === 0;
6683
6710
  if (!ok) {
@@ -6704,11 +6731,9 @@ async function installAgent(agent) {
6704
6731
  }
6705
6732
  }
6706
6733
  enrichPathWithNpmGlobalBin();
6707
- const cliBin = agent === "cursor" ? null : CLI_BIN[agent];
6708
6734
  let binPath = null;
6709
6735
  if (cliBin) {
6710
- const which = await run("which", [cliBin], { reject: false, timeoutMs: 3e3 });
6711
- binPath = which.exitCode === 0 ? which.stdout.trim().split(/\r?\n/).find(Boolean) ?? null : null;
6736
+ binPath = await whichCli(cliBin);
6712
6737
  }
6713
6738
  return {
6714
6739
  ok: true,
@@ -6818,6 +6843,7 @@ __export(agents_exports, {
6818
6843
  claudeAdapter: () => claudeAdapter,
6819
6844
  codexAdapter: () => codexAdapter,
6820
6845
  coerceOrchestratorAgent: () => coerceOrchestratorAgent,
6846
+ conductorBundledBinDir: () => conductorBundledBinDir,
6821
6847
  cursorAdapter: () => cursorAdapter,
6822
6848
  cursorSdkMessageToEvents: () => cursorSdkMessageToEvents,
6823
6849
  decodeBrightsyTarget: () => decodeBrightsyTarget,
@@ -6827,6 +6853,7 @@ __export(agents_exports, {
6827
6853
  getAdapter: () => getAdapter,
6828
6854
  getAgentSetupInfo: () => getAgentSetupInfo,
6829
6855
  installAgent: () => installAgent,
6856
+ isConductorBundledCli: () => isConductorBundledCli,
6830
6857
  isCursorAutoModel: () => isCursorAutoModel,
6831
6858
  isOrchestratorCapableAgent: () => isOrchestratorCapableAgent,
6832
6859
  isSessionQuotaLimit: () => isSessionQuotaLimit,
@@ -9719,24 +9746,25 @@ function capPatch(patch, maxHunk) {
9719
9746
 
9720
9747
  \u2026 truncated (${patch.length - maxHunk} more chars)`;
9721
9748
  }
9749
+ var mergeBaseCache = /* @__PURE__ */ new Map();
9750
+ var MERGE_BASE_TTL_MS = 45e3;
9722
9751
  async function resolveMergeBase(worktreePath, base, repoPath) {
9723
9752
  const baseRef = await resolveDiffBaseRef(worktreePath, base, repoPath);
9753
+ const cacheKey = `${worktreePath}\0${baseRef}`;
9754
+ const hit = mergeBaseCache.get(cacheKey);
9755
+ if (hit && Date.now() - hit.at < MERGE_BASE_TTL_MS) return hit.value;
9724
9756
  const { stdout } = await git(["merge-base", baseRef, "HEAD"], worktreePath, {
9725
9757
  reject: false
9726
9758
  });
9727
- const mb = stdout.trim();
9728
- return mb || baseRef;
9759
+ const mb = stdout.trim() || baseRef;
9760
+ mergeBaseCache.set(cacheKey, { at: Date.now(), value: mb });
9761
+ return mb;
9729
9762
  }
9730
9763
  async function countUnpushedCommits(worktreePath) {
9731
9764
  const head = await git(["rev-parse", "--abbrev-ref", "HEAD"], worktreePath, {
9732
9765
  reject: false
9733
9766
  });
9734
9767
  const branch = head.stdout.trim();
9735
- if (branch && branch !== "HEAD") {
9736
- await git(["fetch", "origin", branch, "--quiet"], worktreePath, {
9737
- reject: false
9738
- });
9739
- }
9740
9768
  const upstream = await git(
9741
9769
  ["rev-list", "--count", "@{upstream}..HEAD"],
9742
9770
  worktreePath,
@@ -9769,11 +9797,37 @@ function splitCombinedDiff(stdout) {
9769
9797
  }
9770
9798
  return map;
9771
9799
  }
9800
+ function syntheticAddPatch(path, content, maxHunk) {
9801
+ const lines = content.split("\n");
9802
+ const body = lines.map((l) => `+${l}`).join("\n");
9803
+ const header = `diff --git a/${path} b/${path}
9804
+ new file mode 100644
9805
+ --- /dev/null
9806
+ +++ b/${path}
9807
+ @@ -0,0 +1,${lines.length} @@
9808
+ `;
9809
+ return {
9810
+ path,
9811
+ status: "A",
9812
+ patch: capPatch(`${header}${body}`, maxHunk),
9813
+ additions: lines.length,
9814
+ deletions: 0
9815
+ };
9816
+ }
9772
9817
  async function untrackedPatch(worktreePath, path, maxHunk) {
9818
+ const abs = (0, import_node_path20.join)(worktreePath, path);
9819
+ try {
9820
+ const st = (0, import_node_fs25.statSync)(abs);
9821
+ if (st.isFile() && st.size > maxHunk) {
9822
+ const buf = (0, import_node_fs25.readFileSync)(abs).subarray(0, maxHunk);
9823
+ return syntheticAddPatch(path, buf.toString("utf8"), maxHunk);
9824
+ }
9825
+ } catch {
9826
+ }
9773
9827
  const { stdout: patch } = await git(
9774
- ["diff", "--no-index", "--", "/dev/null", path],
9828
+ ["diff", "--no-ext-diff", "--no-color", "--no-index", "--", "/dev/null", path],
9775
9829
  worktreePath,
9776
- { reject: false }
9830
+ { reject: false, timeoutMs: 4e3 }
9777
9831
  );
9778
9832
  const normalized = patch.replace(/^diff --git a\/.+? b\/.+?$/m, `diff --git a/${path} b/${path}`).replace(/^--- .*$/m, "--- /dev/null").replace(/^\+\+\+ .*$/m, `+++ b/${path}`);
9779
9833
  const addLines = normalized.split("\n").filter((l) => l.startsWith("+") && !l.startsWith("+++")).length;
@@ -9785,22 +9839,97 @@ async function untrackedPatch(worktreePath, path, maxHunk) {
9785
9839
  deletions: 0
9786
9840
  };
9787
9841
  }
9788
- async function listUntrackedPaths(worktreePath) {
9789
- const { stdout } = await git(
9790
- ["ls-files", "-z", "--others", "--exclude-standard"],
9791
- worktreePath,
9792
- { reject: false }
9793
- );
9842
+ async function listUntrackedPaths(worktreePath, path) {
9843
+ const args = ["ls-files", "-z", "--others", "--exclude-standard"];
9844
+ if (path) args.push("--", path);
9845
+ const { stdout } = await git(args, worktreePath, { reject: false });
9794
9846
  return stdout.split("\0").filter(Boolean).filter((p) => !isSideboardScratchPath(p));
9795
9847
  }
9796
- async function listUntrackedFiles(worktreePath, maxHunk) {
9797
- const paths = await listUntrackedPaths(worktreePath);
9798
- if (paths.length === 0) return [];
9799
- return Promise.all(paths.map((path) => untrackedPatch(worktreePath, path, maxHunk)));
9848
+ async function listUntrackedFiles(worktreePath, maxHunk, paths) {
9849
+ const list = paths ?? await listUntrackedPaths(worktreePath);
9850
+ if (list.length === 0) return [];
9851
+ return Promise.all(list.map((path) => untrackedPatch(worktreePath, path, maxHunk)));
9852
+ }
9853
+ function untrackedStubs(paths) {
9854
+ return paths.map((path) => ({
9855
+ path,
9856
+ status: "A",
9857
+ patch: "",
9858
+ additions: 0,
9859
+ deletions: 0
9860
+ }));
9861
+ }
9862
+ function pathspec(path) {
9863
+ return path ? ["--", path] : [];
9864
+ }
9865
+ function gitDiff(args, cwd, opts) {
9866
+ return git(["diff", "--no-ext-diff", "--no-color", ...args], cwd, {
9867
+ reject: false,
9868
+ timeoutMs: opts?.timeoutMs
9869
+ });
9870
+ }
9871
+ function fileFromPatch(path, patch, maxHunk) {
9872
+ let status = "M";
9873
+ if (/^new file mode /m.test(patch) || /^--- \/dev\/null/m.test(patch)) {
9874
+ status = "A";
9875
+ } else if (/^deleted file mode /m.test(patch) || /^\+\+\+ \/dev\/null/m.test(patch)) {
9876
+ status = "D";
9877
+ } else if (/^rename from /m.test(patch)) {
9878
+ status = "R";
9879
+ }
9880
+ const additions = patch.split("\n").filter((l) => l.startsWith("+") && !l.startsWith("+++")).length;
9881
+ const deletions = patch.split("\n").filter((l) => l.startsWith("-") && !l.startsWith("---")).length;
9882
+ return {
9883
+ path,
9884
+ status,
9885
+ patch: capPatch(patch, maxHunk),
9886
+ additions,
9887
+ deletions
9888
+ };
9889
+ }
9890
+ async function gitDiffParts(worktreePath, diffArgs, includePatches, path) {
9891
+ const extra = pathspec(path);
9892
+ if (includePatches) {
9893
+ const diff = await gitDiff([...diffArgs, ...extra], worktreePath, {
9894
+ timeoutMs: 4e3
9895
+ });
9896
+ return { nameStatus: "", numstat: "", combinedDiff: diff.stdout };
9897
+ }
9898
+ const [ns, num2] = await Promise.all([
9899
+ gitDiff(["--name-status", ...diffArgs, ...extra], worktreePath),
9900
+ gitDiff(["--numstat", ...diffArgs, ...extra], worktreePath)
9901
+ ]);
9902
+ return {
9903
+ nameStatus: ns.stdout,
9904
+ numstat: num2.stdout,
9905
+ combinedDiff: ""
9906
+ };
9907
+ }
9908
+ function formatStat(files) {
9909
+ if (files.length === 0) return "(no changes)";
9910
+ const additions = files.reduce((n2, f) => n2 + (f.additions ?? 0), 0);
9911
+ const deletions = files.reduce((n2, f) => n2 + (f.deletions ?? 0), 0);
9912
+ const n = files.length;
9913
+ return `${n} file${n === 1 ? "" : "s"} changed, ${additions} insertions(+), ${deletions} deletions(-)`;
9914
+ }
9915
+ function emptyScopeStats() {
9916
+ const z2 = emptyStat();
9917
+ return {
9918
+ commits: z2,
9919
+ uncommitted: z2,
9920
+ staged: z2,
9921
+ unstaged: z2,
9922
+ last_turn: z2
9923
+ };
9800
9924
  }
9801
9925
  function filesFromDiff(nameStatus, numstat, combinedDiff, maxHunk) {
9802
- const counts = parseNumstat(numstat);
9803
9926
  const patches = splitCombinedDiff(combinedDiff);
9927
+ if (!nameStatus.trim() && patches.size > 0) {
9928
+ return [...patches.entries()].map(
9929
+ ([path, patch]) => fileFromPatch(path, patch, maxHunk)
9930
+ );
9931
+ }
9932
+ const counts = parseNumstat(numstat);
9804
9933
  const files = [];
9805
9934
  for (const { status, path } of parseNameStatus(nameStatus)) {
9806
9935
  const n = counts.get(path);
@@ -9822,11 +9951,11 @@ async function computeScopeStats(worktreePath, mergeBase, lastTurnBase, untracke
9822
9951
  { stdout: unstagedNum },
9823
9952
  lastTurnNum
9824
9953
  ] = await Promise.all([
9825
- mergeBase ? git(["diff", "--numstat", mergeBase], worktreePath, { reject: false }) : Promise.resolve({ stdout: "" }),
9826
- git(["diff", "--numstat", "HEAD"], worktreePath, { reject: false }),
9827
- git(["diff", "--numstat", "--cached"], worktreePath, { reject: false }),
9828
- git(["diff", "--numstat"], worktreePath, { reject: false }),
9829
- lastTurnBase ? git(["diff", "--numstat", lastTurnBase], worktreePath, { reject: false }) : Promise.resolve({ stdout: "" })
9954
+ mergeBase ? gitDiff(["--numstat", mergeBase], worktreePath) : Promise.resolve({ stdout: "" }),
9955
+ gitDiff(["--numstat", "HEAD"], worktreePath),
9956
+ gitDiff(["--numstat", "--cached"], worktreePath),
9957
+ gitDiff(["--numstat"], worktreePath),
9958
+ lastTurnBase ? gitDiff(["--numstat", lastTurnBase], worktreePath) : Promise.resolve({ stdout: "" })
9830
9959
  ]);
9831
9960
  return {
9832
9961
  commits: mergeBase ? statFromNumstat(commitsNum, untrackedCount) : emptyStat(),
@@ -9845,7 +9974,7 @@ async function listBranchCommits(worktreePath, repoPath, opts) {
9845
9974
  if (head.exitCode !== 0) return [];
9846
9975
  let base = opts?.base;
9847
9976
  try {
9848
- base = base ?? await resolveDefaultBranch(repoPath);
9977
+ base = base ?? await resolveDefaultBranch(repoPath, { network: false });
9849
9978
  } catch {
9850
9979
  base = "HEAD";
9851
9980
  }
@@ -9875,7 +10004,121 @@ async function listBranchCommits(worktreePath, repoPath, opts) {
9875
10004
  }
9876
10005
  return commits;
9877
10006
  }
10007
+ var SHA_RE = /^[0-9a-f]{7,40}$/i;
10008
+ function emptyPathDiff(scope, base, hasLastTurnBase, commitSha = null) {
10009
+ return {
10010
+ scope,
10011
+ commitSha: scope === "commits" ? commitSha : null,
10012
+ base,
10013
+ files: [],
10014
+ stat: formatStat([]),
10015
+ dirty: false,
10016
+ unpushed: 0,
10017
+ hasLastTurnBase,
10018
+ commits: [],
10019
+ scopeStats: emptyScopeStats()
10020
+ };
10021
+ }
10022
+ async function getPathDiff(worktreePath, repoPath, opts) {
10023
+ const { path, scope, commitSha, lastTurnBase, hasLastTurnBase, includePatches, maxHunk } = opts;
10024
+ if (scope === "last_turn" && !lastTurnBase) {
10025
+ return emptyPathDiff(scope, "last turn", false);
10026
+ }
10027
+ let diffArgs;
10028
+ let labelBase;
10029
+ if (scope === "staged") {
10030
+ diffArgs = ["--cached"];
10031
+ labelBase = "staged";
10032
+ } else if (scope === "unstaged") {
10033
+ diffArgs = [];
10034
+ labelBase = "unstaged";
10035
+ } else if (scope === "uncommitted") {
10036
+ diffArgs = ["HEAD"];
10037
+ labelBase = "HEAD";
10038
+ } else if (scope === "last_turn") {
10039
+ diffArgs = [lastTurnBase];
10040
+ labelBase = "last turn";
10041
+ } else if (scope === "commits" && commitSha) {
10042
+ diffArgs = [`${commitSha}^!`];
10043
+ labelBase = commitSha.slice(0, 7);
10044
+ } else if (scope === "commits" && opts.base && SHA_RE.test(opts.base)) {
10045
+ diffArgs = [opts.base];
10046
+ labelBase = opts.base.slice(0, 7);
10047
+ } else if (scope === "commits") {
10048
+ let base = opts.base;
10049
+ try {
10050
+ base = base ?? await resolveDefaultBranch(repoPath, { network: false });
10051
+ } catch {
10052
+ base = "HEAD";
10053
+ }
10054
+ const mergeBase = await resolveMergeBase(worktreePath, base, repoPath);
10055
+ diffArgs = [mergeBase];
10056
+ labelBase = mergeBase;
10057
+ } else {
10058
+ diffArgs = ["HEAD"];
10059
+ labelBase = "HEAD";
10060
+ }
10061
+ const diff = await gitDiff(
10062
+ includePatches ? [...diffArgs, "--", path] : ["--name-status", ...diffArgs, "--", path],
10063
+ worktreePath,
10064
+ { timeoutMs: 4e3 }
10065
+ );
10066
+ const filesMap = /* @__PURE__ */ new Map();
10067
+ if (includePatches) {
10068
+ const parsed = filesFromDiff("", "", diff.stdout, maxHunk);
10069
+ for (const file of parsed) filesMap.set(file.path, file);
10070
+ if (diff.stdout.trim() && !filesMap.has(path)) {
10071
+ filesMap.set(path, fileFromPatch(path, diff.stdout, maxHunk));
10072
+ }
10073
+ } else {
10074
+ for (const file of filesFromDiff(diff.stdout, "", "", maxHunk)) {
10075
+ filesMap.set(file.path, file);
10076
+ }
10077
+ }
10078
+ if (filesMap.size === 0) {
10079
+ const maybe = await listUntrackedPaths(worktreePath, path);
10080
+ if (maybe.length > 0) {
10081
+ const extra = includePatches ? await listUntrackedFiles(worktreePath, maxHunk, maybe) : untrackedStubs(maybe);
10082
+ for (const file of extra) filesMap.set(file.path, file);
10083
+ }
10084
+ }
10085
+ const files = [...filesMap.values()].sort((a, b) => a.path.localeCompare(b.path));
10086
+ return {
10087
+ scope,
10088
+ commitSha: scope === "commits" ? commitSha : null,
10089
+ base: labelBase,
10090
+ files,
10091
+ stat: formatStat(files),
10092
+ dirty: files.length > 0,
10093
+ unpushed: 0,
10094
+ hasLastTurnBase,
10095
+ commits: [],
10096
+ scopeStats: emptyScopeStats(),
10097
+ mergeBase: SHA_RE.test(diffArgs[0] ?? "") ? diffArgs[0] : null
10098
+ };
10099
+ }
9878
10100
  async function getDiff(worktreePath, repoPath, opts) {
10101
+ const maxHunk = opts?.maxHunkChars ?? 8e3;
10102
+ const scope = opts?.scope ?? "commits";
10103
+ const lastTurnBase = opts?.lastTurnBase?.trim() || null;
10104
+ const hasLastTurnBase = Boolean(lastTurnBase);
10105
+ const commitSha = opts?.commitSha?.trim() || null;
10106
+ const includePatches = opts?.includePatches ?? true;
10107
+ const pathFilter = opts?.path?.trim() || null;
10108
+ const fileOnly = Boolean(pathFilter);
10109
+ const includeMeta = (opts?.includeMeta ?? true) && !fileOnly;
10110
+ if (fileOnly && pathFilter && !includeMeta) {
10111
+ return getPathDiff(worktreePath, repoPath, {
10112
+ path: pathFilter,
10113
+ scope,
10114
+ commitSha,
10115
+ lastTurnBase,
10116
+ hasLastTurnBase,
10117
+ base: opts?.base,
10118
+ includePatches,
10119
+ maxHunk
10120
+ });
10121
+ }
9879
10122
  const status = await inspectGitWorktree(worktreePath);
9880
10123
  if (status === "missing_worktree") {
9881
10124
  throw new Error("Worktree not found");
@@ -9883,18 +10126,19 @@ async function getDiff(worktreePath, repoPath, opts) {
9883
10126
  if (status === "not_git") {
9884
10127
  throw new Error("Not a Git repository");
9885
10128
  }
9886
- const maxHunk = opts?.maxHunkChars ?? 8e3;
9887
- const scope = opts?.scope ?? "commits";
9888
- const lastTurnBase = opts?.lastTurnBase?.trim() || null;
9889
- const hasLastTurnBase = Boolean(lastTurnBase);
9890
- const commitSha = opts?.commitSha?.trim() || null;
9891
- const head = await git(["rev-parse", "--verify", "HEAD"], worktreePath, {
10129
+ const wantUntracked = opts?.includeUntracked !== void 0 ? opts.includeUntracked : !fileOnly && (scope === "commits" && !commitSha || scope === "uncommitted" || scope === "unstaged" || scope === "last_turn");
10130
+ const needsMergeBase = scope === "commits" && !commitSha || includeMeta;
10131
+ const headP = git(["rev-parse", "--verify", "HEAD"], worktreePath, {
9892
10132
  reject: false
9893
10133
  });
10134
+ const untrackedP = wantUntracked ? listUntrackedPaths(worktreePath, pathFilter) : Promise.resolve([]);
10135
+ const head = await headP;
9894
10136
  const hasHead = head.exitCode === 0;
9895
- const untrackedPaths = await listUntrackedPaths(worktreePath);
9896
10137
  if (!hasHead) {
9897
- const files2 = scope === "staged" ? [] : (await listUntrackedFiles(worktreePath, maxHunk)).sort(
10138
+ const untrackedPaths2 = await untrackedP;
10139
+ const files2 = scope === "staged" ? [] : includePatches ? (await listUntrackedFiles(worktreePath, maxHunk, untrackedPaths2)).sort(
10140
+ (a, b) => a.path.localeCompare(b.path)
10141
+ ) : untrackedStubs(untrackedPaths2).sort(
9898
10142
  (a, b) => a.path.localeCompare(b.path)
9899
10143
  );
9900
10144
  const untrackedStat = {
@@ -9908,7 +10152,7 @@ async function getDiff(worktreePath, repoPath, opts) {
9908
10152
  commitSha: null,
9909
10153
  base: "(no commits)",
9910
10154
  files: files2,
9911
- stat: files2.length ? files2.map((f) => ` ${f.path} | untracked`).join("\n") : "(no changes)",
10155
+ stat: formatStat(files2),
9912
10156
  dirty: files2.length > 0,
9913
10157
  unpushed: 0,
9914
10158
  hasLastTurnBase,
@@ -9922,137 +10166,122 @@ async function getDiff(worktreePath, repoPath, opts) {
9922
10166
  }
9923
10167
  };
9924
10168
  }
9925
- let base = opts?.base;
9926
- try {
9927
- base = base ?? await resolveDefaultBranch(repoPath);
9928
- } catch {
9929
- base = "HEAD";
10169
+ let mergeBase = "HEAD";
10170
+ let baseLabel = "HEAD";
10171
+ if (needsMergeBase) {
10172
+ let base = opts?.base;
10173
+ try {
10174
+ base = base ?? await resolveDefaultBranch(repoPath, { network: false });
10175
+ } catch {
10176
+ base = "HEAD";
10177
+ }
10178
+ mergeBase = await resolveMergeBase(worktreePath, base, repoPath);
10179
+ baseLabel = await resolveDiffBaseRef(worktreePath, base, repoPath);
9930
10180
  }
9931
- base = await resolveDiffBaseRef(worktreePath, base, repoPath);
9932
- if (base.startsWith("origin/")) {
9933
- const short = base.slice("origin/".length);
9934
- await git(["fetch", "origin", short, "--quiet"], worktreePath, {
9935
- reject: false
9936
- });
9937
- base = await resolveDiffBaseRef(worktreePath, short, repoPath);
10181
+ const includeUntracked = wantUntracked;
10182
+ const metaPromise = includeMeta ? Promise.all([
10183
+ computeScopeStats(
10184
+ worktreePath,
10185
+ mergeBase,
10186
+ lastTurnBase,
10187
+ // Cheap: don't wait on the untracked walk just to pad the badge.
10188
+ 0
10189
+ ),
10190
+ listBranchCommits(worktreePath, repoPath, { base: baseLabel }),
10191
+ isDirty(worktreePath),
10192
+ countUnpushedCommits(worktreePath)
10193
+ ]).then(([scopeStats, commits, dirty, unpushed]) => ({
10194
+ scopeStats,
10195
+ commits,
10196
+ dirty,
10197
+ unpushed
10198
+ })) : Promise.resolve({
10199
+ scopeStats: emptyScopeStats(),
10200
+ commits: [],
10201
+ dirty: false,
10202
+ unpushed: 0
10203
+ });
10204
+ if (scope === "last_turn" && !lastTurnBase) {
10205
+ const meta2 = await metaPromise;
10206
+ return {
10207
+ scope,
10208
+ commitSha: null,
10209
+ base: "last turn",
10210
+ files: [],
10211
+ stat: "(no last agent turn)",
10212
+ dirty: meta2.dirty,
10213
+ unpushed: meta2.unpushed,
10214
+ hasLastTurnBase: false,
10215
+ commits: meta2.commits,
10216
+ scopeStats: meta2.scopeStats
10217
+ };
9938
10218
  }
9939
- const mergeBase = await resolveMergeBase(worktreePath, base, repoPath);
9940
- const scopeStats = await computeScopeStats(
9941
- worktreePath,
9942
- mergeBase,
9943
- lastTurnBase,
9944
- untrackedPaths.length
9945
- );
9946
- const includeUntracked = scope === "commits" && !commitSha || scope === "uncommitted" || scope === "unstaged" || scope === "last_turn";
9947
- let nameStatus = "";
9948
- let numstat = "";
9949
- let combinedDiff = "";
9950
- let labelBase = base;
10219
+ let diffArgs;
10220
+ let labelBase = baseLabel;
9951
10221
  if (scope === "staged") {
9952
- const [ns, num2, diff] = await Promise.all([
9953
- git(["diff", "--name-status", "--cached"], worktreePath, { reject: false }),
9954
- git(["diff", "--numstat", "--cached"], worktreePath, { reject: false }),
9955
- git(["diff", "--cached"], worktreePath, { reject: false })
9956
- ]);
9957
- nameStatus = ns.stdout;
9958
- numstat = num2.stdout;
9959
- combinedDiff = diff.stdout;
10222
+ diffArgs = ["--cached"];
9960
10223
  labelBase = "staged";
9961
10224
  } else if (scope === "unstaged") {
9962
- const [ns, num2, diff] = await Promise.all([
9963
- git(["diff", "--name-status"], worktreePath, { reject: false }),
9964
- git(["diff", "--numstat"], worktreePath, { reject: false }),
9965
- git(["diff"], worktreePath, { reject: false })
9966
- ]);
9967
- nameStatus = ns.stdout;
9968
- numstat = num2.stdout;
9969
- combinedDiff = diff.stdout;
10225
+ diffArgs = [];
9970
10226
  labelBase = "unstaged";
9971
10227
  } else if (scope === "uncommitted") {
9972
- const [ns, num2, diff] = await Promise.all([
9973
- git(["diff", "--name-status", "HEAD"], worktreePath, { reject: false }),
9974
- git(["diff", "--numstat", "HEAD"], worktreePath, { reject: false }),
9975
- git(["diff", "HEAD"], worktreePath, { reject: false })
9976
- ]);
9977
- nameStatus = ns.stdout;
9978
- numstat = num2.stdout;
9979
- combinedDiff = diff.stdout;
10228
+ diffArgs = ["HEAD"];
9980
10229
  labelBase = "HEAD";
9981
10230
  } else if (scope === "last_turn") {
9982
- if (!lastTurnBase) {
9983
- const commits2 = await listBranchCommits(worktreePath, repoPath, { base });
9984
- return {
9985
- scope,
9986
- commitSha: null,
9987
- base: "last turn",
9988
- files: [],
9989
- stat: "(no last agent turn)",
9990
- dirty: await isDirty(worktreePath),
9991
- unpushed: await countUnpushedCommits(worktreePath),
9992
- hasLastTurnBase: false,
9993
- commits: commits2,
9994
- scopeStats
9995
- };
9996
- }
9997
- const [ns, num2, diff] = await Promise.all([
9998
- git(["diff", "--name-status", lastTurnBase], worktreePath, { reject: false }),
9999
- git(["diff", "--numstat", lastTurnBase], worktreePath, { reject: false }),
10000
- git(["diff", lastTurnBase], worktreePath, { reject: false })
10001
- ]);
10002
- nameStatus = ns.stdout;
10003
- numstat = num2.stdout;
10004
- combinedDiff = diff.stdout;
10231
+ diffArgs = [lastTurnBase];
10005
10232
  labelBase = "last turn";
10006
10233
  } else if (scope === "commits" && commitSha) {
10007
- const range = `${commitSha}^!`;
10008
- const [ns, num2, diff] = await Promise.all([
10009
- git(["diff", "--name-status", range], worktreePath, { reject: false }),
10010
- git(["diff", "--numstat", range], worktreePath, { reject: false }),
10011
- git(["diff", range], worktreePath, { reject: false })
10012
- ]);
10013
- nameStatus = ns.stdout;
10014
- numstat = num2.stdout;
10015
- combinedDiff = diff.stdout;
10234
+ diffArgs = [`${commitSha}^!`];
10016
10235
  labelBase = commitSha.slice(0, 7);
10017
10236
  } else {
10018
- const [ns, num2, diff] = await Promise.all([
10019
- git(["diff", "--name-status", mergeBase], worktreePath, { reject: false }),
10020
- git(["diff", "--numstat", mergeBase], worktreePath, { reject: false }),
10021
- git(["diff", mergeBase], worktreePath, { reject: false })
10022
- ]);
10023
- nameStatus = ns.stdout;
10024
- numstat = num2.stdout;
10025
- combinedDiff = diff.stdout;
10026
- labelBase = base;
10237
+ diffArgs = [mergeBase];
10238
+ labelBase = baseLabel;
10027
10239
  }
10028
- const commits = await listBranchCommits(worktreePath, repoPath, { base });
10240
+ const partsPromise = gitDiffParts(
10241
+ worktreePath,
10242
+ diffArgs,
10243
+ includePatches,
10244
+ pathFilter
10245
+ );
10246
+ const [parts, meta, untrackedPaths] = await Promise.all([
10247
+ partsPromise,
10248
+ metaPromise,
10249
+ untrackedP
10250
+ ]);
10029
10251
  const filesMap = /* @__PURE__ */ new Map();
10030
- for (const file of filesFromDiff(nameStatus, numstat, combinedDiff, maxHunk)) {
10252
+ for (const file of filesFromDiff(
10253
+ parts.nameStatus,
10254
+ parts.numstat,
10255
+ parts.combinedDiff,
10256
+ maxHunk
10257
+ )) {
10031
10258
  filesMap.set(file.path, file);
10032
10259
  }
10033
10260
  if (includeUntracked) {
10034
- const untrackedFiles = await listUntrackedFiles(worktreePath, maxHunk);
10035
- for (const file of untrackedFiles) {
10261
+ const extra = includePatches ? await listUntrackedFiles(worktreePath, maxHunk, untrackedPaths) : untrackedStubs(untrackedPaths);
10262
+ for (const file of extra) {
10036
10263
  if (!filesMap.has(file.path)) filesMap.set(file.path, file);
10037
10264
  }
10265
+ } else if (fileOnly && pathFilter && !filesMap.has(pathFilter)) {
10266
+ const maybe = await listUntrackedPaths(worktreePath, pathFilter);
10267
+ if (maybe.length > 0) {
10268
+ const extra = includePatches ? await listUntrackedFiles(worktreePath, maxHunk, maybe) : untrackedStubs(maybe);
10269
+ for (const file of extra) filesMap.set(file.path, file);
10270
+ }
10038
10271
  }
10039
10272
  const files = [...filesMap.values()].sort((a, b) => a.path.localeCompare(b.path));
10040
- const { stdout: statOut } = await git(
10041
- scope === "staged" ? ["diff", "--stat", "--cached"] : scope === "unstaged" ? ["diff", "--stat"] : scope === "uncommitted" ? ["diff", "--stat", "HEAD"] : scope === "last_turn" && lastTurnBase ? ["diff", "--stat", lastTurnBase] : scope === "commits" && commitSha ? ["diff", "--stat", `${commitSha}^!`] : ["diff", "--stat", mergeBase],
10042
- worktreePath,
10043
- { reject: false }
10044
- );
10045
10273
  return {
10046
10274
  scope,
10047
10275
  commitSha: scope === "commits" ? commitSha : null,
10048
10276
  base: labelBase,
10049
10277
  files,
10050
- stat: statOut.trim() || (files.length ? `${files.length} file(s)` : "(no changes)"),
10051
- dirty: await isDirty(worktreePath),
10052
- unpushed: await countUnpushedCommits(worktreePath),
10278
+ stat: formatStat(files),
10279
+ dirty: fileOnly || !includeMeta ? files.length > 0 : meta.dirty,
10280
+ unpushed: meta.unpushed,
10053
10281
  hasLastTurnBase,
10054
- commits,
10055
- scopeStats
10282
+ commits: meta.commits,
10283
+ scopeStats: meta.scopeStats,
10284
+ mergeBase: needsMergeBase ? mergeBase : null
10056
10285
  };
10057
10286
  }
10058
10287
  async function listWorktreeFiles(worktreePath, opts) {
@@ -12006,6 +12235,11 @@ var Orchestrator = class {
12006
12235
  return getDiff(thread.worktreePath, thread.repoPath, {
12007
12236
  scope: opts?.scope,
12008
12237
  commitSha: opts?.commitSha,
12238
+ base: opts?.base,
12239
+ includePatches: opts?.includePatches,
12240
+ includeMeta: opts?.includeMeta,
12241
+ includeUntracked: opts?.includeUntracked,
12242
+ path: opts?.path,
12009
12243
  lastTurnBase: this.turnBaselines.get(thread.id) ?? null
12010
12244
  });
12011
12245
  }