@sideboard-ai/core 0.1.67 → 0.1.71

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 (44) hide show
  1. package/dist/{agents-OTBZVTZO.js → agents-GB7XL2LI.js} +8 -7
  2. package/dist/{agents-TS3EI2YI.js → agents-TCAK3MXN.js} +8 -7
  3. package/dist/{app-settings-ONKMFSEJ.js → app-settings-3TLA2EJ7.js} +16 -1
  4. package/dist/{app-settings-GJVZGU3M.js → app-settings-P42Z3VOB.js} +16 -1
  5. package/dist/caffeinate-hold-BEJIYPJ7.js +107 -0
  6. package/dist/caffeinate-hold-KLC6SABD.js +14 -0
  7. package/dist/{chunk-WAFIF22N.js → chunk-3CY6WJ7O.js} +2 -2
  8. package/dist/{chunk-7ZOQY4A7.js → chunk-57ROTAFD.js} +62 -3
  9. package/dist/{chunk-57WWNMFP.js → chunk-7NCIHRAU.js} +22 -4
  10. package/dist/{chunk-FGDJOJ2L.js → chunk-AE5VBOFE.js} +1 -1
  11. package/dist/{chunk-DB3I75FI.js → chunk-BTL7EMGT.js} +4 -4
  12. package/dist/{chunk-4UIYN56C.js → chunk-D4DPQ552.js} +1 -1
  13. package/dist/{chunk-JWZNK3CQ.js → chunk-DGPUAZA4.js} +62 -3
  14. package/dist/{chunk-ZRFAZVGL.js → chunk-H2IXNFQ2.js} +22 -4
  15. package/dist/chunk-JAWEDVVA.js +106 -0
  16. package/dist/chunk-JT7R45JB.js +20 -0
  17. package/dist/{chunk-4BUCGNZS.js → chunk-MF2YMEGD.js} +83 -29
  18. package/dist/{chunk-K7LM4CLD.js → chunk-NG7S3OPX.js} +12 -6
  19. package/dist/chunk-NSTQ6QKD.js +23 -0
  20. package/dist/{chunk-LW5CMQGZ.js → chunk-OE7YBB5X.js} +83 -29
  21. package/dist/{chunk-BPOTVI5J.js → chunk-PTJJIQK7.js} +472 -18
  22. package/dist/{chunk-B4PRRKMA.js → chunk-S25IHL5H.js} +12 -6
  23. package/dist/{chunk-5HTTCYWA.js → chunk-UH57WVFP.js} +2 -2
  24. package/dist/{chunk-M5V4QIWF.js → chunk-VROPG6QF.js} +1 -1
  25. package/dist/{chunk-MHR4OGZ4.js → chunk-W7YOTDVO.js} +493 -18
  26. package/dist/{connected-teams-RUNMOCG5.js → connected-teams-35EIRJCP.js} +2 -2
  27. package/dist/{connected-teams-OMDHTL6A.js → connected-teams-ZRS53OAT.js} +2 -2
  28. package/dist/{coordinator-prompt-6HMTOX4G.js → coordinator-prompt-2J4PIMUF.js} +7 -4
  29. package/dist/{coordinator-prompt-3BSMKZL4.js → coordinator-prompt-QYYG2IBB.js} +7 -4
  30. package/dist/{global-workspace-YZ2NJVLL.js → global-workspace-KHIFQUUM.js} +12 -5
  31. package/dist/{global-workspace-QS7A23CB.js → global-workspace-SVRYNJZA.js} +12 -5
  32. package/dist/index.cjs +4805 -1262
  33. package/dist/index.d.cts +570 -22
  34. package/dist/index.d.ts +570 -22
  35. package/dist/index.js +3084 -340
  36. package/dist/mcp/run-stdio.cjs +2339 -617
  37. package/dist/mcp/run-stdio.js +1289 -290
  38. package/dist/{run-Y2MI3EVV.js → run-A4RHY7HH.js} +1 -1
  39. package/dist/{run-JLD2Y52J.js → run-V2WZUMJA.js} +1 -1
  40. package/dist/{workspaces-EBFW7I7S.js → workspaces-AHFTHVBT.js} +7 -6
  41. package/dist/{workspaces-AIUYUXB7.js → workspaces-SVQIEVIZ.js} +7 -6
  42. package/dist/{worktree-WMDJW2BL.js → worktree-KWXHMKRN.js} +2 -2
  43. package/dist/{worktree-7NBPIVFZ.js → worktree-VOCH3WS3.js} +2 -2
  44. package/package.json +3 -1
@@ -92,13 +92,13 @@ function summarizeTurnStderr(tail, maxChars = 500) {
92
92
  if (joined.length <= maxChars) return joined;
93
93
  return joined.slice(joined.length - maxChars);
94
94
  }
95
- function looksLikeInvalidAgentSession(text) {
96
- const lower = text.trim().toLowerCase();
95
+ function looksLikeInvalidAgentSession(text2) {
96
+ const lower = text2.trim().toLowerCase();
97
97
  if (!lower) return false;
98
98
  return /session not found/.test(lower) || /no conversation found/.test(lower) || /conversation .+ not found/.test(lower) || /thread .+ not found/.test(lower) || /unknown session/.test(lower) || /invalid session/.test(lower) || /session .+ (missing|expired|deleted|gone)/.test(lower) || /cannot resume/.test(lower) || /failed to (load|resume|open) session/.test(lower);
99
99
  }
100
- function looksLikeAgentFailureMessage(text) {
101
- const lower = text.trim().toLowerCase();
100
+ function looksLikeAgentFailureMessage(text2) {
101
+ const lower = text2.trim().toLowerCase();
102
102
  if (!lower) return false;
103
103
  return /you've hit your|hit your (session|weekly|opus) limit|usage limit/.test(lower) || /credit balance is too low|out of credits|insufficient.?quota|quota.?exceeded/.test(lower) || /invalid user api key|invalid api key|not logged in|not authenticated|unauthorized/.test(
104
104
  lower
@@ -1451,8 +1451,8 @@ var init_worktree_labels = __esm({
1451
1451
  });
1452
1452
 
1453
1453
  // src/git/gh-errors.ts
1454
- function isGhRateLimitError(text) {
1455
- return /API rate limit (already )?exceeded/i.test(text) || /rate limit exceeded/i.test(text);
1454
+ function isGhRateLimitError(text2) {
1455
+ return /API rate limit (already )?exceeded/i.test(text2) || /rate limit exceeded/i.test(text2);
1456
1456
  }
1457
1457
  function formatRateLimitResetHint(resetEpochSec, nowMs = Date.now()) {
1458
1458
  const ms = resetEpochSec * 1e3 - nowMs;
@@ -1464,8 +1464,8 @@ function formatRateLimitResetHint(resetEpochSec, nowMs = Date.now()) {
1464
1464
  const hours = Math.ceil(mins / 60);
1465
1465
  return `in about ${hours} hour${hours === 1 ? "" : "s"}`;
1466
1466
  }
1467
- function extractGhErrorDetail(text) {
1468
- const trimmed = text.trim();
1467
+ function extractGhErrorDetail(text2) {
1468
+ const trimmed = text2.trim();
1469
1469
  if (!trimmed) return "";
1470
1470
  const graphql = trimmed.match(/\bGraphQL:\s*(.+)$/im);
1471
1471
  if (graphql?.[1]) return `GraphQL: ${graphql[1].trim()}`;
@@ -1649,7 +1649,7 @@ async function git(args, cwd, opts) {
1649
1649
  prefix.push("-c", `${key}=${value}`);
1650
1650
  }
1651
1651
  }
1652
- return run("git", [...prefix, ...args], {
1652
+ return run("git", ["--no-pager", ...prefix, ...args], {
1653
1653
  cwd,
1654
1654
  reject: opts?.reject,
1655
1655
  timeoutMs: opts?.timeoutMs,
@@ -1685,6 +1685,10 @@ var init_run = __esm({
1685
1685
  });
1686
1686
 
1687
1687
  // src/git/pr-gates.ts
1688
+ function prIsInMergeQueue(gate) {
1689
+ if (gate.isInMergeQueue) return true;
1690
+ return (gate.mergeStateStatus ?? "").toUpperCase() === "QUEUED";
1691
+ }
1688
1692
  function buildMergeGateChecks(gate, opts = {}) {
1689
1693
  const rows = [];
1690
1694
  const base = gate.baseRefName?.trim() || "the base branch";
@@ -1694,7 +1698,8 @@ function buildMergeGateChecks(gate, opts = {}) {
1694
1698
  const review = (gate.reviewDecision ?? "").toUpperCase();
1695
1699
  const suppressBlocked = opts.suppressGenericBlocked !== false;
1696
1700
  const conflicting = mergeable === "CONFLICTING" || mergeState === "DIRTY";
1697
- if (conflicting) {
1701
+ const inQueue = prIsInMergeQueue(gate);
1702
+ if (conflicting && !inQueue) {
1698
1703
  rows.push({
1699
1704
  name: "Merge conflicts",
1700
1705
  state: mergeable === "CONFLICTING" ? "CONFLICTING" : mergeState || "DIRTY",
@@ -1706,6 +1711,18 @@ function buildMergeGateChecks(gate, opts = {}) {
1706
1711
  workflow: "mergeability",
1707
1712
  kind: "mergeability"
1708
1713
  });
1714
+ } else if (inQueue) {
1715
+ rows.push({
1716
+ name: "Merge queue",
1717
+ state: "QUEUED",
1718
+ bucket: "pending",
1719
+ startedAt: null,
1720
+ completedAt: null,
1721
+ link,
1722
+ description: `This pull request is in the GitHub merge queue for ${base}.`,
1723
+ workflow: "mergeability",
1724
+ kind: "mergeability"
1725
+ });
1709
1726
  } else if (mergeState === "BEHIND") {
1710
1727
  rows.push({
1711
1728
  name: "Branch behind",
@@ -2124,7 +2141,7 @@ async function originGhRepoEnv(cwd) {
2124
2141
  const slug = await resolveGithubRepoSlug(cwd);
2125
2142
  return slug ? { GH_REPO: slug } : {};
2126
2143
  }
2127
- async function resolveDefaultBranch(repoPath) {
2144
+ async function resolveDefaultBranch(repoPath, opts) {
2128
2145
  const viaOrigin = await git(
2129
2146
  ["symbolic-ref", "refs/remotes/origin/HEAD"],
2130
2147
  repoPath,
@@ -2133,22 +2150,24 @@ async function resolveDefaultBranch(repoPath) {
2133
2150
  if (viaOrigin.exitCode === 0 && viaOrigin.stdout.trim()) {
2134
2151
  return viaOrigin.stdout.trim().replace(/^refs\/remotes\/origin\//, "");
2135
2152
  }
2136
- const slug = await resolveGithubRepoSlug(repoPath);
2137
- const viaGh = await gh(
2138
- [
2139
- "repo",
2140
- "view",
2141
- ...slug ? ["--repo", slug] : [],
2142
- "--json",
2143
- "defaultBranchRef",
2144
- "--jq",
2145
- ".defaultBranchRef.name"
2146
- ],
2147
- repoPath,
2148
- { reject: false }
2149
- );
2150
- if (viaGh.exitCode === 0 && viaGh.stdout.trim()) {
2151
- return viaGh.stdout.trim();
2153
+ if (opts?.network !== false) {
2154
+ const slug = await resolveGithubRepoSlug(repoPath);
2155
+ const viaGh = await gh(
2156
+ [
2157
+ "repo",
2158
+ "view",
2159
+ ...slug ? ["--repo", slug] : [],
2160
+ "--json",
2161
+ "defaultBranchRef",
2162
+ "--jq",
2163
+ ".defaultBranchRef.name"
2164
+ ],
2165
+ repoPath,
2166
+ { reject: false, timeoutMs: 8e3 }
2167
+ );
2168
+ if (viaGh.exitCode === 0 && viaGh.stdout.trim()) {
2169
+ return viaGh.stdout.trim();
2170
+ }
2152
2171
  }
2153
2172
  for (const candidate of ["main", "master"]) {
2154
2173
  const check = await git(
@@ -2289,10 +2308,21 @@ async function fetchPrMergeGate(cwd, selector, slug) {
2289
2308
  "view",
2290
2309
  selector,
2291
2310
  "--json",
2292
- "mergeable,mergeStateStatus,reviewDecision,baseRefName,url"
2311
+ "mergeable,mergeStateStatus,reviewDecision,baseRefName,url,isInMergeQueue"
2293
2312
  ];
2294
2313
  if (slug) args.push("--repo", slug);
2295
- const { stdout, exitCode, stderr } = await gh(args, cwd, { reject: false });
2314
+ let { stdout, exitCode, stderr } = await gh(args, cwd, { reject: false });
2315
+ if (exitCode !== 0 && isUnknownJsonField(stderr, "isInMergeQueue")) {
2316
+ const retry = [
2317
+ "pr",
2318
+ "view",
2319
+ selector,
2320
+ "--json",
2321
+ "mergeable,mergeStateStatus,reviewDecision,baseRefName,url"
2322
+ ];
2323
+ if (slug) retry.push("--repo", slug);
2324
+ ({ stdout, exitCode, stderr } = await gh(retry, cwd, { reject: false }));
2325
+ }
2296
2326
  if (exitCode !== 0 || !stdout.trim()) {
2297
2327
  if (/no pull requests found/i.test(stderr)) return null;
2298
2328
  return null;
@@ -2304,12 +2334,22 @@ async function fetchPrMergeGate(cwd, selector, slug) {
2304
2334
  mergeStateStatus: typeof view.mergeStateStatus === "string" && view.mergeStateStatus ? view.mergeStateStatus : null,
2305
2335
  reviewDecision: typeof view.reviewDecision === "string" && view.reviewDecision ? view.reviewDecision : null,
2306
2336
  baseRefName: typeof view.baseRefName === "string" && view.baseRefName ? view.baseRefName : null,
2307
- url: typeof view.url === "string" && view.url ? view.url : null
2337
+ url: typeof view.url === "string" && view.url ? view.url : null,
2338
+ isInMergeQueue: parseInMergeQueue(view)
2308
2339
  };
2309
2340
  } catch {
2310
2341
  return null;
2311
2342
  }
2312
2343
  }
2344
+ function isUnknownJsonField(stderr, field) {
2345
+ return new RegExp(`unknown json field[^\\n]*${field}`, "i").test(stderr);
2346
+ }
2347
+ function parseInMergeQueue(view) {
2348
+ return prIsInMergeQueue({
2349
+ isInMergeQueue: view.isInMergeQueue === true,
2350
+ mergeStateStatus: typeof view.mergeStateStatus === "string" ? view.mergeStateStatus : null
2351
+ });
2352
+ }
2313
2353
  async function detectLocalMergeConflicts(cwd, baseRefName) {
2314
2354
  const baseName = (baseRefName?.trim() || await resolveDefaultBranch(cwd)).replace(
2315
2355
  /^origin\//,
@@ -2370,7 +2410,8 @@ async function getPrChecks(cwd, selector) {
2370
2410
  const mergeable = (gate?.mergeable ?? "").toUpperCase();
2371
2411
  const mergeState = (gate?.mergeStateStatus ?? "").toUpperCase();
2372
2412
  const alreadyConflicting = mergeable === "CONFLICTING" || mergeState === "DIRTY";
2373
- const needsLocalProbe = !alreadyConflicting;
2413
+ const inQueue = gate ? prIsInMergeQueue(gate) : false;
2414
+ const needsLocalProbe = !alreadyConflicting && !inQueue;
2374
2415
  if (needsLocalProbe) {
2375
2416
  try {
2376
2417
  const local = await detectLocalMergeConflicts(cwd, gate?.baseRefName ?? null);
@@ -2381,7 +2422,8 @@ async function getPrChecks(cwd, selector) {
2381
2422
  mergeStateStatus: "DIRTY",
2382
2423
  reviewDecision: gate?.reviewDecision ?? null,
2383
2424
  baseRefName: local.base,
2384
- url: gate?.url ?? null
2425
+ url: gate?.url ?? null,
2426
+ isInMergeQueue: false
2385
2427
  };
2386
2428
  const ciFailed2 = ciChecks.some((c) => c.bucket === "fail");
2387
2429
  const review2 = (gate.reviewDecision ?? "").toUpperCase();
@@ -2425,10 +2467,21 @@ async function getPrMeta(cwd, selector) {
2425
2467
  "view",
2426
2468
  selector,
2427
2469
  "--json",
2428
- "number,title,url,state,isDraft,reviewDecision,baseRefName,headRefName"
2470
+ "number,title,url,state,isDraft,reviewDecision,baseRefName,headRefName,isInMergeQueue,mergeStateStatus"
2429
2471
  ];
2430
2472
  if (slug) viewArgs.push("--repo", slug);
2431
- const { stdout, exitCode, stderr } = await gh(viewArgs, cwd, { reject: false });
2473
+ let { stdout, exitCode, stderr } = await gh(viewArgs, cwd, { reject: false });
2474
+ if (exitCode !== 0 && isUnknownJsonField(stderr, "isInMergeQueue")) {
2475
+ const retry = [
2476
+ "pr",
2477
+ "view",
2478
+ selector,
2479
+ "--json",
2480
+ "number,title,url,state,isDraft,reviewDecision,baseRefName,headRefName,mergeStateStatus"
2481
+ ];
2482
+ if (slug) retry.push("--repo", slug);
2483
+ ({ stdout, exitCode, stderr } = await gh(retry, cwd, { reject: false }));
2484
+ }
2432
2485
  if (exitCode !== 0 || !stdout.trim()) {
2433
2486
  if (/no pull requests found/i.test(stderr)) return null;
2434
2487
  return null;
@@ -2443,7 +2496,8 @@ async function getPrMeta(cwd, selector) {
2443
2496
  isDraft: Boolean(view.isDraft),
2444
2497
  reviewDecision: typeof view.reviewDecision === "string" && view.reviewDecision ? view.reviewDecision : null,
2445
2498
  baseRefName: String(view.baseRefName ?? ""),
2446
- headRefName: String(view.headRefName ?? "")
2499
+ headRefName: String(view.headRefName ?? ""),
2500
+ isInMergeQueue: parseInMergeQueue(view)
2447
2501
  };
2448
2502
  } catch {
2449
2503
  return null;
@@ -2800,11 +2854,11 @@ async function listWorktrees(repoPath) {
2800
2854
  return entries;
2801
2855
  }
2802
2856
  async function isDirty(worktreePath) {
2803
- const { stdout } = await git(["status", "--porcelain", "-uall"], worktreePath);
2857
+ const { stdout } = await git(["status", "--porcelain"], worktreePath);
2804
2858
  for (const line of stdout.split("\n")) {
2805
2859
  if (!line.trim()) continue;
2806
2860
  const rel = porcelainStatusPath(line);
2807
- if (!rel || isSideboardScratchPath(rel)) continue;
2861
+ if (!rel || isSideboardScratchPath(rel) || rel === ".sideboard") continue;
2808
2862
  return true;
2809
2863
  }
2810
2864
  return false;
@@ -3048,6 +3102,208 @@ var init_worktree = __esm({
3048
3102
  }
3049
3103
  });
3050
3104
 
3105
+ // src/store/private-file.ts
3106
+ function writePrivateFile(path, contents) {
3107
+ (0, import_node_fs6.mkdirSync)((0, import_node_path6.dirname)(path), { recursive: true });
3108
+ (0, import_node_fs6.writeFileSync)(path, contents, { encoding: "utf8", mode: PRIVATE_FILE_MODE });
3109
+ chmodOwnerOnly(path);
3110
+ }
3111
+ function chmodOwnerOnly(path) {
3112
+ try {
3113
+ (0, import_node_fs6.chmodSync)(path, PRIVATE_FILE_MODE);
3114
+ } catch {
3115
+ }
3116
+ }
3117
+ var import_node_fs6, import_node_path6, PRIVATE_FILE_MODE;
3118
+ var init_private_file = __esm({
3119
+ "src/store/private-file.ts"() {
3120
+ "use strict";
3121
+ import_node_fs6 = require("fs");
3122
+ import_node_path6 = require("path");
3123
+ PRIVATE_FILE_MODE = 384;
3124
+ }
3125
+ });
3126
+
3127
+ // src/store/secure-file.ts
3128
+ function persistVaultKeyInKeychain(key) {
3129
+ if (process.platform !== "darwin" || key.length !== 32) return;
3130
+ try {
3131
+ (0, import_node_child_process2.execFileSync)(
3132
+ "security",
3133
+ [
3134
+ "add-generic-password",
3135
+ "-s",
3136
+ KEYCHAIN_SERVICE,
3137
+ "-a",
3138
+ KEYCHAIN_ACCOUNT,
3139
+ "-w",
3140
+ key.toString("hex"),
3141
+ "-U"
3142
+ ],
3143
+ { encoding: "utf8", timeout: 8e3, stdio: ["ignore", "pipe", "pipe"] }
3144
+ );
3145
+ } catch {
3146
+ }
3147
+ }
3148
+ function hexKey(value) {
3149
+ const trimmed = value?.trim() ?? "";
3150
+ if (!/^[0-9a-f]{64}$/i.test(trimmed)) return null;
3151
+ return Buffer.from(trimmed, "hex");
3152
+ }
3153
+ function keychainKey(create) {
3154
+ if (process.platform !== "darwin") return null;
3155
+ try {
3156
+ const out = (0, import_node_child_process2.execFileSync)(
3157
+ "security",
3158
+ ["find-generic-password", "-s", KEYCHAIN_SERVICE, "-a", KEYCHAIN_ACCOUNT, "-w"],
3159
+ { encoding: "utf8", timeout: 5e3, stdio: ["ignore", "pipe", "pipe"] }
3160
+ ).trim();
3161
+ const key2 = hexKey(out);
3162
+ if (key2) return key2;
3163
+ } catch {
3164
+ }
3165
+ if (!create) return null;
3166
+ const key = (0, import_node_crypto2.randomBytes)(32);
3167
+ persistVaultKeyInKeychain(key);
3168
+ return key;
3169
+ }
3170
+ function resolveVaultKey() {
3171
+ if (process.env.SIDEBOARD_SECRET_VAULT === "plain") return null;
3172
+ if (injectedKey) return injectedKey;
3173
+ const fromEnv = hexKey(process.env.SIDEBOARD_VAULT_KEY);
3174
+ if (fromEnv) return fromEnv;
3175
+ if (process.env.VITEST) return null;
3176
+ return keychainKey(true);
3177
+ }
3178
+ function isEnvelope(value) {
3179
+ if (!value || typeof value !== "object") return false;
3180
+ const o = value;
3181
+ return o.v === 1 && o.alg === ALG && typeof o.iv === "string" && typeof o.tag === "string" && typeof o.data === "string";
3182
+ }
3183
+ function encryptJson(value, key) {
3184
+ const iv = (0, import_node_crypto2.randomBytes)(12);
3185
+ const cipher = (0, import_node_crypto2.createCipheriv)(ALG, key, iv);
3186
+ const plaintext = Buffer.from(`${JSON.stringify(value)}
3187
+ `, "utf8");
3188
+ const data = Buffer.concat([cipher.update(plaintext), cipher.final()]);
3189
+ const tag = cipher.getAuthTag();
3190
+ return {
3191
+ v: 1,
3192
+ alg: ALG,
3193
+ iv: iv.toString("base64"),
3194
+ tag: tag.toString("base64"),
3195
+ data: data.toString("base64")
3196
+ };
3197
+ }
3198
+ function decryptEnvelope(envelope, key) {
3199
+ const decipher = (0, import_node_crypto2.createDecipheriv)(ALG, key, Buffer.from(envelope.iv, "base64"));
3200
+ decipher.setAuthTag(Buffer.from(envelope.tag, "base64"));
3201
+ const plaintext = Buffer.concat([
3202
+ decipher.update(Buffer.from(envelope.data, "base64")),
3203
+ decipher.final()
3204
+ ]);
3205
+ return JSON.parse(plaintext.toString("utf8"));
3206
+ }
3207
+ function readSecureJson(path) {
3208
+ if (!(0, import_node_fs7.existsSync)(path)) return null;
3209
+ chmodOwnerOnly(path);
3210
+ let parsed;
3211
+ try {
3212
+ parsed = JSON.parse((0, import_node_fs7.readFileSync)(path, "utf8"));
3213
+ } catch {
3214
+ return null;
3215
+ }
3216
+ if (!isEnvelope(parsed)) return parsed;
3217
+ const key = resolveVaultKey();
3218
+ if (!key) {
3219
+ throw new Error(
3220
+ "Encrypted Sideboard secrets need a vault key (desktop Keychain / safeStorage, or SIDEBOARD_VAULT_KEY)."
3221
+ );
3222
+ }
3223
+ return decryptEnvelope(parsed, key);
3224
+ }
3225
+ function writeSecureJson(path, value) {
3226
+ const key = resolveVaultKey();
3227
+ const body = key ? encryptJson(value, key) : value;
3228
+ writePrivateFile(path, `${JSON.stringify(body, null, 2)}
3229
+ `);
3230
+ }
3231
+ function isSecureFileEncrypted(path) {
3232
+ if (!(0, import_node_fs7.existsSync)(path)) return false;
3233
+ try {
3234
+ return isEnvelope(JSON.parse((0, import_node_fs7.readFileSync)(path, "utf8")));
3235
+ } catch {
3236
+ return false;
3237
+ }
3238
+ }
3239
+ var import_node_child_process2, import_node_crypto2, import_node_fs7, KEYCHAIN_SERVICE, KEYCHAIN_ACCOUNT, ALG, injectedKey;
3240
+ var init_secure_file = __esm({
3241
+ "src/store/secure-file.ts"() {
3242
+ "use strict";
3243
+ import_node_child_process2 = require("child_process");
3244
+ import_node_crypto2 = require("crypto");
3245
+ import_node_fs7 = require("fs");
3246
+ init_private_file();
3247
+ KEYCHAIN_SERVICE = "ai.sideboard.app";
3248
+ KEYCHAIN_ACCOUNT = "secret-vault-v1";
3249
+ ALG = "aes-256-gcm";
3250
+ injectedKey = null;
3251
+ }
3252
+ });
3253
+
3254
+ // src/store/secret-vault.ts
3255
+ function secretVaultPath() {
3256
+ return (0, import_node_path7.join)(appDataDir(), "secrets.json");
3257
+ }
3258
+ function loadSecretVault() {
3259
+ const parsed = readSecureJson(secretVaultPath());
3260
+ if (!parsed || typeof parsed !== "object") return {};
3261
+ return normalizeVault(parsed);
3262
+ }
3263
+ function saveSecretVault(vault) {
3264
+ writeSecureJson(secretVaultPath(), normalizeVault(vault));
3265
+ }
3266
+ function normalizeVault(raw) {
3267
+ const out = {};
3268
+ if (typeof raw.linearApiKey === "string" && raw.linearApiKey.trim()) {
3269
+ out.linearApiKey = raw.linearApiKey.trim();
3270
+ }
3271
+ if (typeof raw.linearAccessToken === "string" && raw.linearAccessToken.trim()) {
3272
+ out.linearAccessToken = raw.linearAccessToken.trim();
3273
+ }
3274
+ if (typeof raw.linearRefreshToken === "string" && raw.linearRefreshToken.trim()) {
3275
+ out.linearRefreshToken = raw.linearRefreshToken.trim();
3276
+ }
3277
+ if (typeof raw.linearClientSecret === "string" && raw.linearClientSecret.trim()) {
3278
+ out.linearClientSecret = raw.linearClientSecret.trim();
3279
+ }
3280
+ if (typeof raw.slackClientSecret === "string" && raw.slackClientSecret.trim()) {
3281
+ out.slackClientSecret = raw.slackClientSecret.trim();
3282
+ }
3283
+ if (typeof raw.slackAppToken === "string" && raw.slackAppToken.trim()) {
3284
+ out.slackAppToken = raw.slackAppToken.trim();
3285
+ }
3286
+ if (raw.environment && typeof raw.environment === "object") {
3287
+ const environment = {};
3288
+ for (const [key, value] of Object.entries(raw.environment)) {
3289
+ const k = key.trim();
3290
+ const v = typeof value === "string" ? value.trim() : "";
3291
+ if (k && v) environment[k] = v;
3292
+ }
3293
+ if (Object.keys(environment).length > 0) out.environment = environment;
3294
+ }
3295
+ return out;
3296
+ }
3297
+ var import_node_path7;
3298
+ var init_secret_vault = __esm({
3299
+ "src/store/secret-vault.ts"() {
3300
+ "use strict";
3301
+ import_node_path7 = require("path");
3302
+ init_paths();
3303
+ init_secure_file();
3304
+ }
3305
+ });
3306
+
3051
3307
  // src/store/app-settings.ts
3052
3308
  var app_settings_exports = {};
3053
3309
  __export(app_settings_exports, {
@@ -3063,10 +3319,13 @@ __export(app_settings_exports, {
3063
3319
  brightsyInjectWorktreeMcpEnabled: () => brightsyInjectWorktreeMcpEnabled,
3064
3320
  caffeinateWhileCloudConnectEnabled: () => caffeinateWhileCloudConnectEnabled,
3065
3321
  caffeinateWhileRunningEnabled: () => caffeinateWhileRunningEnabled,
3322
+ caffeinateWhileSlackListenEnabled: () => caffeinateWhileSlackListenEnabled,
3066
3323
  childEnvWithAppSettings: () => childEnvWithAppSettings,
3067
3324
  claudeChromeEnabled: () => claudeChromeEnabled,
3068
3325
  claudeUserSettingsPath: () => claudeUserSettingsPath,
3069
3326
  deleteBranchOnPurgeEnabled: () => deleteBranchOnPurgeEnabled,
3327
+ disconnectLinearConnection: () => disconnectLinearConnection,
3328
+ ensureSlackDeviceIdentity: () => ensureSlackDeviceIdentity,
3070
3329
  getDefaultAgent: () => getDefaultAgent,
3071
3330
  getDefaultEffort: () => getDefaultEffort,
3072
3331
  getDefaultFast: () => getDefaultFast,
@@ -3085,6 +3344,10 @@ __export(app_settings_exports, {
3085
3344
  resolveNewThreadOptions: () => resolveNewThreadOptions,
3086
3345
  resolveThreadDefaults: () => resolveThreadDefaults,
3087
3346
  saveAppSettings: () => saveAppSettings,
3347
+ saveLinearOAuth: () => saveLinearOAuth,
3348
+ slackAppLevelToken: () => slackAppLevelToken,
3349
+ slackListenEnabled: () => slackListenEnabled,
3350
+ toPublicAppSettings: () => toPublicAppSettings,
3088
3351
  updateAdvancedSettings: () => updateAdvancedSettings,
3089
3352
  updateAgentExecutable: () => updateAgentExecutable,
3090
3353
  updateAppEnvironment: () => updateAppEnvironment,
@@ -3095,11 +3358,45 @@ __export(app_settings_exports, {
3095
3358
  updateIntegrationsSettings: () => updateIntegrationsSettings,
3096
3359
  updateOpencodeSettings: () => updateOpencodeSettings
3097
3360
  });
3361
+ function hasLinearOAuth(integrations) {
3362
+ return Boolean(
3363
+ integrations.linearAccessToken?.trim() || integrations.linearRefreshToken?.trim()
3364
+ );
3365
+ }
3366
+ function hasLinearCredentials(integrations) {
3367
+ return Boolean(integrations.linearApiKey?.trim()) || hasLinearOAuth(integrations);
3368
+ }
3369
+ function toPublicAppSettings(settings) {
3370
+ const environment = {};
3371
+ for (const key of Object.keys(settings.environment)) {
3372
+ if (key.trim()) environment[key] = "";
3373
+ }
3374
+ const integrations = { ...settings.integrations };
3375
+ const slackClientSecret = integrations.slackClientSecret;
3376
+ const slackAppToken = integrations.slackAppToken;
3377
+ delete integrations.linearApiKey;
3378
+ delete integrations.linearAccessToken;
3379
+ delete integrations.linearRefreshToken;
3380
+ delete integrations.linearClientSecret;
3381
+ delete integrations.slackClientSecret;
3382
+ delete integrations.slackAppToken;
3383
+ return {
3384
+ ...settings,
3385
+ environment,
3386
+ integrations: {
3387
+ ...integrations,
3388
+ hasLinearApiKey: hasLinearCredentials(settings.integrations),
3389
+ hasLinearOAuth: hasLinearOAuth(settings.integrations),
3390
+ hasSlackClientSecret: Boolean(slackClientSecret?.trim()),
3391
+ hasSlackAppToken: Boolean(slackAppToken?.trim())
3392
+ }
3393
+ };
3394
+ }
3098
3395
  function appSettingsPath() {
3099
- return (0, import_node_path6.join)(appDataDir(), "settings.json");
3396
+ return (0, import_node_path8.join)(appDataDir(), "settings.json");
3100
3397
  }
3101
3398
  function claudeUserSettingsPath() {
3102
- return (0, import_node_path6.join)((0, import_node_os4.homedir)(), ".claude", "settings.json");
3399
+ return (0, import_node_path8.join)((0, import_node_os4.homedir)(), ".claude", "settings.json");
3103
3400
  }
3104
3401
  function normalizeClaude(raw) {
3105
3402
  if (!raw || typeof raw !== "object") return {};
@@ -3151,9 +3448,62 @@ function normalizeIntegrations(raw) {
3151
3448
  const key = source.linearApiKey.trim();
3152
3449
  if (key) out.linearApiKey = key;
3153
3450
  }
3451
+ if (typeof source.linearAccessToken === "string") {
3452
+ const token = source.linearAccessToken.trim();
3453
+ if (token) out.linearAccessToken = token;
3454
+ }
3455
+ if (typeof source.linearRefreshToken === "string") {
3456
+ const token = source.linearRefreshToken.trim();
3457
+ if (token) out.linearRefreshToken = token;
3458
+ }
3459
+ if (typeof source.linearTokenExpiresAt === "number" && Number.isFinite(source.linearTokenExpiresAt)) {
3460
+ out.linearTokenExpiresAt = source.linearTokenExpiresAt;
3461
+ } else if (typeof source.linearTokenExpiresAt === "string") {
3462
+ const n = Number(source.linearTokenExpiresAt);
3463
+ if (Number.isFinite(n)) out.linearTokenExpiresAt = n;
3464
+ }
3465
+ if (typeof source.linearClientId === "string") {
3466
+ const id = source.linearClientId.trim();
3467
+ if (id) out.linearClientId = id;
3468
+ }
3469
+ if (typeof source.linearClientSecret === "string") {
3470
+ const secret = source.linearClientSecret.trim();
3471
+ if (secret) out.linearClientSecret = secret;
3472
+ }
3473
+ if (typeof source.linearViewerName === "string") {
3474
+ const name = source.linearViewerName.trim().slice(0, 120);
3475
+ if (name) out.linearViewerName = name;
3476
+ }
3477
+ if (typeof source.linearOrganizationName === "string") {
3478
+ const name = source.linearOrganizationName.trim().slice(0, 120);
3479
+ if (name) out.linearOrganizationName = name;
3480
+ }
3154
3481
  if (typeof source.issueSource === "string" && ISSUE_SOURCES.has(source.issueSource)) {
3155
3482
  out.issueSource = source.issueSource;
3156
3483
  }
3484
+ if (typeof source.slackClientId === "string") {
3485
+ const id = source.slackClientId.trim();
3486
+ if (id) out.slackClientId = id;
3487
+ }
3488
+ if (typeof source.slackClientSecret === "string") {
3489
+ const secret = source.slackClientSecret.trim();
3490
+ if (secret) out.slackClientSecret = secret;
3491
+ }
3492
+ if (typeof source.slackAppToken === "string") {
3493
+ const token = source.slackAppToken.trim();
3494
+ if (token) out.slackAppToken = token;
3495
+ }
3496
+ if (typeof source.slackListenEnabled === "boolean") {
3497
+ out.slackListenEnabled = source.slackListenEnabled;
3498
+ }
3499
+ if (typeof source.slackDeviceId === "string") {
3500
+ const id = source.slackDeviceId.trim();
3501
+ if (id) out.slackDeviceId = id;
3502
+ }
3503
+ if (typeof source.slackDeviceLabel === "string") {
3504
+ const label = source.slackDeviceLabel.trim().slice(0, 64);
3505
+ if (label) out.slackDeviceLabel = label;
3506
+ }
3157
3507
  return out;
3158
3508
  }
3159
3509
  function normalizeDefaults(raw) {
@@ -3188,8 +3538,10 @@ function normalizeAdvanced(raw) {
3188
3538
  if (typeof source.caffeinateWhileRunning === "boolean") {
3189
3539
  out.caffeinateWhileRunning = source.caffeinateWhileRunning;
3190
3540
  }
3191
- if (typeof source.caffeinateWhileCloudConnect === "boolean") {
3192
- out.caffeinateWhileCloudConnect = source.caffeinateWhileCloudConnect;
3541
+ if (typeof source.caffeinateWhileSlackListen === "boolean") {
3542
+ out.caffeinateWhileSlackListen = source.caffeinateWhileSlackListen;
3543
+ } else if (typeof source.caffeinateWhileCloudConnect === "boolean") {
3544
+ out.caffeinateWhileSlackListen = source.caffeinateWhileCloudConnect;
3193
3545
  }
3194
3546
  if (typeof source.deleteBranchOnPurge === "boolean") {
3195
3547
  out.deleteBranchOnPurge = source.deleteBranchOnPurge;
@@ -3278,23 +3630,84 @@ function normalizeSettings(raw) {
3278
3630
  advanced
3279
3631
  };
3280
3632
  }
3281
- function loadAppSettings() {
3633
+ function readSettingsFile() {
3282
3634
  const path = appSettingsPath();
3283
- if (!(0, import_node_fs6.existsSync)(path)) {
3284
- return { ...EMPTY_SETTINGS };
3285
- }
3635
+ if (!(0, import_node_fs8.existsSync)(path)) return { ...EMPTY_SETTINGS };
3286
3636
  try {
3287
- const parsed = JSON.parse((0, import_node_fs6.readFileSync)(path, "utf8"));
3637
+ chmodOwnerOnly(path);
3638
+ const parsed = JSON.parse((0, import_node_fs8.readFileSync)(path, "utf8"));
3288
3639
  return normalizeSettings(parsed);
3289
3640
  } catch {
3290
3641
  return { ...EMPTY_SETTINGS };
3291
3642
  }
3292
3643
  }
3644
+ function diskHoldsSecrets(disk) {
3645
+ return Boolean(
3646
+ disk.integrations.linearApiKey || disk.integrations.linearAccessToken || disk.integrations.linearRefreshToken || disk.integrations.linearClientSecret || disk.integrations.slackClientSecret || disk.integrations.slackAppToken || Object.keys(disk.environment).length > 0
3647
+ );
3648
+ }
3649
+ function mergeVault(disk) {
3650
+ const vault = loadSecretVault();
3651
+ const environment = { ...vault.environment ?? {}, ...disk.environment };
3652
+ const integrations = { ...disk.integrations };
3653
+ if (vault.linearApiKey && !integrations.linearApiKey) {
3654
+ integrations.linearApiKey = vault.linearApiKey;
3655
+ }
3656
+ if (vault.linearAccessToken && !integrations.linearAccessToken) {
3657
+ integrations.linearAccessToken = vault.linearAccessToken;
3658
+ }
3659
+ if (vault.linearRefreshToken && !integrations.linearRefreshToken) {
3660
+ integrations.linearRefreshToken = vault.linearRefreshToken;
3661
+ }
3662
+ if (vault.linearClientSecret && !integrations.linearClientSecret) {
3663
+ integrations.linearClientSecret = vault.linearClientSecret;
3664
+ }
3665
+ if (vault.slackClientSecret && !integrations.slackClientSecret) {
3666
+ integrations.slackClientSecret = vault.slackClientSecret;
3667
+ }
3668
+ if (vault.slackAppToken && !integrations.slackAppToken) {
3669
+ integrations.slackAppToken = vault.slackAppToken;
3670
+ }
3671
+ return { ...disk, environment, integrations };
3672
+ }
3673
+ function persistSplit(settings) {
3674
+ const integrations = { ...settings.integrations };
3675
+ const linearApiKey = integrations.linearApiKey;
3676
+ const linearAccessToken = integrations.linearAccessToken;
3677
+ const linearRefreshToken = integrations.linearRefreshToken;
3678
+ const linearClientSecret = integrations.linearClientSecret;
3679
+ const slackClientSecret = integrations.slackClientSecret;
3680
+ const slackAppToken = integrations.slackAppToken;
3681
+ delete integrations.linearApiKey;
3682
+ delete integrations.linearAccessToken;
3683
+ delete integrations.linearRefreshToken;
3684
+ delete integrations.linearClientSecret;
3685
+ delete integrations.slackClientSecret;
3686
+ delete integrations.slackAppToken;
3687
+ writePrivateFile(
3688
+ appSettingsPath(),
3689
+ `${JSON.stringify({ ...settings, environment: {}, integrations }, null, 2)}
3690
+ `
3691
+ );
3692
+ saveSecretVault({
3693
+ linearApiKey,
3694
+ linearAccessToken,
3695
+ linearRefreshToken,
3696
+ linearClientSecret,
3697
+ slackClientSecret,
3698
+ slackAppToken,
3699
+ environment: settings.environment
3700
+ });
3701
+ }
3702
+ function loadAppSettings() {
3703
+ const disk = readSettingsFile();
3704
+ const merged = mergeVault(disk);
3705
+ if (diskHoldsSecrets(disk)) persistSplit(merged);
3706
+ return merged;
3707
+ }
3293
3708
  function saveAppSettings(settings) {
3294
3709
  const normalized = normalizeSettings(settings);
3295
- (0, import_node_fs6.mkdirSync)(appDataDir(), { recursive: true });
3296
- (0, import_node_fs6.writeFileSync)(appSettingsPath(), `${JSON.stringify(normalized, null, 2)}
3297
- `, "utf8");
3710
+ persistSplit(normalized);
3298
3711
  return normalized;
3299
3712
  }
3300
3713
  function updateAppEnvironment(patch) {
@@ -3385,6 +3798,20 @@ function updateIntegrationsSettings(patch) {
3385
3798
  integrations.linearApiKey = patch.linearApiKey.trim();
3386
3799
  }
3387
3800
  }
3801
+ if ("linearClientId" in patch) {
3802
+ if (patch.linearClientId == null || patch.linearClientId.trim() === "") {
3803
+ delete integrations.linearClientId;
3804
+ } else {
3805
+ integrations.linearClientId = patch.linearClientId.trim();
3806
+ }
3807
+ }
3808
+ if ("linearClientSecret" in patch) {
3809
+ if (patch.linearClientSecret == null || patch.linearClientSecret.trim() === "") {
3810
+ delete integrations.linearClientSecret;
3811
+ } else {
3812
+ integrations.linearClientSecret = patch.linearClientSecret.trim();
3813
+ }
3814
+ }
3388
3815
  if ("issueSource" in patch) {
3389
3816
  if (patch.issueSource == null) {
3390
3817
  delete integrations.issueSource;
@@ -3392,6 +3819,48 @@ function updateIntegrationsSettings(patch) {
3392
3819
  integrations.issueSource = patch.issueSource;
3393
3820
  }
3394
3821
  }
3822
+ if ("slackClientId" in patch) {
3823
+ if (patch.slackClientId == null || patch.slackClientId.trim() === "") {
3824
+ delete integrations.slackClientId;
3825
+ } else {
3826
+ integrations.slackClientId = patch.slackClientId.trim();
3827
+ }
3828
+ }
3829
+ if ("slackClientSecret" in patch) {
3830
+ if (patch.slackClientSecret == null || patch.slackClientSecret.trim() === "") {
3831
+ delete integrations.slackClientSecret;
3832
+ } else {
3833
+ integrations.slackClientSecret = patch.slackClientSecret.trim();
3834
+ }
3835
+ }
3836
+ if ("slackAppToken" in patch) {
3837
+ if (patch.slackAppToken == null || patch.slackAppToken.trim() === "") {
3838
+ delete integrations.slackAppToken;
3839
+ } else {
3840
+ integrations.slackAppToken = patch.slackAppToken.trim();
3841
+ }
3842
+ }
3843
+ if ("slackListenEnabled" in patch) {
3844
+ if (patch.slackListenEnabled == null) {
3845
+ delete integrations.slackListenEnabled;
3846
+ } else {
3847
+ integrations.slackListenEnabled = Boolean(patch.slackListenEnabled);
3848
+ }
3849
+ }
3850
+ if ("slackDeviceId" in patch) {
3851
+ if (patch.slackDeviceId == null || patch.slackDeviceId.trim() === "") {
3852
+ delete integrations.slackDeviceId;
3853
+ } else {
3854
+ integrations.slackDeviceId = patch.slackDeviceId.trim();
3855
+ }
3856
+ }
3857
+ if ("slackDeviceLabel" in patch) {
3858
+ if (patch.slackDeviceLabel == null || patch.slackDeviceLabel.trim() === "") {
3859
+ delete integrations.slackDeviceLabel;
3860
+ } else {
3861
+ integrations.slackDeviceLabel = patch.slackDeviceLabel.trim().slice(0, 64);
3862
+ }
3863
+ }
3395
3864
  return saveAppSettings({ ...current, integrations });
3396
3865
  }
3397
3866
  function updateDefaultsSettings(patch) {
@@ -3465,7 +3934,36 @@ function resolveNewThreadOptions(overrides = {}, settings = loadAppSettings()) {
3465
3934
  };
3466
3935
  }
3467
3936
  function isLinearConnected(settings = loadAppSettings()) {
3468
- return Boolean(settings.integrations.linearApiKey?.trim());
3937
+ return hasLinearCredentials(settings.integrations);
3938
+ }
3939
+ function saveLinearOAuth(input) {
3940
+ const current = loadAppSettings();
3941
+ const integrations = { ...current.integrations };
3942
+ integrations.linearAccessToken = input.accessToken.trim();
3943
+ if (input.refreshToken?.trim()) {
3944
+ integrations.linearRefreshToken = input.refreshToken.trim();
3945
+ }
3946
+ if (typeof input.expiresIn === "number" && Number.isFinite(input.expiresIn)) {
3947
+ integrations.linearTokenExpiresAt = Date.now() + Math.max(0, input.expiresIn) * 1e3;
3948
+ }
3949
+ if (input.viewerName?.trim()) {
3950
+ integrations.linearViewerName = input.viewerName.trim().slice(0, 120);
3951
+ }
3952
+ if (input.organizationName?.trim()) {
3953
+ integrations.linearOrganizationName = input.organizationName.trim().slice(0, 120);
3954
+ }
3955
+ return saveAppSettings({ ...current, integrations });
3956
+ }
3957
+ function disconnectLinearConnection() {
3958
+ const current = loadAppSettings();
3959
+ const integrations = { ...current.integrations };
3960
+ delete integrations.linearApiKey;
3961
+ delete integrations.linearAccessToken;
3962
+ delete integrations.linearRefreshToken;
3963
+ delete integrations.linearTokenExpiresAt;
3964
+ delete integrations.linearViewerName;
3965
+ delete integrations.linearOrganizationName;
3966
+ return saveAppSettings({ ...current, integrations });
3469
3967
  }
3470
3968
  function getIssueSource(settings = loadAppSettings()) {
3471
3969
  return settings.integrations.issueSource ?? "github";
@@ -3482,6 +3980,33 @@ function getLinearApiKey(settings = loadAppSettings()) {
3482
3980
  function brightsyCloudConnectEnabled(settings = loadAppSettings()) {
3483
3981
  return Boolean(settings.brightsy.cloudConnectEnabled);
3484
3982
  }
3983
+ function slackListenEnabled(settings = loadAppSettings()) {
3984
+ return settings.integrations.slackListenEnabled === true;
3985
+ }
3986
+ function ensureSlackDeviceIdentity(settings = loadAppSettings()) {
3987
+ let deviceId = settings.integrations.slackDeviceId?.trim() ?? "";
3988
+ let deviceLabel = settings.integrations.slackDeviceLabel?.trim() ?? "";
3989
+ const patch = {};
3990
+ if (!deviceId) {
3991
+ deviceId = (0, import_node_crypto3.randomUUID)();
3992
+ patch.slackDeviceId = deviceId;
3993
+ }
3994
+ if (!deviceLabel) {
3995
+ try {
3996
+ deviceLabel = (0, import_node_os4.hostname)().split(".")[0]?.trim() || "This Mac";
3997
+ } catch {
3998
+ deviceLabel = "This Mac";
3999
+ }
4000
+ patch.slackDeviceLabel = deviceLabel;
4001
+ }
4002
+ if (Object.keys(patch).length > 0) {
4003
+ updateIntegrationsSettings(patch);
4004
+ }
4005
+ return { deviceId, deviceLabel };
4006
+ }
4007
+ function slackAppLevelToken(settings = loadAppSettings()) {
4008
+ return process.env.SIDEBOARD_SLACK_APP_TOKEN?.trim() || settings.integrations.slackAppToken?.trim() || "";
4009
+ }
3485
4010
  function brightsyInjectWorktreeMcpEnabled(settings = loadAppSettings()) {
3486
4011
  return Boolean(settings.brightsy.injectWorktreeMcp);
3487
4012
  }
@@ -3505,8 +4030,12 @@ function updateAdvancedSettings(patch) {
3505
4030
  if (typeof patch.caffeinateWhileRunning === "boolean") {
3506
4031
  advanced.caffeinateWhileRunning = patch.caffeinateWhileRunning;
3507
4032
  }
3508
- if (typeof patch.caffeinateWhileCloudConnect === "boolean") {
3509
- advanced.caffeinateWhileCloudConnect = patch.caffeinateWhileCloudConnect;
4033
+ if (typeof patch.caffeinateWhileSlackListen === "boolean") {
4034
+ advanced.caffeinateWhileSlackListen = patch.caffeinateWhileSlackListen;
4035
+ delete advanced.caffeinateWhileCloudConnect;
4036
+ } else if (typeof patch.caffeinateWhileCloudConnect === "boolean") {
4037
+ advanced.caffeinateWhileSlackListen = patch.caffeinateWhileCloudConnect;
4038
+ delete advanced.caffeinateWhileCloudConnect;
3510
4039
  }
3511
4040
  if (typeof patch.deleteBranchOnPurge === "boolean") {
3512
4041
  advanced.deleteBranchOnPurge = patch.deleteBranchOnPurge;
@@ -3549,8 +4078,11 @@ function autoRunAfterSetupEnabled(settings = loadAppSettings()) {
3549
4078
  function caffeinateWhileRunningEnabled(settings = loadAppSettings()) {
3550
4079
  return Boolean(settings.advanced.caffeinateWhileRunning);
3551
4080
  }
4081
+ function caffeinateWhileSlackListenEnabled(settings = loadAppSettings()) {
4082
+ return Boolean(settings.advanced.caffeinateWhileSlackListen);
4083
+ }
3552
4084
  function caffeinateWhileCloudConnectEnabled(settings = loadAppSettings()) {
3553
- return Boolean(settings.advanced.caffeinateWhileCloudConnect);
4085
+ return caffeinateWhileSlackListenEnabled(settings);
3554
4086
  }
3555
4087
  function deleteBranchOnPurgeEnabled(settings = loadAppSettings()) {
3556
4088
  return Boolean(settings.advanced.deleteBranchOnPurge);
@@ -3636,15 +4168,18 @@ function childEnvWithAppSettings(extra) {
3636
4168
  function harnessEnvKey(harness) {
3637
4169
  return HARNESS_ENV_KEYS[harness];
3638
4170
  }
3639
- var import_node_fs6, import_node_os4, import_node_path6, HARNESS_ENV_KEYS, DEFAULT_AGENTS, CLOUD_CONNECT_AGENTS, ISSUE_SOURCES, EMPTY_SETTINGS, DEFAULT_CLI_BIN;
4171
+ var import_node_crypto3, import_node_fs8, import_node_os4, import_node_path8, HARNESS_ENV_KEYS, DEFAULT_AGENTS, CLOUD_CONNECT_AGENTS, ISSUE_SOURCES, EMPTY_SETTINGS, DEFAULT_CLI_BIN;
3640
4172
  var init_app_settings = __esm({
3641
4173
  "src/store/app-settings.ts"() {
3642
4174
  "use strict";
3643
- import_node_fs6 = require("fs");
4175
+ import_node_crypto3 = require("crypto");
4176
+ import_node_fs8 = require("fs");
3644
4177
  import_node_os4 = require("os");
3645
- import_node_path6 = require("path");
4178
+ import_node_path8 = require("path");
3646
4179
  init_thinking_effort();
3647
4180
  init_paths();
4181
+ init_private_file();
4182
+ init_secret_vault();
3648
4183
  HARNESS_ENV_KEYS = {
3649
4184
  claude: "ANTHROPIC_API_KEY",
3650
4185
  codex: "CODEX_API_KEY",
@@ -3747,6 +4282,7 @@ var init_orchestrator_capable = __esm({
3747
4282
  var coordinator_prompt_exports = {};
3748
4283
  __export(coordinator_prompt_exports, {
3749
4284
  COORDINATOR_TOOL_PLAYBOOK: () => COORDINATOR_TOOL_PLAYBOOK,
4285
+ SLACK_REPLY_FORMATTING: () => SLACK_REPLY_FORMATTING,
3750
4286
  coordinatorGreenfieldPlaybook: () => coordinatorGreenfieldPlaybook,
3751
4287
  coordinatorSystemPrompt: () => coordinatorSystemPrompt,
3752
4288
  coordinatorTurnReminder: () => coordinatorTurnReminder,
@@ -3801,13 +4337,14 @@ function coordinatorTurnReminder(opts) {
3801
4337
  `- For "what's going on": call list_threads (and list_workspaces if needed). Summarize fleet status \u2014 do not ls/git-status this synthetic home.`,
3802
4338
  "- Existing repo: create_thread on a repoPath \u2192 send_to_thread \u2192 wait_for_turn.",
3803
4339
  "- New repo: Bash (clone or gh repo create under ~/sideboard/repos/<name>) \u2192 add_workspace \u2192 create_thread \u2192 send_to_thread \u2192 wait_for_turn \u2192 draft PR.",
3804
- "- When naming threads for the user, link them as `[Title](sideboard://thread/<id>)`."
4340
+ "- When naming threads for the user, link them as `[Title](sideboard://thread/<id>)`.",
4341
+ "- If they will wait on Slack or leave the Mac, call set_caffeinate enabled=true. When they say they are done / wrapping up / going to sleep, call set_caffeinate enabled=false."
3805
4342
  ].filter(Boolean).join("\n");
3806
4343
  }
3807
4344
  function ensureGlobalCoordinatorCwd(opts) {
3808
4345
  const dir = globalAgentCwd();
3809
4346
  try {
3810
- (0, import_node_fs7.mkdirSync)(dir, { recursive: true });
4347
+ (0, import_node_fs9.mkdirSync)(dir, { recursive: true });
3811
4348
  } catch {
3812
4349
  return dir;
3813
4350
  }
@@ -3815,7 +4352,7 @@ function ensureGlobalCoordinatorCwd(opts) {
3815
4352
  let orchId = opts?.orchestratorThreadId?.trim() || "";
3816
4353
  if (!orchId) {
3817
4354
  try {
3818
- const existing = (0, import_node_fs7.readFileSync)((0, import_node_path7.join)(dir, "AGENTS.md"), "utf8");
4355
+ const existing = (0, import_node_fs9.readFileSync)((0, import_node_path9.join)(dir, "AGENTS.md"), "utf8");
3819
4356
  const m = existing.match(
3820
4357
  /YOUR orchestration thread id is `([0-9a-f-]{36})`/i
3821
4358
  );
@@ -3857,9 +4394,9 @@ function ensureGlobalCoordinatorCwd(opts) {
3857
4394
  "Always ask worktree agents to open draft PRs (`send_to_thread` + `gh pr create --draft -R <origin>`); never open PRs from the orchestrator."
3858
4395
  ].join("\n");
3859
4396
  try {
3860
- (0, import_node_fs7.writeFileSync)((0, import_node_path7.join)(dir, "CLAUDE.md"), `${body}
4397
+ (0, import_node_fs9.writeFileSync)((0, import_node_path9.join)(dir, "CLAUDE.md"), `${body}
3861
4398
  `, "utf8");
3862
- (0, import_node_fs7.writeFileSync)((0, import_node_path7.join)(dir, "AGENTS.md"), `${body}
4399
+ (0, import_node_fs9.writeFileSync)((0, import_node_path9.join)(dir, "AGENTS.md"), `${body}
3863
4400
  `, "utf8");
3864
4401
  } catch {
3865
4402
  }
@@ -3869,8 +4406,14 @@ function coordinatorSystemPrompt(opts) {
3869
4406
  const audience = opts.audience ?? "cloud";
3870
4407
  const reposDir = sideboardReposDir();
3871
4408
  const intro = audience === "cloud" ? [
3872
- "You are a Sideboard coordinator responding to a request from a Brightsy cloud agent (Slack, Discord, Teams, or other chat).",
4409
+ "You are a Sideboard coordinator responding to a request from a Brightsy cloud agent (Discord, Teams, or other chat).",
3873
4410
  "Your reply will be sent back to that cloud agent \u2014 be concise and actionable."
4411
+ ] : audience === "slack" ? [
4412
+ "You are a Sideboard coordinator responding to a Slack DM or @mention via the Sideboard Slack app.",
4413
+ "Your reply will be posted back in Slack \u2014 be concise and actionable.",
4414
+ "Sideboard signs the Slack reply with this Mac's destination name (Personal / Work). Do not prefix that name yourself.",
4415
+ "Call set_caffeinate enabled=true when they start a stretch of remote work so the Mac stays awake. Call set_caffeinate enabled=false when they say they are done, wrapping up, or going to sleep.",
4416
+ SLACK_REPLY_FORMATTING
3874
4417
  ] : [
3875
4418
  "You are a Sideboard orchestration agent: you oversee worktree agents across registered workspaces in the Sideboard app.",
3876
4419
  "Stay concise and actionable; prefer Sideboard MCP for fleet status; use Bash for greenfield repo setup and inspecting target worktree paths."
@@ -3892,12 +4435,12 @@ function coordinatorSystemPrompt(opts) {
3892
4435
  formatWorkspaceInventory(opts.workspaces)
3893
4436
  ].join("\n");
3894
4437
  }
3895
- var import_node_fs7, import_node_path7, COORDINATOR_TOOL_PLAYBOOK;
4438
+ var import_node_fs9, import_node_path9, COORDINATOR_TOOL_PLAYBOOK, SLACK_REPLY_FORMATTING;
3896
4439
  var init_coordinator_prompt = __esm({
3897
4440
  "src/orchestrator/coordinator-prompt.ts"() {
3898
4441
  "use strict";
3899
- import_node_fs7 = require("fs");
3900
- import_node_path7 = require("path");
4442
+ import_node_fs9 = require("fs");
4443
+ import_node_path9 = require("path");
3901
4444
  init_worktree();
3902
4445
  init_app_settings();
3903
4446
  init_paths();
@@ -3907,9 +4450,12 @@ var init_coordinator_prompt = __esm({
3907
4450
  "Discover:",
3908
4451
  "- list_workspaces \u2014 registered repos (path + github slug when known)",
3909
4452
  "- list_branches / list_prs / list_issues \u2014 pass repoPath from list_workspaces (issues: Linear API or GitHub Issues)",
4453
+ "- list_teams / slack_list_channels / slack_list_users / slack_search / slack_read / slack_post \u2014 Slack workspaces from Account settings; pass team_id from list_teams",
4454
+ "- Slack notify (only when the user asks): list_teams \u2192 slack_list_users or slack_list_channels \u2192 slack_post with to=@user or #channel and optional github_url (PR, blob permalink, or review/issue comment). Do not notify proactively.",
3910
4455
  "- get_pr_stack / open_pr_stack_layers / add_stack_layer / create_pr_stack \u2014 GitHub stacked PRs (`gh stack`); one worktree per layer",
3911
4456
  "- list_models \u2014 only when you need a specific model (rare); otherwise omit model so Account defaults apply",
3912
4457
  "- list_threads / get_thread \u2014 fleet status (what is going on)",
4458
+ "- set_caffeinate \u2014 keep this Mac awake across turns (macOS caffeinate). Turn on for Slack / away-from-keyboard work. Turn OFF when the user says they are done, wrapping up, going to sleep, or no longer need the machine awake.",
3913
4459
  "Workspaces:",
3914
4460
  "- add_workspace / remove_workspace \u2014 register or unregister a git repo",
3915
4461
  "Worktree threads (chats):",
@@ -3931,6 +4477,13 @@ var init_coordinator_prompt = __esm({
3931
4477
  "Thread links in replies: when mentioning a chat/thread for the user, include a markdown link `[Title](sideboard://thread/<id>)` using the full id (or the link field from create_thread / list_threads). Sideboard renders these as clickable opens.",
3932
4478
  "Bash / Read / etc: allowed for (1) inspecting target worktrees / registered repo paths from MCP, and (2) greenfield setup under ~/sideboard/repos (git clone, gh repo create, git init+remote). Never git init/clone *inside* this synthetic home cwd \u2014 emptiness here is expected, not a bug."
3933
4479
  ].join("\n");
4480
+ SLACK_REPLY_FORMATTING = [
4481
+ "Slack formatting (mandatory \u2014 your reply is posted verbatim to Slack):",
4482
+ "- Use Slack mrkdwn, NOT GitHub/CommonMark markdown. Slack does not render `**bold**`, `# headings`, or `[label](url)`.",
4483
+ "- Bold: *text* (single asterisks). Italic: _text_. Strikethrough: ~text~. Inline code: `code`.",
4484
+ "- Links: <https://example.com|label> or bare URLs. Thread ids: plain `sideboard://thread/<id>` (no markdown link syntax).",
4485
+ "- Prefer short paragraphs and *bold* section labels over markdown headings or bullet trees with `**`."
4486
+ ].join("\n");
3934
4487
  }
3935
4488
  });
3936
4489
 
@@ -3940,15 +4493,18 @@ __export(global_workspace_exports, {
3940
4493
  GLOBAL_WORKSPACE_ID: () => GLOBAL_WORKSPACE_ID,
3941
4494
  createGlobalChat: () => createGlobalChat,
3942
4495
  ensureCloudCoordinator: () => ensureCloudCoordinator,
4496
+ ensureSlackCoordinator: () => ensureSlackCoordinator,
3943
4497
  globalAgentCwd: () => globalAgentCwd,
3944
4498
  healOrchestrationSoccerTitles: () => healOrchestrationSoccerTitles,
3945
4499
  isCloudCoordinatorThread: () => isCloudCoordinatorThread,
3946
4500
  isGlobalRepoPath: () => isGlobalRepoPath,
3947
4501
  isGlobalThread: () => isGlobalThread,
3948
4502
  isOrchestratorThread: () => isOrchestratorThread,
4503
+ isSlackCoordinatorThread: () => isSlackCoordinatorThread,
3949
4504
  listGlobalThreads: () => listGlobalThreads,
3950
4505
  orchestrationTitleNeedsSoccerNickname: () => orchestrationTitleNeedsSoccerNickname,
3951
4506
  orchestratorSessionPoisonedByBuiltins: () => orchestratorSessionPoisonedByBuiltins,
4507
+ slackCoordinatorSourceRef: () => slackCoordinatorSourceRef,
3952
4508
  takenTeamSlugsForOrchestration: () => takenTeamSlugsForOrchestration
3953
4509
  });
3954
4510
  function isGlobalThread(thread) {
@@ -4065,6 +4621,62 @@ function findCloudCoordinator() {
4065
4621
  (t) => t.status !== "archived" && isCloudCoordinatorThread(t)
4066
4622
  );
4067
4623
  }
4624
+ function slackCoordinatorSourceRef(teamId, userId) {
4625
+ const team = teamId.trim();
4626
+ const user = userId.trim();
4627
+ if (!team || !user) {
4628
+ throw new Error("Slack coordinator needs teamId and userId");
4629
+ }
4630
+ return `slack:${team}:${user}`;
4631
+ }
4632
+ function isSlackCoordinatorThread(thread) {
4633
+ const ref = thread.sourceRef?.trim() ?? "";
4634
+ if (!ref.startsWith("slack:")) return false;
4635
+ return isGlobalThread(thread) || thread.sourceType === "orchestration";
4636
+ }
4637
+ function findSlackCoordinator(teamId, userId) {
4638
+ const ref = slackCoordinatorSourceRef(teamId, userId);
4639
+ return listThreads({ includeArchived: true }).find(
4640
+ (t) => t.status !== "archived" && t.sourceRef === ref && isSlackCoordinatorThread(t)
4641
+ );
4642
+ }
4643
+ function ensureSlackCoordinator(teamId, userId, agent) {
4644
+ const defaults = resolveThreadDefaults();
4645
+ const desired = assertOrchestratorCapableAgent(agent);
4646
+ const ref = slackCoordinatorSourceRef(teamId, userId);
4647
+ const existing = findSlackCoordinator(teamId, userId);
4648
+ if (existing) {
4649
+ const patch = {};
4650
+ if (existing.repoPath !== GLOBAL_WORKSPACE_ID) {
4651
+ patch.repoPath = GLOBAL_WORKSPACE_ID;
4652
+ patch.worktreePath = globalAgentCwd();
4653
+ patch.branchName = "global";
4654
+ }
4655
+ const hasAgentTurns = existing.messages.some((m) => m.role === "agent");
4656
+ const canRetarget = !existing.sessionId && !hasAgentTurns && existing.status !== "running" && existing.status !== "queued";
4657
+ if (canRetarget) {
4658
+ if (existing.agent !== desired) patch.agent = desired;
4659
+ if (existing.model !== defaults.model) patch.model = defaults.model;
4660
+ if (existing.effort !== defaults.effort) patch.effort = defaults.effort;
4661
+ if (existing.fast !== defaults.fast) patch.fast = defaults.fast;
4662
+ }
4663
+ if (Object.keys(patch).length > 0) {
4664
+ return updateThread(existing.id, patch);
4665
+ }
4666
+ return existing;
4667
+ }
4668
+ const created = createGlobalChat({
4669
+ sourceRef: ref,
4670
+ agent: desired,
4671
+ model: defaults.model,
4672
+ effort: defaults.effort,
4673
+ fast: defaults.fast
4674
+ });
4675
+ const all = listThreads({ includeArchived: true }).filter(
4676
+ (t) => t.status !== "archived" && t.sourceRef === ref && isSlackCoordinatorThread(t)
4677
+ ).sort((a, b) => a.createdAt.localeCompare(b.createdAt));
4678
+ return all[0] ?? created;
4679
+ }
4068
4680
  function ensureCloudCoordinator(agent) {
4069
4681
  const defaults = resolveThreadDefaults();
4070
4682
  const desired = assertOrchestratorCapableAgent(agent);
@@ -4118,32 +4730,32 @@ var init_global_workspace = __esm({
4118
4730
 
4119
4731
  // src/brightsy/config.ts
4120
4732
  function brightsyConfigPath() {
4121
- return (0, import_node_path8.join)((0, import_node_os5.homedir)(), ".brightsy", "config.json");
4733
+ return (0, import_node_path10.join)((0, import_node_os5.homedir)(), ".brightsy", "config.json");
4122
4734
  }
4123
4735
  function loadBrightsyConfig() {
4124
4736
  const path = brightsyConfigPath();
4125
- if (!(0, import_node_fs8.existsSync)(path)) {
4737
+ if (!(0, import_node_fs10.existsSync)(path)) {
4126
4738
  throw new Error("Brightsy not logged in \u2014 run `brightsy login` first");
4127
4739
  }
4128
- const raw = JSON.parse((0, import_node_fs8.readFileSync)(path, "utf8"));
4740
+ const raw = JSON.parse((0, import_node_fs10.readFileSync)(path, "utf8"));
4129
4741
  if (!raw.access_token || !raw.account_id) {
4130
4742
  throw new Error("Brightsy config incomplete \u2014 run `brightsy login`");
4131
4743
  }
4132
4744
  return raw;
4133
4745
  }
4134
4746
  function saveBrightsyConfig(cfg) {
4135
- (0, import_node_fs8.writeFileSync)(brightsyConfigPath(), `${JSON.stringify(cfg, null, 2)}
4747
+ (0, import_node_fs10.writeFileSync)(brightsyConfigPath(), `${JSON.stringify(cfg, null, 2)}
4136
4748
  `, {
4137
4749
  mode: 384
4138
4750
  });
4139
4751
  }
4140
- var import_node_fs8, import_node_os5, import_node_path8;
4752
+ var import_node_fs10, import_node_os5, import_node_path10;
4141
4753
  var init_config = __esm({
4142
4754
  "src/brightsy/config.ts"() {
4143
4755
  "use strict";
4144
- import_node_fs8 = require("fs");
4756
+ import_node_fs10 = require("fs");
4145
4757
  import_node_os5 = require("os");
4146
- import_node_path8 = require("path");
4758
+ import_node_path10 = require("path");
4147
4759
  }
4148
4760
  });
4149
4761
 
@@ -4158,22 +4770,22 @@ var init_accounts = __esm({
4158
4770
 
4159
4771
  // src/brightsy/connected-teams.ts
4160
4772
  function storePath() {
4161
- return (0, import_node_path9.join)(appDataDir(), "brightsy-teams.json");
4773
+ return (0, import_node_path11.join)(appDataDir(), "brightsy-teams.json");
4162
4774
  }
4163
4775
  function readStore() {
4164
4776
  const path = storePath();
4165
- if (!(0, import_node_fs9.existsSync)(path)) return [];
4777
+ if (!(0, import_node_fs11.existsSync)(path)) return [];
4166
4778
  try {
4167
- const parsed = JSON.parse((0, import_node_fs9.readFileSync)(path, "utf8"));
4779
+ const parsed = JSON.parse((0, import_node_fs11.readFileSync)(path, "utf8"));
4168
4780
  return Array.isArray(parsed.teams) ? parsed.teams : [];
4169
4781
  } catch {
4170
4782
  return [];
4171
4783
  }
4172
4784
  }
4173
4785
  function writeStore(teams) {
4174
- (0, import_node_fs9.mkdirSync)(appDataDir(), { recursive: true });
4786
+ (0, import_node_fs11.mkdirSync)(appDataDir(), { recursive: true });
4175
4787
  const path = storePath();
4176
- (0, import_node_fs9.writeFileSync)(path, `${JSON.stringify({ teams }, null, 2)}
4788
+ (0, import_node_fs11.writeFileSync)(path, `${JSON.stringify({ teams }, null, 2)}
4177
4789
  `, {
4178
4790
  mode: 384
4179
4791
  });
@@ -4285,12 +4897,12 @@ function brightsyMcpServerName(slug) {
4285
4897
  const cleaned = slug.replace(/[^A-Za-z0-9_-]/g, "_").replace(/^_+|_+$/g, "");
4286
4898
  return `brightsy_${cleaned || "team"}`;
4287
4899
  }
4288
- var import_node_fs9, import_node_path9;
4900
+ var import_node_fs11, import_node_path11;
4289
4901
  var init_connected_teams = __esm({
4290
4902
  "src/brightsy/connected-teams.ts"() {
4291
4903
  "use strict";
4292
- import_node_fs9 = require("fs");
4293
- import_node_path9 = require("path");
4904
+ import_node_fs11 = require("fs");
4905
+ import_node_path11 = require("path");
4294
4906
  init_paths();
4295
4907
  init_accounts();
4296
4908
  init_config();
@@ -4576,11 +5188,11 @@ async function syncCliForTarget(accountId) {
4576
5188
  }
4577
5189
  applyConnectedTeamToCli(team);
4578
5190
  }
4579
- var import_node_fs10, brightsyAdapter;
5191
+ var import_node_fs12, brightsyAdapter;
4580
5192
  var init_brightsy = __esm({
4581
5193
  "src/agents/brightsy.ts"() {
4582
5194
  "use strict";
4583
- import_node_fs10 = require("fs");
5195
+ import_node_fs12 = require("fs");
4584
5196
  init_run();
4585
5197
  init_connected_teams();
4586
5198
  init_config();
@@ -4594,7 +5206,7 @@ var init_brightsy = __esm({
4594
5206
  async detect() {
4595
5207
  const brightsy = resolveAgentExecutable("brightsy");
4596
5208
  if (brightsy !== "brightsy") {
4597
- if (!(0, import_node_fs10.existsSync)(brightsy)) {
5209
+ if (!(0, import_node_fs12.existsSync)(brightsy)) {
4598
5210
  return {
4599
5211
  agent: "brightsy",
4600
5212
  installed: false,
@@ -4670,10 +5282,10 @@ var init_brightsy = __esm({
4670
5282
  });
4671
5283
 
4672
5284
  // src/agents/claude-mcp.ts
4673
- function parseMcpList(text) {
5285
+ function parseMcpList(text2) {
4674
5286
  const servers = [];
4675
5287
  const seen = /* @__PURE__ */ new Set();
4676
- for (const raw of text.split("\n")) {
5288
+ for (const raw of text2.split("\n")) {
4677
5289
  const line = raw.trim();
4678
5290
  if (!line || /^Checking MCP/i.test(line)) continue;
4679
5291
  const m = line.match(/^(.+?):\s+\S+/);
@@ -4767,8 +5379,8 @@ function isBrightsyConnected() {
4767
5379
  return false;
4768
5380
  }
4769
5381
  }
4770
- function promptMentionsBrightsy(text) {
4771
- return BRIGHTSY_WORD.test(text ?? "");
5382
+ function promptMentionsBrightsy(text2) {
5383
+ return BRIGHTSY_WORD.test(text2 ?? "");
4772
5384
  }
4773
5385
  function isBrightsyMcpToolName(name) {
4774
5386
  const n = name.toLowerCase();
@@ -4831,35 +5443,35 @@ function corePackageDir() {
4831
5443
  try {
4832
5444
  const url = import_meta.url;
4833
5445
  if (typeof url === "string" && url.length > 0) {
4834
- return (0, import_node_path10.dirname)((0, import_node_url.fileURLToPath)(url));
5446
+ return (0, import_node_path12.dirname)((0, import_node_url.fileURLToPath)(url));
4835
5447
  }
4836
5448
  } catch {
4837
5449
  }
4838
5450
  try {
4839
- const req = (0, import_node_module.createRequire)((0, import_node_path10.join)(process.cwd(), "package.json"));
4840
- return (0, import_node_path10.dirname)(req.resolve("@sideboard-ai/core"));
5451
+ const req = (0, import_node_module.createRequire)((0, import_node_path12.join)(process.cwd(), "package.json"));
5452
+ return (0, import_node_path12.dirname)(req.resolve("@sideboard-ai/core"));
4841
5453
  } catch {
4842
5454
  return process.cwd();
4843
5455
  }
4844
5456
  }
4845
5457
  function findSideboardMcpJsEntry() {
4846
5458
  const override = process.env.SIDEBOARD_MCP_ENTRY?.trim() || process.env.SIDEBOARD_CLI?.trim();
4847
- if (override && (0, import_node_fs11.existsSync)(override)) return override;
5459
+ if (override && (0, import_node_fs13.existsSync)(override)) return override;
4848
5460
  let dir = corePackageDir();
4849
5461
  for (let i = 0; i < 10; i++) {
4850
5462
  const candidates = [
4851
- (0, import_node_path10.join)(dir, "mcp/run-stdio.js"),
4852
- (0, import_node_path10.join)(dir, "mcp/run-stdio.cjs"),
4853
- (0, import_node_path10.join)(dir, "dist/mcp/run-stdio.js"),
4854
- (0, import_node_path10.join)(dir, "dist/mcp/run-stdio.cjs"),
4855
- (0, import_node_path10.join)(dir, "packages/core/dist/mcp/run-stdio.js"),
4856
- (0, import_node_path10.join)(dir, "packages/cli/dist/index.js"),
4857
- (0, import_node_path10.join)(dir, "cli/dist/index.js")
5463
+ (0, import_node_path12.join)(dir, "mcp/run-stdio.js"),
5464
+ (0, import_node_path12.join)(dir, "mcp/run-stdio.cjs"),
5465
+ (0, import_node_path12.join)(dir, "dist/mcp/run-stdio.js"),
5466
+ (0, import_node_path12.join)(dir, "dist/mcp/run-stdio.cjs"),
5467
+ (0, import_node_path12.join)(dir, "packages/core/dist/mcp/run-stdio.js"),
5468
+ (0, import_node_path12.join)(dir, "packages/cli/dist/index.js"),
5469
+ (0, import_node_path12.join)(dir, "cli/dist/index.js")
4858
5470
  ];
4859
5471
  for (const p of candidates) {
4860
- if ((0, import_node_fs11.existsSync)(p)) return p;
5472
+ if ((0, import_node_fs13.existsSync)(p)) return p;
4861
5473
  }
4862
- const parent = (0, import_node_path10.dirname)(dir);
5474
+ const parent = (0, import_node_path12.dirname)(dir);
4863
5475
  if (parent === dir) break;
4864
5476
  dir = parent;
4865
5477
  }
@@ -4969,19 +5581,19 @@ function writeMcpServersConfig(servers) {
4969
5581
  ...s.env ? { env: s.env } : {}
4970
5582
  };
4971
5583
  }
4972
- const dir = (0, import_node_fs11.mkdtempSync)((0, import_node_path10.join)((0, import_node_os6.tmpdir)(), "sideboard-mcp-"));
4973
- const cfgPath = (0, import_node_path10.join)(dir, "mcp.json");
4974
- (0, import_node_fs11.writeFileSync)(cfgPath, JSON.stringify({ mcpServers }, null, 2));
5584
+ const dir = (0, import_node_fs13.mkdtempSync)((0, import_node_path12.join)((0, import_node_os6.tmpdir)(), "sideboard-mcp-"));
5585
+ const cfgPath = (0, import_node_path12.join)(dir, "mcp.json");
5586
+ (0, import_node_fs13.writeFileSync)(cfgPath, JSON.stringify({ mcpServers }, null, 2));
4975
5587
  return cfgPath;
4976
5588
  }
4977
- var import_node_fs11, import_node_module, import_node_os6, import_node_path10, import_node_url, import_meta, SIDEBOARD_MCP_ALLOWED_TOOLS, SIDEBOARD_ARTIFACT_MCP_ALLOWED_TOOLS, brightsyMcpCommandCache, BRIGHTSY_WORD;
5589
+ var import_node_fs13, import_node_module, import_node_os6, import_node_path12, import_node_url, import_meta, SIDEBOARD_MCP_ALLOWED_TOOLS, SIDEBOARD_ARTIFACT_MCP_ALLOWED_TOOLS, brightsyMcpCommandCache, BRIGHTSY_WORD;
4978
5590
  var init_injected_mcp = __esm({
4979
5591
  "src/agents/injected-mcp.ts"() {
4980
5592
  "use strict";
4981
- import_node_fs11 = require("fs");
5593
+ import_node_fs13 = require("fs");
4982
5594
  import_node_module = require("module");
4983
5595
  import_node_os6 = require("os");
4984
- import_node_path10 = require("path");
5596
+ import_node_path12 = require("path");
4985
5597
  import_node_url = require("url");
4986
5598
  init_run();
4987
5599
  init_config();
@@ -5000,7 +5612,13 @@ var init_injected_mcp = __esm({
5000
5612
  "mcp__sideboard__present_schema",
5001
5613
  "mcp__sideboard__present_files",
5002
5614
  "mcp__sideboard__ask_user",
5003
- "mcp__sideboard__present_plan"
5615
+ "mcp__sideboard__present_plan",
5616
+ "mcp__sideboard__list_teams",
5617
+ "mcp__sideboard__slack_list_channels",
5618
+ "mcp__sideboard__slack_list_users",
5619
+ "mcp__sideboard__slack_search",
5620
+ "mcp__sideboard__slack_read",
5621
+ "mcp__sideboard__slack_post"
5004
5622
  ];
5005
5623
  brightsyMcpCommandCache = null;
5006
5624
  BRIGHTSY_WORD = /(?<![a-z0-9_-])brightsy(?![a-z0-9_-])/i;
@@ -5134,9 +5752,9 @@ function eventsFromContentBlocks(blocks) {
5134
5752
  return out;
5135
5753
  }
5136
5754
  function parseIssuesJson(raw) {
5137
- const text = raw.trim();
5138
- const candidates = [text];
5139
- const match = text.match(/\[[\s\S]*\]/);
5755
+ const text2 = raw.trim();
5756
+ const candidates = [text2];
5757
+ const match = text2.match(/\[[\s\S]*\]/);
5140
5758
  if (match) candidates.push(match[0]);
5141
5759
  for (const c of candidates) {
5142
5760
  try {
@@ -5158,11 +5776,11 @@ function parseIssuesJson(raw) {
5158
5776
  }
5159
5777
  return [];
5160
5778
  }
5161
- var import_node_fs12, BASE_ALLOWED_TOOLS, CLAUDE_CHROME_ALLOWED_TOOLS, CLAUDE_PROMPT_ARG_MAX, claudeAdapter;
5779
+ var import_node_fs14, BASE_ALLOWED_TOOLS, CLAUDE_CHROME_ALLOWED_TOOLS, CLAUDE_PROMPT_ARG_MAX, claudeAdapter;
5162
5780
  var init_claude = __esm({
5163
5781
  "src/agents/claude.ts"() {
5164
5782
  "use strict";
5165
- import_node_fs12 = require("fs");
5783
+ import_node_fs14 = require("fs");
5166
5784
  init_run();
5167
5785
  init_app_settings();
5168
5786
  init_claude_mcp();
@@ -5191,7 +5809,7 @@ var init_claude = __esm({
5191
5809
  async detect() {
5192
5810
  const claude = resolveClaudeExecutable();
5193
5811
  if (claude !== "claude") {
5194
- if (!(0, import_node_fs12.existsSync)(claude)) {
5812
+ if (!(0, import_node_fs14.existsSync)(claude)) {
5195
5813
  return {
5196
5814
  agent: "claude",
5197
5815
  installed: false,
@@ -5365,8 +5983,8 @@ var init_claude = __esm({
5365
5983
  if (errorDetail) {
5366
5984
  events.push({ type: "stderr", data: errorDetail });
5367
5985
  } else {
5368
- const text = obj.result;
5369
- if (typeof text === "string" && text) events.push({ type: "stdout", data: text });
5986
+ const text2 = obj.result;
5987
+ if (typeof text2 === "string" && text2) events.push({ type: "stdout", data: text2 });
5370
5988
  }
5371
5989
  const usage = usageFromClaude(obj.usage);
5372
5990
  if (usage) events.push({ type: "usage", data: usage });
@@ -5424,7 +6042,7 @@ async function listCodexModels() {
5424
6042
  if (codex === "codex") {
5425
6043
  const which = await run("which", ["codex"], { reject: false });
5426
6044
  if (which.exitCode !== 0) return FALLBACK_CODEX_MODELS;
5427
- } else if (!(0, import_node_fs13.existsSync)(codex)) {
6045
+ } else if (!(0, import_node_fs15.existsSync)(codex)) {
5428
6046
  return FALLBACK_CODEX_MODELS;
5429
6047
  }
5430
6048
  const listed = await run(codex, ["debug", "models"], { reject: false });
@@ -5462,13 +6080,13 @@ function usageFromCodex(usage) {
5462
6080
  }
5463
6081
  function codexConfigHasNetworkAccess() {
5464
6082
  const candidates = [
5465
- (0, import_node_path11.join)((0, import_node_os7.homedir)(), ".codex", "config.toml"),
5466
- (0, import_node_path11.join)((0, import_node_os7.homedir)(), ".config", "codex", "config.toml")
6083
+ (0, import_node_path13.join)((0, import_node_os7.homedir)(), ".codex", "config.toml"),
6084
+ (0, import_node_path13.join)((0, import_node_os7.homedir)(), ".config", "codex", "config.toml")
5467
6085
  ];
5468
6086
  for (const path of candidates) {
5469
- if (!(0, import_node_fs13.existsSync)(path)) continue;
5470
- const text = (0, import_node_fs13.readFileSync)(path, "utf8");
5471
- if (/network_access\s*=\s*true/.test(text)) return true;
6087
+ if (!(0, import_node_fs15.existsSync)(path)) continue;
6088
+ const text2 = (0, import_node_fs15.readFileSync)(path, "utf8");
6089
+ if (/network_access\s*=\s*true/.test(text2)) return true;
5472
6090
  }
5473
6091
  return false;
5474
6092
  }
@@ -5481,8 +6099,8 @@ function unwrapCodexMcpResult(result) {
5481
6099
  const texts = [];
5482
6100
  for (const item of rec.content) {
5483
6101
  if (!item || typeof item !== "object") continue;
5484
- const text = item.text;
5485
- if (typeof text === "string") texts.push(text);
6102
+ const text2 = item.text;
6103
+ if (typeof text2 === "string") texts.push(text2);
5486
6104
  }
5487
6105
  if (texts.length) return texts.join("\n");
5488
6106
  }
@@ -5499,21 +6117,21 @@ function asRecord(value) {
5499
6117
  return void 0;
5500
6118
  }
5501
6119
  function codexLooksAuthenticated() {
5502
- const authPath = (0, import_node_path11.join)((0, import_node_os7.homedir)(), ".codex", "auth.json");
5503
- if (!(0, import_node_fs13.existsSync)(authPath)) return false;
6120
+ const authPath = (0, import_node_path13.join)((0, import_node_os7.homedir)(), ".codex", "auth.json");
6121
+ if (!(0, import_node_fs15.existsSync)(authPath)) return false;
5504
6122
  try {
5505
- return (0, import_node_fs13.statSync)(authPath).size > 2;
6123
+ return (0, import_node_fs15.statSync)(authPath).size > 2;
5506
6124
  } catch {
5507
6125
  return false;
5508
6126
  }
5509
6127
  }
5510
- var import_node_fs13, import_node_os7, import_node_path11, CODEX_PROMPT_ARG_MAX, FALLBACK_CODEX_MODELS, cachedCodexModels, CODEX_MODEL_CACHE_MS, codexAdapter;
6128
+ var import_node_fs15, import_node_os7, import_node_path13, CODEX_PROMPT_ARG_MAX, FALLBACK_CODEX_MODELS, cachedCodexModels, CODEX_MODEL_CACHE_MS, codexAdapter;
5511
6129
  var init_codex = __esm({
5512
6130
  "src/agents/codex.ts"() {
5513
6131
  "use strict";
5514
- import_node_fs13 = require("fs");
6132
+ import_node_fs15 = require("fs");
5515
6133
  import_node_os7 = require("os");
5516
- import_node_path11 = require("path");
6134
+ import_node_path13 = require("path");
5517
6135
  init_run();
5518
6136
  init_app_settings();
5519
6137
  init_global_workspace();
@@ -5536,7 +6154,7 @@ var init_codex = __esm({
5536
6154
  async detect() {
5537
6155
  const codex = resolveAgentExecutable("codex");
5538
6156
  if (codex !== "codex") {
5539
- if (!(0, import_node_fs13.existsSync)(codex)) {
6157
+ if (!(0, import_node_fs15.existsSync)(codex)) {
5540
6158
  return {
5541
6159
  agent: "codex",
5542
6160
  installed: false,
@@ -5968,11 +6586,11 @@ function entryDir() {
5968
6586
  const cjsDir = typeof __dirname !== "undefined" ? __dirname : "";
5969
6587
  if (cjsDir) return cjsDir;
5970
6588
  try {
5971
- return (0, import_node_path12.dirname)((0, import_node_url2.fileURLToPath)(import_meta2.url));
6589
+ return (0, import_node_path14.dirname)((0, import_node_url2.fileURLToPath)(import_meta2.url));
5972
6590
  } catch {
5973
6591
  try {
5974
6592
  const req = (0, import_node_module2.createRequire)(process.cwd() + "/");
5975
- return (0, import_node_path12.dirname)(req.resolve("@sideboard-ai/core"));
6593
+ return (0, import_node_path14.dirname)(req.resolve("@sideboard-ai/core"));
5976
6594
  } catch {
5977
6595
  return process.cwd();
5978
6596
  }
@@ -5981,27 +6599,27 @@ function entryDir() {
5981
6599
  function cursorRunnerPath() {
5982
6600
  const root = entryDir();
5983
6601
  const candidates = [
5984
- (0, import_node_path12.join)(root, "agents", "cursor-runner.js"),
5985
- (0, import_node_path12.join)(root, "agents", "cursor-runner.cjs"),
6602
+ (0, import_node_path14.join)(root, "agents", "cursor-runner.js"),
6603
+ (0, import_node_path14.join)(root, "agents", "cursor-runner.cjs"),
5986
6604
  // If somehow resolved from package root instead of dist/
5987
- (0, import_node_path12.join)(root, "dist", "agents", "cursor-runner.js"),
5988
- (0, import_node_path12.join)(root, "dist", "agents", "cursor-runner.cjs"),
6605
+ (0, import_node_path14.join)(root, "dist", "agents", "cursor-runner.js"),
6606
+ (0, import_node_path14.join)(root, "dist", "agents", "cursor-runner.cjs"),
5989
6607
  // Source tree (dev): packages/core/src/agents/cursor-runner.ts
5990
- (0, import_node_path12.join)(root, "cursor-runner.ts"),
5991
- (0, import_node_path12.join)(root, "src", "agents", "cursor-runner.ts")
6608
+ (0, import_node_path14.join)(root, "cursor-runner.ts"),
6609
+ (0, import_node_path14.join)(root, "src", "agents", "cursor-runner.ts")
5992
6610
  ];
5993
6611
  for (const candidate of candidates) {
5994
- if ((0, import_node_fs14.existsSync)(candidate)) return candidate;
6612
+ if ((0, import_node_fs16.existsSync)(candidate)) return candidate;
5995
6613
  }
5996
6614
  return candidates[0];
5997
6615
  }
5998
- var import_node_fs14, import_node_module2, import_node_path12, import_node_url2, import_sdk, import_meta2, FALLBACK_CURSOR_MODELS, cachedModels, MODEL_CACHE_MS, cursorAdapter;
6616
+ var import_node_fs16, import_node_module2, import_node_path14, import_node_url2, import_sdk, import_meta2, FALLBACK_CURSOR_MODELS, cachedModels, MODEL_CACHE_MS, cursorAdapter;
5999
6617
  var init_cursor = __esm({
6000
6618
  "src/agents/cursor.ts"() {
6001
6619
  "use strict";
6002
- import_node_fs14 = require("fs");
6620
+ import_node_fs16 = require("fs");
6003
6621
  import_node_module2 = require("module");
6004
- import_node_path12 = require("path");
6622
+ import_node_path14 = require("path");
6005
6623
  import_node_url2 = require("url");
6006
6624
  import_sdk = require("@cursor/sdk");
6007
6625
  init_run();
@@ -6127,7 +6745,7 @@ async function listOpencodeModels() {
6127
6745
  if (opencode === "opencode") {
6128
6746
  const which = await run("which", ["opencode"], { reject: false });
6129
6747
  if (which.exitCode !== 0) return FALLBACK_OPENCODE_MODELS;
6130
- } else if (!(0, import_node_fs15.existsSync)(opencode)) {
6748
+ } else if (!(0, import_node_fs17.existsSync)(opencode)) {
6131
6749
  return FALLBACK_OPENCODE_MODELS;
6132
6750
  }
6133
6751
  const listed = await run(opencode, ["models"], { reject: false });
@@ -6157,11 +6775,11 @@ function usageFromOpencode(tokens) {
6157
6775
  cacheWriteTokens: tokens.cache?.write ? Number(tokens.cache.write) : void 0
6158
6776
  };
6159
6777
  }
6160
- var import_node_fs15, FALLBACK_OPENCODE_MODELS, cachedOpencodeModels, OPENCODE_MODEL_CACHE_MS, opencodeAdapter;
6778
+ var import_node_fs17, FALLBACK_OPENCODE_MODELS, cachedOpencodeModels, OPENCODE_MODEL_CACHE_MS, opencodeAdapter;
6161
6779
  var init_opencode = __esm({
6162
6780
  "src/agents/opencode.ts"() {
6163
6781
  "use strict";
6164
- import_node_fs15 = require("fs");
6782
+ import_node_fs17 = require("fs");
6165
6783
  init_run();
6166
6784
  init_app_settings();
6167
6785
  init_global_workspace();
@@ -6187,7 +6805,7 @@ var init_opencode = __esm({
6187
6805
  async detect() {
6188
6806
  const opencode = resolveAgentExecutable("opencode");
6189
6807
  if (opencode !== "opencode") {
6190
- if (!(0, import_node_fs15.existsSync)(opencode)) {
6808
+ if (!(0, import_node_fs17.existsSync)(opencode)) {
6191
6809
  return {
6192
6810
  agent: "opencode",
6193
6811
  installed: false,
@@ -6277,8 +6895,8 @@ var init_opencode = __esm({
6277
6895
  return { type: "session_id", data: sid };
6278
6896
  }
6279
6897
  if (obj.type === "text") {
6280
- const text = obj.part?.text ?? obj.text;
6281
- if (text) return { type: "stdout", data: text };
6898
+ const text2 = obj.part?.text ?? obj.text;
6899
+ if (text2) return { type: "stdout", data: text2 };
6282
6900
  }
6283
6901
  if (obj.type === "tool_use") {
6284
6902
  const part = obj.part;
@@ -6491,8 +7109,8 @@ var init_list_models = __esm({
6491
7109
  });
6492
7110
 
6493
7111
  // src/agents/session-quota.ts
6494
- function isSessionQuotaLimit(text) {
6495
- const lower = text.trim().toLowerCase();
7112
+ function isSessionQuotaLimit(text2) {
7113
+ const lower = text2.trim().toLowerCase();
6496
7114
  if (!lower) return false;
6497
7115
  if (/credit balance is too low|out of credits|insufficient.?quota|billing/.test(lower)) {
6498
7116
  return false;
@@ -6500,10 +7118,10 @@ function isSessionQuotaLimit(text) {
6500
7118
  if (/prompt is too long|context.*(too long|exceed)|conversation too long/.test(lower)) {
6501
7119
  return false;
6502
7120
  }
6503
- return /you've hit your/.test(lower) || /hit your (session|weekly|opus) limit/.test(lower) || /usage limit/.test(lower) || /rate.?limit|too many requests|\b429\b/.test(lower) && /reset/i.test(text);
7121
+ return /you've hit your/.test(lower) || /hit your (session|weekly|opus) limit/.test(lower) || /usage limit/.test(lower) || /rate.?limit|too many requests|\b429\b/.test(lower) && /reset/i.test(text2);
6504
7122
  }
6505
- function parseSessionQuotaResetAt(text, now = /* @__PURE__ */ new Date()) {
6506
- const absolute = text.match(
7123
+ function parseSessionQuotaResetAt(text2, now = /* @__PURE__ */ new Date()) {
7124
+ const absolute = text2.match(
6507
7125
  /resets\s+(?:at\s+)?(\d{1,2}):(\d{2})\s*(am|pm)(?:\s*\(([^)]+)\))?/i
6508
7126
  );
6509
7127
  if (absolute) {
@@ -6521,7 +7139,7 @@ function parseSessionQuotaResetAt(text, now = /* @__PURE__ */ new Date()) {
6521
7139
  }
6522
7140
  return at;
6523
7141
  }
6524
- const relative = text.match(
7142
+ const relative = text2.match(
6525
7143
  /resets\s+in\s+(\d+)\s*(minutes?|hours?|days?)/i
6526
7144
  );
6527
7145
  if (relative) {
@@ -6924,38 +7542,38 @@ __export(workspaces_exports, {
6924
7542
  syncWorkspacesFromThreads: () => syncWorkspacesFromThreads
6925
7543
  });
6926
7544
  function workspacesFile() {
6927
- return (0, import_node_path16.join)(appDataDir(), "workspaces.json");
7545
+ return (0, import_node_path18.join)(appDataDir(), "workspaces.json");
6928
7546
  }
6929
7547
  function removedWorkspacesFile() {
6930
- return (0, import_node_path16.join)(appDataDir(), "removed-workspaces.json");
7548
+ return (0, import_node_path18.join)(appDataDir(), "removed-workspaces.json");
6931
7549
  }
6932
7550
  function readAll() {
6933
7551
  const path = workspacesFile();
6934
- if (!(0, import_node_fs19.existsSync)(path)) return [];
7552
+ if (!(0, import_node_fs21.existsSync)(path)) return [];
6935
7553
  try {
6936
- const raw = JSON.parse((0, import_node_fs19.readFileSync)(path, "utf8"));
7554
+ const raw = JSON.parse((0, import_node_fs21.readFileSync)(path, "utf8"));
6937
7555
  return Array.isArray(raw) ? raw : [];
6938
7556
  } catch {
6939
7557
  return [];
6940
7558
  }
6941
7559
  }
6942
7560
  function writeAll(list) {
6943
- (0, import_node_fs19.mkdirSync)(appDataDir(), { recursive: true });
6944
- (0, import_node_fs19.writeFileSync)(workspacesFile(), JSON.stringify(list, null, 2), "utf8");
7561
+ (0, import_node_fs21.mkdirSync)(appDataDir(), { recursive: true });
7562
+ (0, import_node_fs21.writeFileSync)(workspacesFile(), JSON.stringify(list, null, 2), "utf8");
6945
7563
  }
6946
7564
  function readRemoved() {
6947
7565
  const path = removedWorkspacesFile();
6948
- if (!(0, import_node_fs19.existsSync)(path)) return /* @__PURE__ */ new Set();
7566
+ if (!(0, import_node_fs21.existsSync)(path)) return /* @__PURE__ */ new Set();
6949
7567
  try {
6950
- const raw = JSON.parse((0, import_node_fs19.readFileSync)(path, "utf8"));
7568
+ const raw = JSON.parse((0, import_node_fs21.readFileSync)(path, "utf8"));
6951
7569
  return new Set(Array.isArray(raw) ? raw.filter((p) => typeof p === "string") : []);
6952
7570
  } catch {
6953
7571
  return /* @__PURE__ */ new Set();
6954
7572
  }
6955
7573
  }
6956
7574
  function writeRemoved(paths) {
6957
- (0, import_node_fs19.mkdirSync)(appDataDir(), { recursive: true });
6958
- (0, import_node_fs19.writeFileSync)(removedWorkspacesFile(), JSON.stringify([...paths].sort(), null, 2), "utf8");
7575
+ (0, import_node_fs21.mkdirSync)(appDataDir(), { recursive: true });
7576
+ (0, import_node_fs21.writeFileSync)(removedWorkspacesFile(), JSON.stringify([...paths].sort(), null, 2), "utf8");
6959
7577
  }
6960
7578
  function rememberRemoved(repoPath) {
6961
7579
  const next = readRemoved();
@@ -6978,7 +7596,7 @@ function listWorkspaces() {
6978
7596
  async function addWorkspace(repoPath) {
6979
7597
  const root = await resolveRepoRoot(repoPath);
6980
7598
  if (!root || root === "/") throw new Error(`Invalid repo path: ${repoPath}`);
6981
- if (!(0, import_node_fs19.existsSync)(root)) throw new Error(`Repo not found: ${root}`);
7599
+ if (!(0, import_node_fs21.existsSync)(root)) throw new Error(`Repo not found: ${root}`);
6982
7600
  forgetRemoved(root);
6983
7601
  await ensureGhPreferOrigin(root);
6984
7602
  const current = readAll();
@@ -6986,7 +7604,7 @@ async function addWorkspace(repoPath) {
6986
7604
  if (existing) return existing;
6987
7605
  const next = {
6988
7606
  path: root,
6989
- name: (0, import_node_path16.basename)(root),
7607
+ name: (0, import_node_path18.basename)(root),
6990
7608
  addedAt: (/* @__PURE__ */ new Date()).toISOString()
6991
7609
  };
6992
7610
  writeAll([...current, next]);
@@ -7008,10 +7626,10 @@ function syncWorkspacesFromThreads(repoPaths) {
7008
7626
  if (!path || path === "/" || isGlobalRepoPath(path) || byPath.has(path) || removed.has(path)) {
7009
7627
  continue;
7010
7628
  }
7011
- if (!(0, import_node_fs19.existsSync)(path)) continue;
7629
+ if (!(0, import_node_fs21.existsSync)(path)) continue;
7012
7630
  const ws = {
7013
7631
  path,
7014
- name: (0, import_node_path16.basename)(path),
7632
+ name: (0, import_node_path18.basename)(path),
7015
7633
  addedAt: (/* @__PURE__ */ new Date()).toISOString()
7016
7634
  };
7017
7635
  byPath.set(path, ws);
@@ -7021,12 +7639,12 @@ function syncWorkspacesFromThreads(repoPaths) {
7021
7639
  if (dirty) writeAll(next);
7022
7640
  return next.sort((a, b) => a.name.localeCompare(b.name));
7023
7641
  }
7024
- var import_node_fs19, import_node_path16;
7642
+ var import_node_fs21, import_node_path18;
7025
7643
  var init_workspaces = __esm({
7026
7644
  "src/store/workspaces.ts"() {
7027
7645
  "use strict";
7028
- import_node_fs19 = require("fs");
7029
- import_node_path16 = require("path");
7646
+ import_node_fs21 = require("fs");
7647
+ import_node_path18 = require("path");
7030
7648
  init_paths();
7031
7649
  init_global_workspace();
7032
7650
  init_worktree();
@@ -7067,11 +7685,11 @@ function resolvePlanMarkdown(opts) {
7067
7685
  source: "exit_plan"
7068
7686
  };
7069
7687
  }
7070
- const text = opts.text?.trim();
7071
- if (text && text.length >= 80) {
7688
+ const text2 = opts.text?.trim();
7689
+ if (text2 && text2.length >= 80) {
7072
7690
  return {
7073
7691
  title: "Plan",
7074
- content: text,
7692
+ content: text2,
7075
7693
  path: PLAN_FILE_REL,
7076
7694
  source: "text"
7077
7695
  };
@@ -7103,40 +7721,40 @@ __export(plan_file_exports, {
7103
7721
  writePlanFile: () => writePlanFile
7104
7722
  });
7105
7723
  function ensureAttachmentsGitignore2(worktreePath) {
7106
- const gitignoreAbs = (0, import_node_path24.join)(worktreePath, ATTACHMENTS_DIR, ".gitignore");
7107
- if ((0, import_node_fs29.existsSync)(gitignoreAbs)) return;
7108
- (0, import_node_fs29.mkdirSync)((0, import_node_path24.dirname)(gitignoreAbs), { recursive: true });
7109
- (0, import_node_fs29.writeFileSync)(gitignoreAbs, attachmentsGitignoreBody(), "utf8");
7724
+ const gitignoreAbs = (0, import_node_path26.join)(worktreePath, ATTACHMENTS_DIR, ".gitignore");
7725
+ if ((0, import_node_fs31.existsSync)(gitignoreAbs)) return;
7726
+ (0, import_node_fs31.mkdirSync)((0, import_node_path26.dirname)(gitignoreAbs), { recursive: true });
7727
+ (0, import_node_fs31.writeFileSync)(gitignoreAbs, attachmentsGitignoreBody(), "utf8");
7110
7728
  }
7111
7729
  function planFileAbs(worktreePath) {
7112
- return (0, import_node_path24.join)(worktreePath, PLAN_FILE_REL);
7730
+ return (0, import_node_path26.join)(worktreePath, PLAN_FILE_REL);
7113
7731
  }
7114
7732
  function readTextIfPresent2(abs) {
7115
- if (!(0, import_node_fs29.existsSync)(abs)) return null;
7733
+ if (!(0, import_node_fs31.existsSync)(abs)) return null;
7116
7734
  try {
7117
- const content = (0, import_node_fs29.readFileSync)(abs, "utf8");
7735
+ const content = (0, import_node_fs31.readFileSync)(abs, "utf8");
7118
7736
  return content.trim() ? content : null;
7119
7737
  } catch {
7120
7738
  return null;
7121
7739
  }
7122
7740
  }
7123
7741
  function readPlanFile(worktreePath) {
7124
- return readTextIfPresent2(planFileAbs(worktreePath)) ?? readTextIfPresent2((0, import_node_path24.join)(worktreePath, `${LEGACY_ATTACHMENTS_DIR}/plan.md`)) ?? readTextIfPresent2((0, import_node_path24.join)(worktreePath, LEGACY_PLAN_FILE_REL));
7742
+ return readTextIfPresent2(planFileAbs(worktreePath)) ?? readTextIfPresent2((0, import_node_path26.join)(worktreePath, `${LEGACY_ATTACHMENTS_DIR}/plan.md`)) ?? readTextIfPresent2((0, import_node_path26.join)(worktreePath, LEGACY_PLAN_FILE_REL));
7125
7743
  }
7126
7744
  function writePlanFile(worktreePath, content) {
7127
7745
  ensureAttachmentsGitignore2(worktreePath);
7128
7746
  const abs = planFileAbs(worktreePath);
7129
- (0, import_node_fs29.mkdirSync)((0, import_node_path24.dirname)(abs), { recursive: true });
7747
+ (0, import_node_fs31.mkdirSync)((0, import_node_path26.dirname)(abs), { recursive: true });
7130
7748
  const body = content.trimEnd() + (content.endsWith("\n") ? "" : "\n");
7131
- (0, import_node_fs29.writeFileSync)(abs, body, "utf8");
7749
+ (0, import_node_fs31.writeFileSync)(abs, body, "utf8");
7132
7750
  return PLAN_FILE_REL;
7133
7751
  }
7134
- var import_node_fs29, import_node_path24;
7752
+ var import_node_fs31, import_node_path26;
7135
7753
  var init_plan_file = __esm({
7136
7754
  "src/plan/plan-file.ts"() {
7137
7755
  "use strict";
7138
- import_node_fs29 = require("fs");
7139
- import_node_path24 = require("path");
7756
+ import_node_fs31 = require("fs");
7757
+ import_node_path26 = require("path");
7140
7758
  init_workspace_scratch();
7141
7759
  init_plan_present();
7142
7760
  init_plan_present();
@@ -7151,10 +7769,10 @@ __export(cursor_recover_exports, {
7151
7769
  function recoverFinishedCursorRun(opts) {
7152
7770
  const agentId = opts.agentId.trim();
7153
7771
  if (!agentId) return null;
7154
- const runsPath = (0, import_node_path25.join)(appDataDir(), "cursor-sdk-store", "runs.ndjson");
7155
- if (!(0, import_node_fs30.existsSync)(runsPath)) return null;
7772
+ const runsPath = (0, import_node_path27.join)(appDataDir(), "cursor-sdk-store", "runs.ndjson");
7773
+ if (!(0, import_node_fs32.existsSync)(runsPath)) return null;
7156
7774
  try {
7157
- const lines = (0, import_node_fs30.readFileSync)(runsPath, "utf8").split("\n");
7775
+ const lines = (0, import_node_fs32.readFileSync)(runsPath, "utf8").split("\n");
7158
7776
  let best = null;
7159
7777
  for (const line of lines) {
7160
7778
  const trimmed = line.trim();
@@ -7180,25 +7798,133 @@ function recoverFinishedCursorRun(opts) {
7180
7798
  return null;
7181
7799
  }
7182
7800
  }
7183
- var import_node_fs30, import_node_path25;
7801
+ var import_node_fs32, import_node_path27;
7184
7802
  var init_cursor_recover = __esm({
7185
7803
  "src/agents/cursor-recover.ts"() {
7186
7804
  "use strict";
7187
- import_node_fs30 = require("fs");
7188
- import_node_path25 = require("path");
7805
+ import_node_fs32 = require("fs");
7806
+ import_node_path27 = require("path");
7189
7807
  init_paths();
7190
7808
  }
7191
7809
  });
7192
7810
 
7811
+ // src/store/caffeinate-hold.ts
7812
+ var caffeinate_hold_exports = {};
7813
+ __export(caffeinate_hold_exports, {
7814
+ caffeinateHoldPath: () => caffeinateHoldPath,
7815
+ getCaffeinateHold: () => getCaffeinateHold,
7816
+ setCaffeinateHold: () => setCaffeinateHold,
7817
+ setCaffeinateHoldHooks: () => setCaffeinateHoldHooks
7818
+ });
7819
+ function setCaffeinateHoldHooks(next) {
7820
+ hooks = next;
7821
+ }
7822
+ function caffeinateHoldPath() {
7823
+ return (0, import_node_path30.join)(appDataDir(), "caffeinate-hold.json");
7824
+ }
7825
+ function processAlive(pid) {
7826
+ if (hooks.processAlive) return hooks.processAlive(pid);
7827
+ try {
7828
+ process.kill(pid, 0);
7829
+ return true;
7830
+ } catch {
7831
+ return false;
7832
+ }
7833
+ }
7834
+ function killPid(pid) {
7835
+ if (hooks.kill) {
7836
+ hooks.kill(pid);
7837
+ return;
7838
+ }
7839
+ try {
7840
+ process.kill(pid, "SIGTERM");
7841
+ } catch {
7842
+ }
7843
+ }
7844
+ function readHold() {
7845
+ const path = caffeinateHoldPath();
7846
+ if (!(0, import_node_fs35.existsSync)(path)) return null;
7847
+ try {
7848
+ const parsed = JSON.parse((0, import_node_fs35.readFileSync)(path, "utf8"));
7849
+ if (typeof parsed?.pid === "number" && parsed.pid > 0) return parsed;
7850
+ } catch {
7851
+ }
7852
+ return null;
7853
+ }
7854
+ function writeHold(pid) {
7855
+ writePrivateFile(caffeinateHoldPath(), `${JSON.stringify({ pid })}
7856
+ `);
7857
+ }
7858
+ function clearHold() {
7859
+ try {
7860
+ (0, import_node_fs35.unlinkSync)(caffeinateHoldPath());
7861
+ } catch {
7862
+ }
7863
+ }
7864
+ function getCaffeinateHold() {
7865
+ const platform = hooks.platform ?? process.platform;
7866
+ const hold = readHold();
7867
+ if (!hold) {
7868
+ return { held: false, pid: null, running: false, platform };
7869
+ }
7870
+ const running = processAlive(hold.pid);
7871
+ if (!running) {
7872
+ clearHold();
7873
+ return { held: false, pid: null, running: false, platform };
7874
+ }
7875
+ return { held: true, pid: hold.pid, running: true, platform };
7876
+ }
7877
+ function setCaffeinateHold(enabled) {
7878
+ const platform = hooks.platform ?? process.platform;
7879
+ const current = getCaffeinateHold();
7880
+ if (!enabled) {
7881
+ if (current.pid) killPid(current.pid);
7882
+ clearHold();
7883
+ return { held: false, pid: null, running: false, platform };
7884
+ }
7885
+ if (current.running && current.pid) return current;
7886
+ if (platform !== "darwin") {
7887
+ return { held: false, pid: null, running: false, platform };
7888
+ }
7889
+ const spawnImpl = hooks.spawn ?? import_node_child_process4.spawn;
7890
+ const child = spawnImpl("caffeinate", ["-dimsu"], {
7891
+ detached: true,
7892
+ stdio: "ignore"
7893
+ });
7894
+ const pid = child.pid;
7895
+ if (!pid) {
7896
+ try {
7897
+ child.kill();
7898
+ } catch {
7899
+ }
7900
+ return { held: false, pid: null, running: false, platform };
7901
+ }
7902
+ child.unref();
7903
+ writeHold(pid);
7904
+ return { held: true, pid, running: true, platform };
7905
+ }
7906
+ var import_node_child_process4, import_node_fs35, import_node_path30, hooks;
7907
+ var init_caffeinate_hold = __esm({
7908
+ "src/store/caffeinate-hold.ts"() {
7909
+ "use strict";
7910
+ import_node_child_process4 = require("child_process");
7911
+ import_node_fs35 = require("fs");
7912
+ import_node_path30 = require("path");
7913
+ init_paths();
7914
+ init_private_file();
7915
+ hooks = {};
7916
+ }
7917
+ });
7918
+
7193
7919
  // src/mcp/server.ts
7194
7920
  var import_mcp = require("@modelcontextprotocol/sdk/server/mcp.js");
7195
7921
  var import_stdio = require("@modelcontextprotocol/sdk/server/stdio.js");
7196
- var import_zod = require("zod");
7197
- var import_node_path26 = require("path");
7922
+ var import_zod2 = require("zod");
7923
+ var import_node_path31 = require("path");
7198
7924
 
7199
7925
  // src/orchestrator/orchestrator.ts
7200
7926
  var import_node_events = require("events");
7201
- var import_node_fs31 = require("fs");
7927
+ var import_node_fs33 = require("fs");
7202
7928
  init_error_detail();
7203
7929
 
7204
7930
  // src/agents/spawn.ts
@@ -7285,9 +8011,9 @@ function toolFilePath(input) {
7285
8011
  if (!input) return void 0;
7286
8012
  return str2(input.file_path) ?? str2(input.path) ?? str2(input.filePath) ?? str2(input.filename);
7287
8013
  }
7288
- function countLines(text) {
7289
- if (!text) return 0;
7290
- return text.split("\n").length;
8014
+ function countLines(text2) {
8015
+ if (!text2) return 0;
8016
+ return text2.split("\n").length;
7291
8017
  }
7292
8018
  function diffFromInput(input) {
7293
8019
  if (!input) return {};
@@ -7414,9 +8140,9 @@ function applyAgentEvent(parts, event) {
7414
8140
  function partsToAssistantText(parts) {
7415
8141
  return parts.filter((p) => p.type === "text").map((p) => p.text).join("").trim();
7416
8142
  }
7417
- function stripBrightsyNdjsonNoise(text) {
7418
- if (!text || !text.includes('"type"')) return text;
7419
- let out = text;
8143
+ function stripBrightsyNdjsonNoise(text2) {
8144
+ if (!text2 || !text2.includes('"type"')) return text2;
8145
+ let out = text2;
7420
8146
  out = out.replace(
7421
8147
  /\{"type":"(?:tool_use|tool_result|tool|thinking|usage|done|error)"[\s\S]*?\}\s*(?=\{"type":"|$|(?=[A-Za-z*#]))/g,
7422
8148
  ""
@@ -7553,8 +8279,8 @@ async function spawnAgentTurn(thread, input, onEvent) {
7553
8279
  onEvent({ type: "exit", data: exitCode });
7554
8280
  const finalized = finalizeParts(parts);
7555
8281
  const rawText = assistantText.trim() || partsToAssistantText(finalized);
7556
- const text = thread.agent === "brightsy" ? stripBrightsyNdjsonNoise(rawText) : rawText;
7557
- return { exitCode, sessionId, assistantText: text, parts: finalized, usage };
8282
+ const text2 = thread.agent === "brightsy" ? stripBrightsyNdjsonNoise(rawText) : rawText;
8283
+ return { exitCode, sessionId, assistantText: text2, parts: finalized, usage };
7558
8284
  });
7559
8285
  return {
7560
8286
  pid: child.pid,
@@ -7588,9 +8314,9 @@ function shouldAutoArchiveOnPrMerge(opts) {
7588
8314
  }
7589
8315
 
7590
8316
  // src/hook/conductor.ts
7591
- var import_node_fs16 = require("fs");
8317
+ var import_node_fs18 = require("fs");
7592
8318
  var import_node_net = require("net");
7593
- var import_node_path13 = require("path");
8319
+ var import_node_path15 = require("path");
7594
8320
  var import_execa3 = require("execa");
7595
8321
  var import_node_readline2 = require("readline");
7596
8322
  init_settings();
@@ -7600,9 +8326,9 @@ function matchSimpleGlob(pattern, name) {
7600
8326
  return new RegExp(`^${escaped}$`).test(name);
7601
8327
  }
7602
8328
  function readWorktreeInclude(repoPath) {
7603
- const path = (0, import_node_path13.join)(repoPath, ".worktreeinclude");
7604
- if (!(0, import_node_fs16.existsSync)(path)) return [];
7605
- return (0, import_node_fs16.readFileSync)(path, "utf8").split("\n").map((l) => l.trim()).filter((l) => l && !l.startsWith("#"));
8329
+ const path = (0, import_node_path15.join)(repoPath, ".worktreeinclude");
8330
+ if (!(0, import_node_fs18.existsSync)(path)) return [];
8331
+ return (0, import_node_fs18.readFileSync)(path, "utf8").split("\n").map((l) => l.trim()).filter((l) => l && !l.startsWith("#"));
7606
8332
  }
7607
8333
  function resolveFilesToCopy(repoPath) {
7608
8334
  const fromInclude = readWorktreeInclude(repoPath);
@@ -7612,10 +8338,10 @@ function resolveFilesToCopy(repoPath) {
7612
8338
  if (settings?.fileIncludeGlobs?.length) {
7613
8339
  const matched = [];
7614
8340
  try {
7615
- for (const entry of (0, import_node_fs16.readdirSync)(repoPath, { withFileTypes: true })) {
8341
+ for (const entry of (0, import_node_fs18.readdirSync)(repoPath, { withFileTypes: true })) {
7616
8342
  if (!entry.isFile()) continue;
7617
8343
  for (const glob of settings.fileIncludeGlobs) {
7618
- if (matchSimpleGlob(glob, entry.name) || matchSimpleGlob((0, import_node_path13.basename)(glob), entry.name)) {
8344
+ if (matchSimpleGlob(glob, entry.name) || matchSimpleGlob((0, import_node_path15.basename)(glob), entry.name)) {
7619
8345
  matched.push(entry.name);
7620
8346
  break;
7621
8347
  }
@@ -7627,7 +8353,7 @@ function resolveFilesToCopy(repoPath) {
7627
8353
  }
7628
8354
  const defaults = [];
7629
8355
  try {
7630
- for (const entry of (0, import_node_fs16.readdirSync)(repoPath, { withFileTypes: true })) {
8356
+ for (const entry of (0, import_node_fs18.readdirSync)(repoPath, { withFileTypes: true })) {
7631
8357
  if (entry.isFile() && entry.name.startsWith(".env")) {
7632
8358
  defaults.push(entry.name);
7633
8359
  }
@@ -7641,11 +8367,11 @@ function copyConfiguredFiles(repoPath, worktreePath) {
7641
8367
  const patterns = resolveFilesToCopy(repoPath);
7642
8368
  const copied = [];
7643
8369
  for (const rel of patterns) {
7644
- const src = (0, import_node_path13.join)(repoPath, rel);
7645
- if (!(0, import_node_fs16.existsSync)(src)) continue;
7646
- const dest = (0, import_node_path13.join)(worktreePath, rel);
7647
- (0, import_node_fs16.mkdirSync)((0, import_node_path13.dirname)(dest), { recursive: true });
7648
- (0, import_node_fs16.copyFileSync)(src, dest);
8370
+ const src = (0, import_node_path15.join)(repoPath, rel);
8371
+ if (!(0, import_node_fs18.existsSync)(src)) continue;
8372
+ const dest = (0, import_node_path15.join)(worktreePath, rel);
8373
+ (0, import_node_fs18.mkdirSync)((0, import_node_path15.dirname)(dest), { recursive: true });
8374
+ (0, import_node_fs18.copyFileSync)(src, dest);
7649
8375
  copied.push(rel);
7650
8376
  }
7651
8377
  return copied;
@@ -7679,7 +8405,7 @@ async function captureLoginEnv() {
7679
8405
  }
7680
8406
  function buildWorkspaceScriptEnv(opts, baseEnv) {
7681
8407
  const env = { ...baseEnv ?? process.env };
7682
- const name = opts.workspaceName ?? (0, import_node_path13.basename)(opts.worktreePath);
8408
+ const name = opts.workspaceName ?? (0, import_node_path15.basename)(opts.worktreePath);
7683
8409
  const ports = opts.ports ?? [];
7684
8410
  const primary = ports[0];
7685
8411
  env.SIDEBOARD_WORKSPACE_NAME = name;
@@ -7898,15 +8624,15 @@ async function startDevServer(repoPath, worktreePath, onLine, opts) {
7898
8624
  }
7899
8625
 
7900
8626
  // src/hook/cursor-worktrees.ts
7901
- var import_node_fs17 = require("fs");
7902
- var import_node_path14 = require("path");
8627
+ var import_node_fs19 = require("fs");
8628
+ var import_node_path16 = require("path");
7903
8629
  var import_execa4 = require("execa");
7904
8630
  var import_node_readline3 = require("readline");
7905
8631
  function loadCursorWorktreesJson(rootPath) {
7906
- const path = (0, import_node_path14.join)(rootPath, ".cursor", "worktrees.json");
7907
- if (!(0, import_node_fs17.existsSync)(path)) return null;
8632
+ const path = (0, import_node_path16.join)(rootPath, ".cursor", "worktrees.json");
8633
+ if (!(0, import_node_fs19.existsSync)(path)) return null;
7908
8634
  try {
7909
- return JSON.parse((0, import_node_fs17.readFileSync)(path, "utf8"));
8635
+ return JSON.parse((0, import_node_fs19.readFileSync)(path, "utf8"));
7910
8636
  } catch {
7911
8637
  return null;
7912
8638
  }
@@ -7932,7 +8658,7 @@ async function runCursorWorktreeSetup(repoPath, worktreePath, onLine) {
7932
8658
  if (process.platform === "win32") {
7933
8659
  env.ROOT_WORKTREE_PATH = repoPath;
7934
8660
  }
7935
- const commands = Array.isArray(spec) ? spec : [spec.endsWith(".sh") || spec.endsWith(".ps1") ? (0, import_node_path14.join)(
8661
+ const commands = Array.isArray(spec) ? spec : [spec.endsWith(".sh") || spec.endsWith(".ps1") ? (0, import_node_path16.join)(
7936
8662
  fromWorktree ? worktreePath : repoPath,
7937
8663
  ".cursor",
7938
8664
  spec
@@ -7964,8 +8690,8 @@ async function runCursorWorktreeSetup(repoPath, worktreePath, onLine) {
7964
8690
  }
7965
8691
 
7966
8692
  // src/git/orphan-cleanup.ts
7967
- var import_node_fs18 = require("fs");
7968
- var import_node_path15 = require("path");
8693
+ var import_node_fs20 = require("fs");
8694
+ var import_node_path17 = require("path");
7969
8695
  init_worktree();
7970
8696
  init_thread_store();
7971
8697
  init_paths();
@@ -7980,9 +8706,9 @@ async function findOrphanWorktrees(repoPaths) {
7980
8706
  repoPaths?.length ? repoPaths : threads.map((t) => t.repoPath).filter(Boolean)
7981
8707
  );
7982
8708
  const homeRoot = sideboardWorkspacesDir();
7983
- if ((0, import_node_fs18.existsSync)(homeRoot)) {
8709
+ if ((0, import_node_fs20.existsSync)(homeRoot)) {
7984
8710
  try {
7985
- for (const entry of (0, import_node_fs18.readdirSync)(homeRoot, { withFileTypes: true })) {
8711
+ for (const entry of (0, import_node_fs20.readdirSync)(homeRoot, { withFileTypes: true })) {
7986
8712
  if (!entry.isDirectory()) continue;
7987
8713
  void entry;
7988
8714
  }
@@ -7992,7 +8718,7 @@ async function findOrphanWorktrees(repoPaths) {
7992
8718
  const orphans = [];
7993
8719
  const seen = /* @__PURE__ */ new Set();
7994
8720
  for (const repoPath of repos) {
7995
- if (!repoPath || !(0, import_node_fs18.existsSync)(repoPath)) continue;
8721
+ if (!repoPath || !(0, import_node_fs20.existsSync)(repoPath)) continue;
7996
8722
  try {
7997
8723
  const wts = await listWorktrees(repoPath);
7998
8724
  for (const wt of wts) {
@@ -8003,7 +8729,7 @@ async function findOrphanWorktrees(repoPaths) {
8003
8729
  seen.add(path);
8004
8730
  let mtimeMs = 0;
8005
8731
  try {
8006
- mtimeMs = (0, import_node_fs18.statSync)(path).mtimeMs;
8732
+ mtimeMs = (0, import_node_fs20.statSync)(path).mtimeMs;
8007
8733
  } catch {
8008
8734
  mtimeMs = 0;
8009
8735
  }
@@ -8013,16 +8739,16 @@ async function findOrphanWorktrees(repoPaths) {
8013
8739
  }
8014
8740
  try {
8015
8741
  const root = worktreesRoot(repoPath);
8016
- if ((0, import_node_fs18.existsSync)(root)) {
8017
- for (const entry of (0, import_node_fs18.readdirSync)(root, { withFileTypes: true })) {
8742
+ if ((0, import_node_fs20.existsSync)(root)) {
8743
+ for (const entry of (0, import_node_fs20.readdirSync)(root, { withFileTypes: true })) {
8018
8744
  if (!entry.isDirectory()) continue;
8019
- const path = (0, import_node_path15.join)(root, entry.name).replace(/\/$/, "");
8745
+ const path = (0, import_node_path17.join)(root, entry.name).replace(/\/$/, "");
8020
8746
  if (known.has(path) || seen.has(path)) continue;
8021
- if (!(0, import_node_fs18.existsSync)((0, import_node_path15.join)(path, ".git"))) continue;
8747
+ if (!(0, import_node_fs20.existsSync)((0, import_node_path17.join)(path, ".git"))) continue;
8022
8748
  seen.add(path);
8023
8749
  let mtimeMs = 0;
8024
8750
  try {
8025
- mtimeMs = (0, import_node_fs18.statSync)(path).mtimeMs;
8751
+ mtimeMs = (0, import_node_fs20.statSync)(path).mtimeMs;
8026
8752
  } catch {
8027
8753
  mtimeMs = Date.now();
8028
8754
  }
@@ -8159,8 +8885,8 @@ async function applyThreadIntoMain(thread, opts) {
8159
8885
  }
8160
8886
 
8161
8887
  // src/git/clone-repo.ts
8162
- var import_node_fs20 = require("fs");
8163
- var import_node_path17 = require("path");
8888
+ var import_node_fs22 = require("fs");
8889
+ var import_node_path19 = require("path");
8164
8890
  var import_execa6 = require("execa");
8165
8891
  init_paths();
8166
8892
  init_workspaces();
@@ -8170,12 +8896,12 @@ async function cloneRepoIntoSideboard(opts) {
8170
8896
  if (!url) throw new Error("Clone URL is required");
8171
8897
  let name = opts.name?.trim();
8172
8898
  if (!name) {
8173
- const leaf = (0, import_node_path17.basename)(url.replace(/\/$/, "").replace(/\.git$/, ""));
8899
+ const leaf = (0, import_node_path19.basename)(url.replace(/\/$/, "").replace(/\.git$/, ""));
8174
8900
  name = leaf || "repo";
8175
8901
  }
8176
8902
  name = name.replace(/[^a-zA-Z0-9._-]+/g, "-").replace(/^-+|-+$/g, "") || "repo";
8177
- const dest = (0, import_node_path17.join)(sideboardReposDir(), name);
8178
- if ((0, import_node_fs20.existsSync)(dest)) {
8903
+ const dest = (0, import_node_path19.join)(sideboardReposDir(), name);
8904
+ if ((0, import_node_fs22.existsSync)(dest)) {
8179
8905
  const repoPath2 = await resolveRepoRoot(dest);
8180
8906
  const workspace2 = await ensureWorkspace(repoPath2);
8181
8907
  return { repoPath: repoPath2, workspace: workspace2 };
@@ -8195,7 +8921,7 @@ async function cloneRepoIntoSideboard(opts) {
8195
8921
  init_thread_store();
8196
8922
 
8197
8923
  // src/threads/create.ts
8198
- var import_node_fs21 = require("fs");
8924
+ var import_node_fs23 = require("fs");
8199
8925
 
8200
8926
  // src/detect/detect.ts
8201
8927
  init_agents();
@@ -8240,7 +8966,7 @@ async function createThread(input, _onSetupLine) {
8240
8966
  });
8241
8967
  await requireAgent(resolved.agent);
8242
8968
  const repoPath = await resolveRepoRoot(input.repoPath);
8243
- if (!(0, import_node_fs21.existsSync)(repoPath)) {
8969
+ if (!(0, import_node_fs23.existsSync)(repoPath)) {
8244
8970
  throw new Error(`Repo not found: ${repoPath}`);
8245
8971
  }
8246
8972
  let sourceRef = input.sourceRef;
@@ -8311,7 +9037,7 @@ async function listLinearIssues(agent, repoPath) {
8311
9037
  init_orchestrator_capable();
8312
9038
 
8313
9039
  // src/threads/chat-tabs.ts
8314
- var import_node_crypto2 = require("crypto");
9040
+ var import_node_crypto4 = require("crypto");
8315
9041
 
8316
9042
  // src/composer/summarize.ts
8317
9043
  init_run();
@@ -8359,9 +9085,9 @@ async function tryClaudeSummary(transcript, opts) {
8359
9085
  { cwd: opts?.cwd, reject: false }
8360
9086
  );
8361
9087
  if (exitCode !== 0) return null;
8362
- const text = stdout.trim();
8363
- if (text.length < 40) return null;
8364
- return text;
9088
+ const text2 = stdout.trim();
9089
+ if (text2.length < 40) return null;
9090
+ return text2;
8365
9091
  } catch {
8366
9092
  return null;
8367
9093
  }
@@ -8410,9 +9136,9 @@ function extractiveSummary(transcript) {
8410
9136
  );
8411
9137
  return parts.join("\n");
8412
9138
  }
8413
- function clipSummary(text) {
8414
- if (text.length <= MAX_SUMMARY_CHARS) return text;
8415
- return `${text.slice(0, MAX_SUMMARY_CHARS)}
9139
+ function clipSummary(text2) {
9140
+ if (text2.length <= MAX_SUMMARY_CHARS) return text2;
9141
+ return `${text2.slice(0, MAX_SUMMARY_CHARS)}
8416
9142
 
8417
9143
  [\u2026summary truncated\u2026]`;
8418
9144
  }
@@ -8648,7 +9374,7 @@ function forkMessageSlice(from, throughIndex) {
8648
9374
  function buildForkTranscriptAttachment(baseTitle, messages) {
8649
9375
  const title = baseTitle || "Chat";
8650
9376
  return {
8651
- id: (0, import_node_crypto2.randomUUID)(),
9377
+ id: (0, import_node_crypto4.randomUUID)(),
8652
9378
  name: `Transcript of ${title}.md`,
8653
9379
  kind: "transcript",
8654
9380
  content: formatTranscriptMarkdown(title, messages)
@@ -8702,9 +9428,9 @@ function forkChatTab(input) {
8702
9428
  }
8703
9429
 
8704
9430
  // src/review/request-review.ts
8705
- var import_node_crypto3 = require("crypto");
8706
- var import_node_fs22 = require("fs");
8707
- var import_node_path18 = require("path");
9431
+ var import_node_crypto5 = require("crypto");
9432
+ var import_node_fs24 = require("fs");
9433
+ var import_node_path20 = require("path");
8708
9434
  init_global_workspace();
8709
9435
  init_thread_store();
8710
9436
 
@@ -8848,22 +9574,22 @@ function shouldRefreshReviewRequestTemplate(content) {
8848
9574
  return LEGACY_REVIEW_TEMPLATE_MARKERS.every((m) => trimmed.includes(m));
8849
9575
  }
8850
9576
  function readTextIfPresent(abs) {
8851
- if (!(0, import_node_fs22.existsSync)(abs)) return null;
9577
+ if (!(0, import_node_fs24.existsSync)(abs)) return null;
8852
9578
  try {
8853
- const content = (0, import_node_fs22.readFileSync)(abs, "utf8");
9579
+ const content = (0, import_node_fs24.readFileSync)(abs, "utf8");
8854
9580
  return content.trim() ? content : null;
8855
9581
  } catch {
8856
9582
  return null;
8857
9583
  }
8858
9584
  }
8859
9585
  function ensureAttachmentsGitignore(worktreePath) {
8860
- const gitignoreAbs = (0, import_node_path18.join)(worktreePath, ATTACHMENTS_DIR, ".gitignore");
8861
- if ((0, import_node_fs22.existsSync)(gitignoreAbs)) return;
8862
- (0, import_node_fs22.mkdirSync)((0, import_node_path18.dirname)(gitignoreAbs), { recursive: true });
8863
- (0, import_node_fs22.writeFileSync)(gitignoreAbs, attachmentsGitignoreBody(), "utf8");
9586
+ const gitignoreAbs = (0, import_node_path20.join)(worktreePath, ATTACHMENTS_DIR, ".gitignore");
9587
+ if ((0, import_node_fs24.existsSync)(gitignoreAbs)) return;
9588
+ (0, import_node_fs24.mkdirSync)((0, import_node_path20.dirname)(gitignoreAbs), { recursive: true });
9589
+ (0, import_node_fs24.writeFileSync)(gitignoreAbs, attachmentsGitignoreBody(), "utf8");
8864
9590
  }
8865
9591
  function resolveReviewGuidelines(worktreePath) {
8866
- const repoAbs = (0, import_node_path18.join)(worktreePath, REPO_REVIEW_PATH);
9592
+ const repoAbs = (0, import_node_path20.join)(worktreePath, REPO_REVIEW_PATH);
8867
9593
  const repoContent = readTextIfPresent(repoAbs);
8868
9594
  if (repoContent) {
8869
9595
  return {
@@ -8873,7 +9599,7 @@ function resolveReviewGuidelines(worktreePath) {
8873
9599
  source: "repo"
8874
9600
  };
8875
9601
  }
8876
- const localAbs = (0, import_node_path18.join)(worktreePath, REVIEW_REQUEST_PATH);
9602
+ const localAbs = (0, import_node_path20.join)(worktreePath, REVIEW_REQUEST_PATH);
8877
9603
  const localContent = readTextIfPresent(localAbs);
8878
9604
  if (localContent && !shouldRefreshReviewRequestTemplate(localContent)) {
8879
9605
  return {
@@ -8883,7 +9609,7 @@ function resolveReviewGuidelines(worktreePath) {
8883
9609
  source: "local"
8884
9610
  };
8885
9611
  }
8886
- const legacyAbs = (0, import_node_path18.join)(worktreePath, LEGACY_REVIEW_REQUEST_PATH);
9612
+ const legacyAbs = (0, import_node_path20.join)(worktreePath, LEGACY_REVIEW_REQUEST_PATH);
8887
9613
  const legacyContent = readTextIfPresent(legacyAbs);
8888
9614
  if (legacyContent && !shouldRefreshReviewRequestTemplate(legacyContent)) {
8889
9615
  return {
@@ -8894,8 +9620,8 @@ function resolveReviewGuidelines(worktreePath) {
8894
9620
  };
8895
9621
  }
8896
9622
  ensureAttachmentsGitignore(worktreePath);
8897
- (0, import_node_fs22.mkdirSync)((0, import_node_path18.dirname)(localAbs), { recursive: true });
8898
- (0, import_node_fs22.writeFileSync)(localAbs, REVIEW_REQUEST_TEMPLATE, "utf8");
9623
+ (0, import_node_fs24.mkdirSync)((0, import_node_path20.dirname)(localAbs), { recursive: true });
9624
+ (0, import_node_fs24.writeFileSync)(localAbs, REVIEW_REQUEST_TEMPLATE, "utf8");
8899
9625
  return {
8900
9626
  path: REVIEW_REQUEST_PATH,
8901
9627
  name: REVIEW_REQUEST_NAME,
@@ -8907,7 +9633,7 @@ function buildReviewRequestAttachment(content, opts) {
8907
9633
  const path = opts?.path ?? REVIEW_REQUEST_PATH;
8908
9634
  const name = opts?.name ?? (path === REPO_REVIEW_PATH ? REPO_REVIEW_NAME : REVIEW_REQUEST_NAME);
8909
9635
  return {
8910
- id: (0, import_node_crypto3.randomUUID)(),
9636
+ id: (0, import_node_crypto5.randomUUID)(),
8911
9637
  name,
8912
9638
  kind: "file",
8913
9639
  path,
@@ -8985,7 +9711,7 @@ async function forkThreadWorktree(input, onSetupLine) {
8985
9711
  }
8986
9712
 
8987
9713
  // src/orchestrator/quota-failover.ts
8988
- var import_node_crypto4 = require("crypto");
9714
+ var import_node_crypto6 = require("crypto");
8989
9715
  init_session_quota();
8990
9716
  init_app_settings();
8991
9717
  init_global_workspace();
@@ -9040,8 +9766,8 @@ function buildQuotaHandoffAttachment(from, limitText, fallbackAgent) {
9040
9766
  );
9041
9767
  const recent = from.messages.slice(-8).map((m) => {
9042
9768
  const role = m.role === "user" ? "User" : m.role === "agent" ? "Agent" : "Summary";
9043
- const text = m.text.trim().replace(/\s+/g, " ").slice(0, 280);
9044
- return text ? `- ${role}: ${text}` : null;
9769
+ const text2 = m.text.trim().replace(/\s+/g, " ").slice(0, 280);
9770
+ return text2 ? `- ${role}: ${text2}` : null;
9045
9771
  }).filter(Boolean);
9046
9772
  const body = [
9047
9773
  `# Orchestration handoff`,
@@ -9066,7 +9792,7 @@ function buildQuotaHandoffAttachment(from, limitText, fallbackAgent) {
9066
9792
  `- Do not wait on the limited ${from.agent} account; keep going on ${fallbackAgent}.`
9067
9793
  ].join("\n");
9068
9794
  return {
9069
- id: (0, import_node_crypto4.randomUUID)(),
9795
+ id: (0, import_node_crypto6.randomUUID)(),
9070
9796
  name: "Orchestration quota handoff.md",
9071
9797
  kind: "transcript",
9072
9798
  content: body
@@ -9094,21 +9820,21 @@ function createQuotaFailoverChat(from, fallbackAgent, limitText) {
9094
9820
  }
9095
9821
 
9096
9822
  // src/threads/adopt.ts
9097
- var import_node_child_process2 = require("child_process");
9098
- var import_node_fs23 = require("fs");
9823
+ var import_node_child_process3 = require("child_process");
9824
+ var import_node_fs25 = require("fs");
9099
9825
  var import_node_os8 = require("os");
9100
- var import_node_path19 = require("path");
9826
+ var import_node_path21 = require("path");
9101
9827
  var import_better_sqlite3 = __toESM(require("better-sqlite3"), 1);
9102
9828
  init_worktree();
9103
9829
  init_thread_store();
9104
- var CONDUCTOR_APP_SUPPORT = (0, import_node_path19.join)(
9830
+ var CONDUCTOR_APP_SUPPORT = (0, import_node_path21.join)(
9105
9831
  process.env.HOME ?? "",
9106
9832
  "Library",
9107
9833
  "Application Support",
9108
9834
  "com.conductor.app"
9109
9835
  );
9110
- var CONDUCTOR_DB = (0, import_node_path19.join)(CONDUCTOR_APP_SUPPORT, "conductor.db");
9111
- var CURSOR_SDK_STORE = (0, import_node_path19.join)(CONDUCTOR_APP_SUPPORT, "cursor-sdk-store");
9836
+ var CONDUCTOR_DB = (0, import_node_path21.join)(CONDUCTOR_APP_SUPPORT, "conductor.db");
9837
+ var CURSOR_SDK_STORE = (0, import_node_path21.join)(CONDUCTOR_APP_SUPPORT, "cursor-sdk-store");
9112
9838
  function mapAgentType(raw) {
9113
9839
  if (!raw) return null;
9114
9840
  const v = raw.toLowerCase();
@@ -9120,25 +9846,25 @@ function mapAgentType(raw) {
9120
9846
  return null;
9121
9847
  }
9122
9848
  function resolveConductorCursorAgentId(workspacePath) {
9123
- if (!workspacePath || !(0, import_node_fs23.existsSync)(CURSOR_SDK_STORE)) return null;
9849
+ if (!workspacePath || !(0, import_node_fs25.existsSync)(CURSOR_SDK_STORE)) return null;
9124
9850
  const normalized = workspacePath.replace(/\/$/, "");
9125
9851
  let best = null;
9126
9852
  let hashes;
9127
9853
  try {
9128
- hashes = (0, import_node_fs23.readdirSync)(CURSOR_SDK_STORE);
9854
+ hashes = (0, import_node_fs25.readdirSync)(CURSOR_SDK_STORE);
9129
9855
  } catch {
9130
9856
  return null;
9131
9857
  }
9132
9858
  for (const hash of hashes) {
9133
- const agentsFile = (0, import_node_path19.join)(CURSOR_SDK_STORE, hash, "agents.ndjson");
9134
- if (!(0, import_node_fs23.existsSync)(agentsFile)) continue;
9135
- let text;
9859
+ const agentsFile = (0, import_node_path21.join)(CURSOR_SDK_STORE, hash, "agents.ndjson");
9860
+ if (!(0, import_node_fs25.existsSync)(agentsFile)) continue;
9861
+ let text2;
9136
9862
  try {
9137
- text = (0, import_node_fs23.readFileSync)(agentsFile, "utf8");
9863
+ text2 = (0, import_node_fs25.readFileSync)(agentsFile, "utf8");
9138
9864
  } catch {
9139
9865
  continue;
9140
9866
  }
9141
- for (const line of text.split("\n")) {
9867
+ for (const line of text2.split("\n")) {
9142
9868
  const trimmed = line.trim();
9143
9869
  if (!trimmed) continue;
9144
9870
  try {
@@ -9158,7 +9884,7 @@ function resolveConductorCursorAgentId(workspacePath) {
9158
9884
  return best?.agentId ?? null;
9159
9885
  }
9160
9886
  async function adoptThread(input) {
9161
- if (!(0, import_node_fs23.existsSync)(input.worktreePath)) {
9887
+ if (!(0, import_node_fs25.existsSync)(input.worktreePath)) {
9162
9888
  throw new Error(`Worktree not found: ${input.worktreePath}`);
9163
9889
  }
9164
9890
  const repoPath = await resolveRepoRoot(input.worktreePath);
@@ -9182,18 +9908,18 @@ async function adoptThread(input) {
9182
9908
  return thread;
9183
9909
  }
9184
9910
  function listConductorWorkspaces() {
9185
- if (!(0, import_node_fs23.existsSync)(CONDUCTOR_DB)) {
9911
+ if (!(0, import_node_fs25.existsSync)(CONDUCTOR_DB)) {
9186
9912
  throw new Error(`Conductor DB not found at ${CONDUCTOR_DB}`);
9187
9913
  }
9188
- const tmp = (0, import_node_fs23.mkdtempSync)((0, import_node_path19.join)((0, import_node_os8.tmpdir)(), "sideboard-conductor-"));
9189
- const snapshot = (0, import_node_path19.join)(tmp, "conductor.db");
9914
+ const tmp = (0, import_node_fs25.mkdtempSync)((0, import_node_path21.join)((0, import_node_os8.tmpdir)(), "sideboard-conductor-"));
9915
+ const snapshot = (0, import_node_path21.join)(tmp, "conductor.db");
9190
9916
  try {
9191
- (0, import_node_fs23.copyFileSync)(CONDUCTOR_DB, snapshot);
9917
+ (0, import_node_fs25.copyFileSync)(CONDUCTOR_DB, snapshot);
9192
9918
  for (const suffix of ["-wal", "-shm"]) {
9193
9919
  const src = `${CONDUCTOR_DB}${suffix}`;
9194
- if ((0, import_node_fs23.existsSync)(src)) {
9920
+ if ((0, import_node_fs25.existsSync)(src)) {
9195
9921
  try {
9196
- (0, import_node_fs23.copyFileSync)(src, `${snapshot}${suffix}`);
9922
+ (0, import_node_fs25.copyFileSync)(src, `${snapshot}${suffix}`);
9197
9923
  } catch {
9198
9924
  }
9199
9925
  }
@@ -9269,22 +9995,22 @@ function listConductorWorkspaces() {
9269
9995
  db.close();
9270
9996
  }
9271
9997
  } finally {
9272
- (0, import_node_fs23.rmSync)(tmp, { recursive: true, force: true });
9998
+ (0, import_node_fs25.rmSync)(tmp, { recursive: true, force: true });
9273
9999
  }
9274
10000
  }
9275
10001
  function importConductorWorkspace(workspaceId) {
9276
- if (!(0, import_node_fs23.existsSync)(CONDUCTOR_DB)) {
10002
+ if (!(0, import_node_fs25.existsSync)(CONDUCTOR_DB)) {
9277
10003
  throw new Error(`Conductor DB not found at ${CONDUCTOR_DB}`);
9278
10004
  }
9279
- const tmp = (0, import_node_fs23.mkdtempSync)((0, import_node_path19.join)((0, import_node_os8.tmpdir)(), "sideboard-conductor-"));
9280
- const snapshot = (0, import_node_path19.join)(tmp, "conductor.db");
10005
+ const tmp = (0, import_node_fs25.mkdtempSync)((0, import_node_path21.join)((0, import_node_os8.tmpdir)(), "sideboard-conductor-"));
10006
+ const snapshot = (0, import_node_path21.join)(tmp, "conductor.db");
9281
10007
  try {
9282
- (0, import_node_fs23.copyFileSync)(CONDUCTOR_DB, snapshot);
10008
+ (0, import_node_fs25.copyFileSync)(CONDUCTOR_DB, snapshot);
9283
10009
  for (const suffix of ["-wal", "-shm"]) {
9284
10010
  const src = `${CONDUCTOR_DB}${suffix}`;
9285
- if ((0, import_node_fs23.existsSync)(src)) {
10011
+ if ((0, import_node_fs25.existsSync)(src)) {
9286
10012
  try {
9287
- (0, import_node_fs23.copyFileSync)(src, `${snapshot}${suffix}`);
10013
+ (0, import_node_fs25.copyFileSync)(src, `${snapshot}${suffix}`);
9288
10014
  } catch {
9289
10015
  }
9290
10016
  }
@@ -9302,7 +10028,7 @@ function importConductorWorkspace(workspaceId) {
9302
10028
  ).get(workspaceId);
9303
10029
  if (!row) throw new Error(`Conductor workspace not found: ${workspaceId}`);
9304
10030
  const worktreePath = String(row.workspacePath);
9305
- if (!(0, import_node_fs23.existsSync)(worktreePath)) {
10031
+ if (!(0, import_node_fs25.existsSync)(worktreePath)) {
9306
10032
  throw new Error(`Conductor worktree missing on disk: ${worktreePath}`);
9307
10033
  }
9308
10034
  let sessionId = null;
@@ -9342,7 +10068,7 @@ function importConductorWorkspace(workspaceId) {
9342
10068
  }
9343
10069
  let repoPath = worktreePath;
9344
10070
  try {
9345
- repoPath = (0, import_node_child_process2.execFileSync)("git", ["rev-parse", "--show-toplevel"], {
10071
+ repoPath = (0, import_node_child_process3.execFileSync)("git", ["rev-parse", "--show-toplevel"], {
9346
10072
  cwd: worktreePath,
9347
10073
  encoding: "utf8"
9348
10074
  }).trim();
@@ -9365,7 +10091,7 @@ function importConductorWorkspace(workspaceId) {
9365
10091
  db.close();
9366
10092
  }
9367
10093
  } finally {
9368
- (0, import_node_fs23.rmSync)(tmp, { recursive: true, force: true });
10094
+ (0, import_node_fs25.rmSync)(tmp, { recursive: true, force: true });
9369
10095
  }
9370
10096
  }
9371
10097
  async function importConductorWorkspaceAsync(workspaceId) {
@@ -9373,7 +10099,7 @@ async function importConductorWorkspaceAsync(workspaceId) {
9373
10099
  }
9374
10100
 
9375
10101
  // src/threads/stack-layers.ts
9376
- var import_node_fs24 = require("fs");
10102
+ var import_node_fs26 = require("fs");
9377
10103
  init_run();
9378
10104
  init_stack();
9379
10105
  init_worktree();
@@ -9441,7 +10167,7 @@ async function openStackLayer(input, _onSetupLine) {
9441
10167
  let createdWorktree = false;
9442
10168
  const trees = await listWorktrees(repoPath);
9443
10169
  const checkedOut = trees.find((w) => w.branch === branchName);
9444
- if (checkedOut?.path && (0, import_node_fs24.existsSync)(checkedOut.path)) {
10170
+ if (checkedOut?.path && (0, import_node_fs26.existsSync)(checkedOut.path)) {
9445
10171
  if (input.reuseExistingWorktree !== false) {
9446
10172
  worktreePath = checkedOut.path;
9447
10173
  } else {
@@ -9583,7 +10309,7 @@ async function initStackFromThread(input, onSetupLine) {
9583
10309
  async function createPrStack(input, onSetupLine) {
9584
10310
  await requireAgent(input.agent);
9585
10311
  const repoPath = await resolveRepoRoot(input.repoPath);
9586
- if (!(0, import_node_fs24.existsSync)(repoPath)) throw new Error(`Repo not found: ${repoPath}`);
10312
+ if (!(0, import_node_fs26.existsSync)(repoPath)) throw new Error(`Repo not found: ${repoPath}`);
9587
10313
  if (!input.branches.length) throw new Error("At least one branch name required");
9588
10314
  const status = await detectGhStack(repoPath);
9589
10315
  if (!status.available) throw new Error(status.reason);
@@ -9650,7 +10376,7 @@ async function createPrStack(input, onSetupLine) {
9650
10376
  }
9651
10377
  }
9652
10378
  const claimed = new Set(threads.map((t) => t.worktreePath));
9653
- if (!claimed.has(bootstrap.worktreePath) && (0, import_node_fs24.existsSync)(bootstrap.worktreePath)) {
10379
+ if (!claimed.has(bootstrap.worktreePath) && (0, import_node_fs26.existsSync)(bootstrap.worktreePath)) {
9654
10380
  try {
9655
10381
  await removeWorktree(repoPath, bootstrap.worktreePath, {
9656
10382
  deleteBranch: bootstrap.branchName
@@ -9665,12 +10391,12 @@ async function createPrStack(input, onSetupLine) {
9665
10391
  init_worktree();
9666
10392
 
9667
10393
  // src/diff/diff.ts
9668
- var import_node_fs25 = require("fs");
9669
- var import_node_path20 = require("path");
10394
+ var import_node_fs27 = require("fs");
10395
+ var import_node_path22 = require("path");
9670
10396
  init_run();
9671
10397
  init_worktree();
9672
10398
  async function inspectGitWorktree(worktreePath) {
9673
- if (!worktreePath || !(0, import_node_fs25.existsSync)(worktreePath)) return "missing_worktree";
10399
+ if (!worktreePath || !(0, import_node_fs27.existsSync)(worktreePath)) return "missing_worktree";
9674
10400
  const check = await git(["rev-parse", "--is-inside-work-tree"], worktreePath, {
9675
10401
  reject: false
9676
10402
  });
@@ -9678,7 +10404,7 @@ async function inspectGitWorktree(worktreePath) {
9678
10404
  return "ok";
9679
10405
  }
9680
10406
  async function initializeGitRepository(worktreePath) {
9681
- if (!worktreePath || !(0, import_node_fs25.existsSync)(worktreePath)) {
10407
+ if (!worktreePath || !(0, import_node_fs27.existsSync)(worktreePath)) {
9682
10408
  throw new Error("Worktree not found");
9683
10409
  }
9684
10410
  const status = await inspectGitWorktree(worktreePath);
@@ -9744,24 +10470,25 @@ function capPatch(patch, maxHunk) {
9744
10470
 
9745
10471
  \u2026 truncated (${patch.length - maxHunk} more chars)`;
9746
10472
  }
10473
+ var mergeBaseCache = /* @__PURE__ */ new Map();
10474
+ var MERGE_BASE_TTL_MS = 45e3;
9747
10475
  async function resolveMergeBase(worktreePath, base, repoPath) {
9748
10476
  const baseRef = await resolveDiffBaseRef(worktreePath, base, repoPath);
10477
+ const cacheKey = `${worktreePath}\0${baseRef}`;
10478
+ const hit = mergeBaseCache.get(cacheKey);
10479
+ if (hit && Date.now() - hit.at < MERGE_BASE_TTL_MS) return hit.value;
9749
10480
  const { stdout } = await git(["merge-base", baseRef, "HEAD"], worktreePath, {
9750
10481
  reject: false
9751
10482
  });
9752
- const mb = stdout.trim();
9753
- return mb || baseRef;
10483
+ const mb = stdout.trim() || baseRef;
10484
+ mergeBaseCache.set(cacheKey, { at: Date.now(), value: mb });
10485
+ return mb;
9754
10486
  }
9755
10487
  async function countUnpushedCommits(worktreePath) {
9756
10488
  const head = await git(["rev-parse", "--abbrev-ref", "HEAD"], worktreePath, {
9757
10489
  reject: false
9758
10490
  });
9759
10491
  const branch = head.stdout.trim();
9760
- if (branch && branch !== "HEAD") {
9761
- await git(["fetch", "origin", branch, "--quiet"], worktreePath, {
9762
- reject: false
9763
- });
9764
- }
9765
10492
  const upstream = await git(
9766
10493
  ["rev-list", "--count", "@{upstream}..HEAD"],
9767
10494
  worktreePath,
@@ -9794,11 +10521,37 @@ function splitCombinedDiff(stdout) {
9794
10521
  }
9795
10522
  return map;
9796
10523
  }
10524
+ function syntheticAddPatch(path, content, maxHunk) {
10525
+ const lines = content.split("\n");
10526
+ const body = lines.map((l) => `+${l}`).join("\n");
10527
+ const header = `diff --git a/${path} b/${path}
10528
+ new file mode 100644
10529
+ --- /dev/null
10530
+ +++ b/${path}
10531
+ @@ -0,0 +1,${lines.length} @@
10532
+ `;
10533
+ return {
10534
+ path,
10535
+ status: "A",
10536
+ patch: capPatch(`${header}${body}`, maxHunk),
10537
+ additions: lines.length,
10538
+ deletions: 0
10539
+ };
10540
+ }
9797
10541
  async function untrackedPatch(worktreePath, path, maxHunk) {
10542
+ const abs = (0, import_node_path22.join)(worktreePath, path);
10543
+ try {
10544
+ const st = (0, import_node_fs27.statSync)(abs);
10545
+ if (st.isFile() && st.size > maxHunk) {
10546
+ const buf = (0, import_node_fs27.readFileSync)(abs).subarray(0, maxHunk);
10547
+ return syntheticAddPatch(path, buf.toString("utf8"), maxHunk);
10548
+ }
10549
+ } catch {
10550
+ }
9798
10551
  const { stdout: patch } = await git(
9799
- ["diff", "--no-index", "--", "/dev/null", path],
10552
+ ["diff", "--no-ext-diff", "--no-color", "--no-index", "--", "/dev/null", path],
9800
10553
  worktreePath,
9801
- { reject: false }
10554
+ { reject: false, timeoutMs: 4e3 }
9802
10555
  );
9803
10556
  const normalized = patch.replace(/^diff --git a\/.+? b\/.+?$/m, `diff --git a/${path} b/${path}`).replace(/^--- .*$/m, "--- /dev/null").replace(/^\+\+\+ .*$/m, `+++ b/${path}`);
9804
10557
  const addLines = normalized.split("\n").filter((l) => l.startsWith("+") && !l.startsWith("+++")).length;
@@ -9810,22 +10563,97 @@ async function untrackedPatch(worktreePath, path, maxHunk) {
9810
10563
  deletions: 0
9811
10564
  };
9812
10565
  }
9813
- async function listUntrackedPaths(worktreePath) {
9814
- const { stdout } = await git(
9815
- ["ls-files", "-z", "--others", "--exclude-standard"],
9816
- worktreePath,
9817
- { reject: false }
9818
- );
10566
+ async function listUntrackedPaths(worktreePath, path) {
10567
+ const args = ["ls-files", "-z", "--others", "--exclude-standard"];
10568
+ if (path) args.push("--", path);
10569
+ const { stdout } = await git(args, worktreePath, { reject: false });
9819
10570
  return stdout.split("\0").filter(Boolean).filter((p) => !isSideboardScratchPath(p));
9820
10571
  }
9821
- async function listUntrackedFiles(worktreePath, maxHunk) {
9822
- const paths = await listUntrackedPaths(worktreePath);
9823
- if (paths.length === 0) return [];
9824
- return Promise.all(paths.map((path) => untrackedPatch(worktreePath, path, maxHunk)));
10572
+ async function listUntrackedFiles(worktreePath, maxHunk, paths) {
10573
+ const list = paths ?? await listUntrackedPaths(worktreePath);
10574
+ if (list.length === 0) return [];
10575
+ return Promise.all(list.map((path) => untrackedPatch(worktreePath, path, maxHunk)));
10576
+ }
10577
+ function untrackedStubs(paths) {
10578
+ return paths.map((path) => ({
10579
+ path,
10580
+ status: "A",
10581
+ patch: "",
10582
+ additions: 0,
10583
+ deletions: 0
10584
+ }));
10585
+ }
10586
+ function pathspec(path) {
10587
+ return path ? ["--", path] : [];
10588
+ }
10589
+ function gitDiff(args, cwd, opts) {
10590
+ return git(["diff", "--no-ext-diff", "--no-color", ...args], cwd, {
10591
+ reject: false,
10592
+ timeoutMs: opts?.timeoutMs
10593
+ });
10594
+ }
10595
+ function fileFromPatch(path, patch, maxHunk) {
10596
+ let status = "M";
10597
+ if (/^new file mode /m.test(patch) || /^--- \/dev\/null/m.test(patch)) {
10598
+ status = "A";
10599
+ } else if (/^deleted file mode /m.test(patch) || /^\+\+\+ \/dev\/null/m.test(patch)) {
10600
+ status = "D";
10601
+ } else if (/^rename from /m.test(patch)) {
10602
+ status = "R";
10603
+ }
10604
+ const additions = patch.split("\n").filter((l) => l.startsWith("+") && !l.startsWith("+++")).length;
10605
+ const deletions = patch.split("\n").filter((l) => l.startsWith("-") && !l.startsWith("---")).length;
10606
+ return {
10607
+ path,
10608
+ status,
10609
+ patch: capPatch(patch, maxHunk),
10610
+ additions,
10611
+ deletions
10612
+ };
10613
+ }
10614
+ async function gitDiffParts(worktreePath, diffArgs, includePatches, path) {
10615
+ const extra = pathspec(path);
10616
+ if (includePatches) {
10617
+ const diff = await gitDiff([...diffArgs, ...extra], worktreePath, {
10618
+ timeoutMs: 4e3
10619
+ });
10620
+ return { nameStatus: "", numstat: "", combinedDiff: diff.stdout };
10621
+ }
10622
+ const [ns, num2] = await Promise.all([
10623
+ gitDiff(["--name-status", ...diffArgs, ...extra], worktreePath),
10624
+ gitDiff(["--numstat", ...diffArgs, ...extra], worktreePath)
10625
+ ]);
10626
+ return {
10627
+ nameStatus: ns.stdout,
10628
+ numstat: num2.stdout,
10629
+ combinedDiff: ""
10630
+ };
10631
+ }
10632
+ function formatStat(files) {
10633
+ if (files.length === 0) return "(no changes)";
10634
+ const additions = files.reduce((n2, f) => n2 + (f.additions ?? 0), 0);
10635
+ const deletions = files.reduce((n2, f) => n2 + (f.deletions ?? 0), 0);
10636
+ const n = files.length;
10637
+ return `${n} file${n === 1 ? "" : "s"} changed, ${additions} insertions(+), ${deletions} deletions(-)`;
10638
+ }
10639
+ function emptyScopeStats() {
10640
+ const z3 = emptyStat();
10641
+ return {
10642
+ commits: z3,
10643
+ uncommitted: z3,
10644
+ staged: z3,
10645
+ unstaged: z3,
10646
+ last_turn: z3
10647
+ };
9825
10648
  }
9826
10649
  function filesFromDiff(nameStatus, numstat, combinedDiff, maxHunk) {
9827
- const counts = parseNumstat(numstat);
9828
10650
  const patches = splitCombinedDiff(combinedDiff);
10651
+ if (!nameStatus.trim() && patches.size > 0) {
10652
+ return [...patches.entries()].map(
10653
+ ([path, patch]) => fileFromPatch(path, patch, maxHunk)
10654
+ );
10655
+ }
10656
+ const counts = parseNumstat(numstat);
9829
10657
  const files = [];
9830
10658
  for (const { status, path } of parseNameStatus(nameStatus)) {
9831
10659
  const n = counts.get(path);
@@ -9847,11 +10675,11 @@ async function computeScopeStats(worktreePath, mergeBase, lastTurnBase, untracke
9847
10675
  { stdout: unstagedNum },
9848
10676
  lastTurnNum
9849
10677
  ] = await Promise.all([
9850
- mergeBase ? git(["diff", "--numstat", mergeBase], worktreePath, { reject: false }) : Promise.resolve({ stdout: "" }),
9851
- git(["diff", "--numstat", "HEAD"], worktreePath, { reject: false }),
9852
- git(["diff", "--numstat", "--cached"], worktreePath, { reject: false }),
9853
- git(["diff", "--numstat"], worktreePath, { reject: false }),
9854
- lastTurnBase ? git(["diff", "--numstat", lastTurnBase], worktreePath, { reject: false }) : Promise.resolve({ stdout: "" })
10678
+ mergeBase ? gitDiff(["--numstat", mergeBase], worktreePath) : Promise.resolve({ stdout: "" }),
10679
+ gitDiff(["--numstat", "HEAD"], worktreePath),
10680
+ gitDiff(["--numstat", "--cached"], worktreePath),
10681
+ gitDiff(["--numstat"], worktreePath),
10682
+ lastTurnBase ? gitDiff(["--numstat", lastTurnBase], worktreePath) : Promise.resolve({ stdout: "" })
9855
10683
  ]);
9856
10684
  return {
9857
10685
  commits: mergeBase ? statFromNumstat(commitsNum, untrackedCount) : emptyStat(),
@@ -9870,7 +10698,7 @@ async function listBranchCommits(worktreePath, repoPath, opts) {
9870
10698
  if (head.exitCode !== 0) return [];
9871
10699
  let base = opts?.base;
9872
10700
  try {
9873
- base = base ?? await resolveDefaultBranch(repoPath);
10701
+ base = base ?? await resolveDefaultBranch(repoPath, { network: false });
9874
10702
  } catch {
9875
10703
  base = "HEAD";
9876
10704
  }
@@ -9900,7 +10728,121 @@ async function listBranchCommits(worktreePath, repoPath, opts) {
9900
10728
  }
9901
10729
  return commits;
9902
10730
  }
10731
+ var SHA_RE = /^[0-9a-f]{7,40}$/i;
10732
+ function emptyPathDiff(scope, base, hasLastTurnBase, commitSha = null) {
10733
+ return {
10734
+ scope,
10735
+ commitSha: scope === "commits" ? commitSha : null,
10736
+ base,
10737
+ files: [],
10738
+ stat: formatStat([]),
10739
+ dirty: false,
10740
+ unpushed: 0,
10741
+ hasLastTurnBase,
10742
+ commits: [],
10743
+ scopeStats: emptyScopeStats()
10744
+ };
10745
+ }
10746
+ async function getPathDiff(worktreePath, repoPath, opts) {
10747
+ const { path, scope, commitSha, lastTurnBase, hasLastTurnBase, includePatches, maxHunk } = opts;
10748
+ if (scope === "last_turn" && !lastTurnBase) {
10749
+ return emptyPathDiff(scope, "last turn", false);
10750
+ }
10751
+ let diffArgs;
10752
+ let labelBase;
10753
+ if (scope === "staged") {
10754
+ diffArgs = ["--cached"];
10755
+ labelBase = "staged";
10756
+ } else if (scope === "unstaged") {
10757
+ diffArgs = [];
10758
+ labelBase = "unstaged";
10759
+ } else if (scope === "uncommitted") {
10760
+ diffArgs = ["HEAD"];
10761
+ labelBase = "HEAD";
10762
+ } else if (scope === "last_turn") {
10763
+ diffArgs = [lastTurnBase];
10764
+ labelBase = "last turn";
10765
+ } else if (scope === "commits" && commitSha) {
10766
+ diffArgs = [`${commitSha}^!`];
10767
+ labelBase = commitSha.slice(0, 7);
10768
+ } else if (scope === "commits" && opts.base && SHA_RE.test(opts.base)) {
10769
+ diffArgs = [opts.base];
10770
+ labelBase = opts.base.slice(0, 7);
10771
+ } else if (scope === "commits") {
10772
+ let base = opts.base;
10773
+ try {
10774
+ base = base ?? await resolveDefaultBranch(repoPath, { network: false });
10775
+ } catch {
10776
+ base = "HEAD";
10777
+ }
10778
+ const mergeBase = await resolveMergeBase(worktreePath, base, repoPath);
10779
+ diffArgs = [mergeBase];
10780
+ labelBase = mergeBase;
10781
+ } else {
10782
+ diffArgs = ["HEAD"];
10783
+ labelBase = "HEAD";
10784
+ }
10785
+ const diff = await gitDiff(
10786
+ includePatches ? [...diffArgs, "--", path] : ["--name-status", ...diffArgs, "--", path],
10787
+ worktreePath,
10788
+ { timeoutMs: 4e3 }
10789
+ );
10790
+ const filesMap = /* @__PURE__ */ new Map();
10791
+ if (includePatches) {
10792
+ const parsed = filesFromDiff("", "", diff.stdout, maxHunk);
10793
+ for (const file of parsed) filesMap.set(file.path, file);
10794
+ if (diff.stdout.trim() && !filesMap.has(path)) {
10795
+ filesMap.set(path, fileFromPatch(path, diff.stdout, maxHunk));
10796
+ }
10797
+ } else {
10798
+ for (const file of filesFromDiff(diff.stdout, "", "", maxHunk)) {
10799
+ filesMap.set(file.path, file);
10800
+ }
10801
+ }
10802
+ if (filesMap.size === 0) {
10803
+ const maybe = await listUntrackedPaths(worktreePath, path);
10804
+ if (maybe.length > 0) {
10805
+ const extra = includePatches ? await listUntrackedFiles(worktreePath, maxHunk, maybe) : untrackedStubs(maybe);
10806
+ for (const file of extra) filesMap.set(file.path, file);
10807
+ }
10808
+ }
10809
+ const files = [...filesMap.values()].sort((a, b) => a.path.localeCompare(b.path));
10810
+ return {
10811
+ scope,
10812
+ commitSha: scope === "commits" ? commitSha : null,
10813
+ base: labelBase,
10814
+ files,
10815
+ stat: formatStat(files),
10816
+ dirty: files.length > 0,
10817
+ unpushed: 0,
10818
+ hasLastTurnBase,
10819
+ commits: [],
10820
+ scopeStats: emptyScopeStats(),
10821
+ mergeBase: SHA_RE.test(diffArgs[0] ?? "") ? diffArgs[0] : null
10822
+ };
10823
+ }
9903
10824
  async function getDiff(worktreePath, repoPath, opts) {
10825
+ const maxHunk = opts?.maxHunkChars ?? 8e3;
10826
+ const scope = opts?.scope ?? "commits";
10827
+ const lastTurnBase = opts?.lastTurnBase?.trim() || null;
10828
+ const hasLastTurnBase = Boolean(lastTurnBase);
10829
+ const commitSha = opts?.commitSha?.trim() || null;
10830
+ const includePatches = opts?.includePatches ?? true;
10831
+ const pathFilter = opts?.path?.trim() || null;
10832
+ const fileOnly = Boolean(pathFilter);
10833
+ const includeMeta = (opts?.includeMeta ?? true) && !fileOnly;
10834
+ if (fileOnly && pathFilter && !includeMeta) {
10835
+ return getPathDiff(worktreePath, repoPath, {
10836
+ path: pathFilter,
10837
+ scope,
10838
+ commitSha,
10839
+ lastTurnBase,
10840
+ hasLastTurnBase,
10841
+ base: opts?.base,
10842
+ includePatches,
10843
+ maxHunk
10844
+ });
10845
+ }
9904
10846
  const status = await inspectGitWorktree(worktreePath);
9905
10847
  if (status === "missing_worktree") {
9906
10848
  throw new Error("Worktree not found");
@@ -9908,18 +10850,19 @@ async function getDiff(worktreePath, repoPath, opts) {
9908
10850
  if (status === "not_git") {
9909
10851
  throw new Error("Not a Git repository");
9910
10852
  }
9911
- const maxHunk = opts?.maxHunkChars ?? 8e3;
9912
- const scope = opts?.scope ?? "commits";
9913
- const lastTurnBase = opts?.lastTurnBase?.trim() || null;
9914
- const hasLastTurnBase = Boolean(lastTurnBase);
9915
- const commitSha = opts?.commitSha?.trim() || null;
9916
- const head = await git(["rev-parse", "--verify", "HEAD"], worktreePath, {
10853
+ const wantUntracked = opts?.includeUntracked !== void 0 ? opts.includeUntracked : !fileOnly && (scope === "commits" && !commitSha || scope === "uncommitted" || scope === "unstaged" || scope === "last_turn");
10854
+ const needsMergeBase = scope === "commits" && !commitSha || includeMeta;
10855
+ const headP = git(["rev-parse", "--verify", "HEAD"], worktreePath, {
9917
10856
  reject: false
9918
10857
  });
10858
+ const untrackedP = wantUntracked ? listUntrackedPaths(worktreePath, pathFilter) : Promise.resolve([]);
10859
+ const head = await headP;
9919
10860
  const hasHead = head.exitCode === 0;
9920
- const untrackedPaths = await listUntrackedPaths(worktreePath);
9921
10861
  if (!hasHead) {
9922
- const files2 = scope === "staged" ? [] : (await listUntrackedFiles(worktreePath, maxHunk)).sort(
10862
+ const untrackedPaths2 = await untrackedP;
10863
+ const files2 = scope === "staged" ? [] : includePatches ? (await listUntrackedFiles(worktreePath, maxHunk, untrackedPaths2)).sort(
10864
+ (a, b) => a.path.localeCompare(b.path)
10865
+ ) : untrackedStubs(untrackedPaths2).sort(
9923
10866
  (a, b) => a.path.localeCompare(b.path)
9924
10867
  );
9925
10868
  const untrackedStat = {
@@ -9933,7 +10876,7 @@ async function getDiff(worktreePath, repoPath, opts) {
9933
10876
  commitSha: null,
9934
10877
  base: "(no commits)",
9935
10878
  files: files2,
9936
- stat: files2.length ? files2.map((f) => ` ${f.path} | untracked`).join("\n") : "(no changes)",
10879
+ stat: formatStat(files2),
9937
10880
  dirty: files2.length > 0,
9938
10881
  unpushed: 0,
9939
10882
  hasLastTurnBase,
@@ -9947,137 +10890,122 @@ async function getDiff(worktreePath, repoPath, opts) {
9947
10890
  }
9948
10891
  };
9949
10892
  }
9950
- let base = opts?.base;
9951
- try {
9952
- base = base ?? await resolveDefaultBranch(repoPath);
9953
- } catch {
9954
- base = "HEAD";
10893
+ let mergeBase = "HEAD";
10894
+ let baseLabel = "HEAD";
10895
+ if (needsMergeBase) {
10896
+ let base = opts?.base;
10897
+ try {
10898
+ base = base ?? await resolveDefaultBranch(repoPath, { network: false });
10899
+ } catch {
10900
+ base = "HEAD";
10901
+ }
10902
+ mergeBase = await resolveMergeBase(worktreePath, base, repoPath);
10903
+ baseLabel = await resolveDiffBaseRef(worktreePath, base, repoPath);
9955
10904
  }
9956
- base = await resolveDiffBaseRef(worktreePath, base, repoPath);
9957
- if (base.startsWith("origin/")) {
9958
- const short = base.slice("origin/".length);
9959
- await git(["fetch", "origin", short, "--quiet"], worktreePath, {
9960
- reject: false
9961
- });
9962
- base = await resolveDiffBaseRef(worktreePath, short, repoPath);
10905
+ const includeUntracked = wantUntracked;
10906
+ const metaPromise = includeMeta ? Promise.all([
10907
+ computeScopeStats(
10908
+ worktreePath,
10909
+ mergeBase,
10910
+ lastTurnBase,
10911
+ // Cheap: don't wait on the untracked walk just to pad the badge.
10912
+ 0
10913
+ ),
10914
+ listBranchCommits(worktreePath, repoPath, { base: baseLabel }),
10915
+ isDirty(worktreePath),
10916
+ countUnpushedCommits(worktreePath)
10917
+ ]).then(([scopeStats, commits, dirty, unpushed]) => ({
10918
+ scopeStats,
10919
+ commits,
10920
+ dirty,
10921
+ unpushed
10922
+ })) : Promise.resolve({
10923
+ scopeStats: emptyScopeStats(),
10924
+ commits: [],
10925
+ dirty: false,
10926
+ unpushed: 0
10927
+ });
10928
+ if (scope === "last_turn" && !lastTurnBase) {
10929
+ const meta2 = await metaPromise;
10930
+ return {
10931
+ scope,
10932
+ commitSha: null,
10933
+ base: "last turn",
10934
+ files: [],
10935
+ stat: "(no last agent turn)",
10936
+ dirty: meta2.dirty,
10937
+ unpushed: meta2.unpushed,
10938
+ hasLastTurnBase: false,
10939
+ commits: meta2.commits,
10940
+ scopeStats: meta2.scopeStats
10941
+ };
9963
10942
  }
9964
- const mergeBase = await resolveMergeBase(worktreePath, base, repoPath);
9965
- const scopeStats = await computeScopeStats(
9966
- worktreePath,
9967
- mergeBase,
9968
- lastTurnBase,
9969
- untrackedPaths.length
9970
- );
9971
- const includeUntracked = scope === "commits" && !commitSha || scope === "uncommitted" || scope === "unstaged" || scope === "last_turn";
9972
- let nameStatus = "";
9973
- let numstat = "";
9974
- let combinedDiff = "";
9975
- let labelBase = base;
10943
+ let diffArgs;
10944
+ let labelBase = baseLabel;
9976
10945
  if (scope === "staged") {
9977
- const [ns, num2, diff] = await Promise.all([
9978
- git(["diff", "--name-status", "--cached"], worktreePath, { reject: false }),
9979
- git(["diff", "--numstat", "--cached"], worktreePath, { reject: false }),
9980
- git(["diff", "--cached"], worktreePath, { reject: false })
9981
- ]);
9982
- nameStatus = ns.stdout;
9983
- numstat = num2.stdout;
9984
- combinedDiff = diff.stdout;
10946
+ diffArgs = ["--cached"];
9985
10947
  labelBase = "staged";
9986
10948
  } else if (scope === "unstaged") {
9987
- const [ns, num2, diff] = await Promise.all([
9988
- git(["diff", "--name-status"], worktreePath, { reject: false }),
9989
- git(["diff", "--numstat"], worktreePath, { reject: false }),
9990
- git(["diff"], worktreePath, { reject: false })
9991
- ]);
9992
- nameStatus = ns.stdout;
9993
- numstat = num2.stdout;
9994
- combinedDiff = diff.stdout;
10949
+ diffArgs = [];
9995
10950
  labelBase = "unstaged";
9996
10951
  } else if (scope === "uncommitted") {
9997
- const [ns, num2, diff] = await Promise.all([
9998
- git(["diff", "--name-status", "HEAD"], worktreePath, { reject: false }),
9999
- git(["diff", "--numstat", "HEAD"], worktreePath, { reject: false }),
10000
- git(["diff", "HEAD"], worktreePath, { reject: false })
10001
- ]);
10002
- nameStatus = ns.stdout;
10003
- numstat = num2.stdout;
10004
- combinedDiff = diff.stdout;
10952
+ diffArgs = ["HEAD"];
10005
10953
  labelBase = "HEAD";
10006
10954
  } else if (scope === "last_turn") {
10007
- if (!lastTurnBase) {
10008
- const commits2 = await listBranchCommits(worktreePath, repoPath, { base });
10009
- return {
10010
- scope,
10011
- commitSha: null,
10012
- base: "last turn",
10013
- files: [],
10014
- stat: "(no last agent turn)",
10015
- dirty: await isDirty(worktreePath),
10016
- unpushed: await countUnpushedCommits(worktreePath),
10017
- hasLastTurnBase: false,
10018
- commits: commits2,
10019
- scopeStats
10020
- };
10021
- }
10022
- const [ns, num2, diff] = await Promise.all([
10023
- git(["diff", "--name-status", lastTurnBase], worktreePath, { reject: false }),
10024
- git(["diff", "--numstat", lastTurnBase], worktreePath, { reject: false }),
10025
- git(["diff", lastTurnBase], worktreePath, { reject: false })
10026
- ]);
10027
- nameStatus = ns.stdout;
10028
- numstat = num2.stdout;
10029
- combinedDiff = diff.stdout;
10955
+ diffArgs = [lastTurnBase];
10030
10956
  labelBase = "last turn";
10031
10957
  } else if (scope === "commits" && commitSha) {
10032
- const range = `${commitSha}^!`;
10033
- const [ns, num2, diff] = await Promise.all([
10034
- git(["diff", "--name-status", range], worktreePath, { reject: false }),
10035
- git(["diff", "--numstat", range], worktreePath, { reject: false }),
10036
- git(["diff", range], worktreePath, { reject: false })
10037
- ]);
10038
- nameStatus = ns.stdout;
10039
- numstat = num2.stdout;
10040
- combinedDiff = diff.stdout;
10958
+ diffArgs = [`${commitSha}^!`];
10041
10959
  labelBase = commitSha.slice(0, 7);
10042
10960
  } else {
10043
- const [ns, num2, diff] = await Promise.all([
10044
- git(["diff", "--name-status", mergeBase], worktreePath, { reject: false }),
10045
- git(["diff", "--numstat", mergeBase], worktreePath, { reject: false }),
10046
- git(["diff", mergeBase], worktreePath, { reject: false })
10047
- ]);
10048
- nameStatus = ns.stdout;
10049
- numstat = num2.stdout;
10050
- combinedDiff = diff.stdout;
10051
- labelBase = base;
10961
+ diffArgs = [mergeBase];
10962
+ labelBase = baseLabel;
10052
10963
  }
10053
- const commits = await listBranchCommits(worktreePath, repoPath, { base });
10964
+ const partsPromise = gitDiffParts(
10965
+ worktreePath,
10966
+ diffArgs,
10967
+ includePatches,
10968
+ pathFilter
10969
+ );
10970
+ const [parts, meta, untrackedPaths] = await Promise.all([
10971
+ partsPromise,
10972
+ metaPromise,
10973
+ untrackedP
10974
+ ]);
10054
10975
  const filesMap = /* @__PURE__ */ new Map();
10055
- for (const file of filesFromDiff(nameStatus, numstat, combinedDiff, maxHunk)) {
10976
+ for (const file of filesFromDiff(
10977
+ parts.nameStatus,
10978
+ parts.numstat,
10979
+ parts.combinedDiff,
10980
+ maxHunk
10981
+ )) {
10056
10982
  filesMap.set(file.path, file);
10057
10983
  }
10058
10984
  if (includeUntracked) {
10059
- const untrackedFiles = await listUntrackedFiles(worktreePath, maxHunk);
10060
- for (const file of untrackedFiles) {
10985
+ const extra = includePatches ? await listUntrackedFiles(worktreePath, maxHunk, untrackedPaths) : untrackedStubs(untrackedPaths);
10986
+ for (const file of extra) {
10061
10987
  if (!filesMap.has(file.path)) filesMap.set(file.path, file);
10062
10988
  }
10989
+ } else if (fileOnly && pathFilter && !filesMap.has(pathFilter)) {
10990
+ const maybe = await listUntrackedPaths(worktreePath, pathFilter);
10991
+ if (maybe.length > 0) {
10992
+ const extra = includePatches ? await listUntrackedFiles(worktreePath, maxHunk, maybe) : untrackedStubs(maybe);
10993
+ for (const file of extra) filesMap.set(file.path, file);
10994
+ }
10063
10995
  }
10064
10996
  const files = [...filesMap.values()].sort((a, b) => a.path.localeCompare(b.path));
10065
- const { stdout: statOut } = await git(
10066
- 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],
10067
- worktreePath,
10068
- { reject: false }
10069
- );
10070
10997
  return {
10071
10998
  scope,
10072
10999
  commitSha: scope === "commits" ? commitSha : null,
10073
11000
  base: labelBase,
10074
11001
  files,
10075
- stat: statOut.trim() || (files.length ? `${files.length} file(s)` : "(no changes)"),
10076
- dirty: await isDirty(worktreePath),
10077
- unpushed: await countUnpushedCommits(worktreePath),
11002
+ stat: formatStat(files),
11003
+ dirty: fileOnly || !includeMeta ? files.length > 0 : meta.dirty,
11004
+ unpushed: meta.unpushed,
10078
11005
  hasLastTurnBase,
10079
- commits,
10080
- scopeStats
11006
+ commits: meta.commits,
11007
+ scopeStats: meta.scopeStats,
11008
+ mergeBase: needsMergeBase ? mergeBase : null
10081
11009
  };
10082
11010
  }
10083
11011
  async function listWorktreeFiles(worktreePath, opts) {
@@ -10115,8 +11043,8 @@ var DEFAULT_UPLOAD_MAX_BYTES = 5e7;
10115
11043
  function readWorktreeFileForUpload(worktreePath, relativePath, opts) {
10116
11044
  assertSafeRelativePath(relativePath);
10117
11045
  const maxBytes = opts?.maxBytes ?? DEFAULT_UPLOAD_MAX_BYTES;
10118
- const abs = (0, import_node_path20.join)(worktreePath, relativePath);
10119
- const st = (0, import_node_fs25.statSync)(abs);
11046
+ const abs = (0, import_node_path22.join)(worktreePath, relativePath);
11047
+ const st = (0, import_node_fs27.statSync)(abs);
10120
11048
  if (!st.isFile()) {
10121
11049
  throw new Error(`Not a file: ${relativePath}`);
10122
11050
  }
@@ -10125,7 +11053,7 @@ function readWorktreeFileForUpload(worktreePath, relativePath, opts) {
10125
11053
  `File too large to upload (${st.size} bytes; max ${maxBytes})`
10126
11054
  );
10127
11055
  }
10128
- const buf = (0, import_node_fs25.readFileSync)(abs);
11056
+ const buf = (0, import_node_fs27.readFileSync)(abs);
10129
11057
  return {
10130
11058
  path: relativePath,
10131
11059
  contentBase64: buf.toString("base64"),
@@ -10135,12 +11063,12 @@ function readWorktreeFileForUpload(worktreePath, relativePath, opts) {
10135
11063
  function readWorktreeFile(worktreePath, relativePath, opts) {
10136
11064
  assertSafeRelativePath(relativePath);
10137
11065
  const maxBytes = opts?.maxBytes ?? 2e5;
10138
- const abs = (0, import_node_path20.join)(worktreePath, relativePath);
10139
- const st = (0, import_node_fs25.statSync)(abs);
11066
+ const abs = (0, import_node_path22.join)(worktreePath, relativePath);
11067
+ const st = (0, import_node_fs27.statSync)(abs);
10140
11068
  if (!st.isFile()) {
10141
11069
  throw new Error(`Not a file: ${relativePath}`);
10142
11070
  }
10143
- const buf = (0, import_node_fs25.readFileSync)(abs);
11071
+ const buf = (0, import_node_fs27.readFileSync)(abs);
10144
11072
  if (isImageRelativePath(relativePath)) {
10145
11073
  const maxImageBytes = Math.max(maxBytes, 15e6);
10146
11074
  const truncated2 = buf.length > maxImageBytes;
@@ -10183,9 +11111,9 @@ function assertSafeRelativePath(relativePath) {
10183
11111
  }
10184
11112
  function writeWorktreeFile(worktreePath, relativePath, content) {
10185
11113
  assertSafeRelativePath(relativePath);
10186
- const abs = (0, import_node_path20.join)(worktreePath, relativePath);
10187
- (0, import_node_fs25.mkdirSync)((0, import_node_path20.dirname)(abs), { recursive: true });
10188
- (0, import_node_fs25.writeFileSync)(abs, content, "utf8");
11114
+ const abs = (0, import_node_path22.join)(worktreePath, relativePath);
11115
+ (0, import_node_fs27.mkdirSync)((0, import_node_path22.dirname)(abs), { recursive: true });
11116
+ (0, import_node_fs27.writeFileSync)(abs, content, "utf8");
10189
11117
  return { path: relativePath };
10190
11118
  }
10191
11119
  async function getDiffSummary(worktreePath, repoPath, opts) {
@@ -10364,9 +11292,9 @@ async function confirmLand(thread, opts) {
10364
11292
  }
10365
11293
 
10366
11294
  // src/skills/discover.ts
10367
- var import_node_fs26 = require("fs");
11295
+ var import_node_fs28 = require("fs");
10368
11296
  var import_node_os9 = require("os");
10369
- var import_node_path21 = require("path");
11297
+ var import_node_path23 = require("path");
10370
11298
  function toCommand(name) {
10371
11299
  return name.normalize("NFKD").replace(/[\u0300-\u036f]/g, "").toLowerCase().trim().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
10372
11300
  }
@@ -10398,7 +11326,7 @@ function parseFrontmatter(content) {
10398
11326
  }
10399
11327
  function readSkill(skillMd, source) {
10400
11328
  try {
10401
- const content = (0, import_node_fs26.readFileSync)(skillMd, "utf8");
11329
+ const content = (0, import_node_fs28.readFileSync)(skillMd, "utf8");
10402
11330
  const { name: fmName, description } = parseFrontmatter(content);
10403
11331
  const dirName = skillMd.split("/").slice(-2, -1)[0] || "skill";
10404
11332
  const name = fmName || dirName;
@@ -10417,19 +11345,19 @@ function readSkill(skillMd, source) {
10417
11345
  }
10418
11346
  }
10419
11347
  function scanSkillsDir(dir, source, out) {
10420
- if (!(0, import_node_fs26.existsSync)(dir)) return;
11348
+ if (!(0, import_node_fs28.existsSync)(dir)) return;
10421
11349
  let entries;
10422
11350
  try {
10423
- entries = (0, import_node_fs26.readdirSync)(dir);
11351
+ entries = (0, import_node_fs28.readdirSync)(dir);
10424
11352
  } catch {
10425
11353
  return;
10426
11354
  }
10427
11355
  for (const entry of entries) {
10428
11356
  if (entry.startsWith(".")) continue;
10429
- const skillMd = (0, import_node_path21.join)(dir, entry, "SKILL.md");
10430
- if (!(0, import_node_fs26.existsSync)(skillMd)) continue;
11357
+ const skillMd = (0, import_node_path23.join)(dir, entry, "SKILL.md");
11358
+ if (!(0, import_node_fs28.existsSync)(skillMd)) continue;
10431
11359
  try {
10432
- if (!(0, import_node_fs26.statSync)(skillMd).isFile()) continue;
11360
+ if (!(0, import_node_fs28.statSync)(skillMd).isFile()) continue;
10433
11361
  } catch {
10434
11362
  continue;
10435
11363
  }
@@ -10438,24 +11366,24 @@ function scanSkillsDir(dir, source, out) {
10438
11366
  }
10439
11367
  }
10440
11368
  function scanClaudePluginSkills(pluginsRoot, out) {
10441
- if (!(0, import_node_fs26.existsSync)(pluginsRoot)) return;
11369
+ if (!(0, import_node_fs28.existsSync)(pluginsRoot)) return;
10442
11370
  const walk = (dir, depth, lookingForSkillsDir) => {
10443
11371
  if (depth > 7) return;
10444
11372
  let entries;
10445
11373
  try {
10446
- entries = (0, import_node_fs26.readdirSync)(dir);
11374
+ entries = (0, import_node_fs28.readdirSync)(dir);
10447
11375
  } catch {
10448
11376
  return;
10449
11377
  }
10450
11378
  if (lookingForSkillsDir && entries.includes("SKILL.md")) {
10451
- const skill = readSkill((0, import_node_path21.join)(dir, "SKILL.md"), "cli");
11379
+ const skill = readSkill((0, import_node_path23.join)(dir, "SKILL.md"), "cli");
10452
11380
  if (skill) out.push(skill);
10453
11381
  }
10454
11382
  for (const entry of entries) {
10455
11383
  if (entry === "node_modules" || entry === ".git") continue;
10456
- const full = (0, import_node_path21.join)(dir, entry);
11384
+ const full = (0, import_node_path23.join)(dir, entry);
10457
11385
  try {
10458
- if (!(0, import_node_fs26.statSync)(full).isDirectory()) continue;
11386
+ if (!(0, import_node_fs28.statSync)(full).isDirectory()) continue;
10459
11387
  } catch {
10460
11388
  continue;
10461
11389
  }
@@ -10473,17 +11401,17 @@ function discoverSkills(worktreePath) {
10473
11401
  const home = (0, import_node_os9.homedir)();
10474
11402
  const collected = [];
10475
11403
  for (const rel of [".claude/skills", ".cursor/skills", ".sideboard/skills", ".brightsy/skills", "skills"]) {
10476
- scanSkillsDir((0, import_node_path21.join)(worktreePath, rel), "workspace", collected);
11404
+ scanSkillsDir((0, import_node_path23.join)(worktreePath, rel), "workspace", collected);
10477
11405
  }
10478
11406
  for (const abs of [
10479
- (0, import_node_path21.join)(home, ".claude/skills"),
10480
- (0, import_node_path21.join)(home, ".cursor/skills"),
10481
- (0, import_node_path21.join)(home, ".sideboard/skills"),
10482
- (0, import_node_path21.join)(home, ".brightsy/skills")
11407
+ (0, import_node_path23.join)(home, ".claude/skills"),
11408
+ (0, import_node_path23.join)(home, ".cursor/skills"),
11409
+ (0, import_node_path23.join)(home, ".sideboard/skills"),
11410
+ (0, import_node_path23.join)(home, ".brightsy/skills")
10483
11411
  ]) {
10484
11412
  scanSkillsDir(abs, "user", collected);
10485
11413
  }
10486
- scanClaudePluginSkills((0, import_node_path21.join)(home, ".claude/plugins"), collected);
11414
+ scanClaudePluginSkills((0, import_node_path23.join)(home, ".claude/plugins"), collected);
10487
11415
  const rank = { workspace: 0, user: 1, cli: 2 };
10488
11416
  const byCommand = /* @__PURE__ */ new Map();
10489
11417
  for (const skill of collected) {
@@ -10495,7 +11423,7 @@ function discoverSkills(worktreePath) {
10495
11423
  return [...byCommand.values()].sort((a, b) => a.command.localeCompare(b.command));
10496
11424
  }
10497
11425
  function readSkillBody(skillPath, maxChars = 12e3) {
10498
- const raw = (0, import_node_fs26.readFileSync)(skillPath, "utf8");
11426
+ const raw = (0, import_node_fs28.readFileSync)(skillPath, "utf8");
10499
11427
  if (raw.startsWith("---")) {
10500
11428
  const end = raw.indexOf("\n---", 3);
10501
11429
  if (end >= 0) {
@@ -10588,9 +11516,9 @@ function expandComposerPrompt(worktreePath, prompt, opts) {
10588
11516
  }
10589
11517
 
10590
11518
  // src/composer/stage-files.ts
10591
- var import_node_fs27 = require("fs");
10592
- var import_node_path22 = require("path");
10593
- var import_node_crypto5 = require("crypto");
11519
+ var import_node_fs29 = require("fs");
11520
+ var import_node_path24 = require("path");
11521
+ var import_node_crypto7 = require("crypto");
10594
11522
  init_workspace_scratch();
10595
11523
  var IMAGE_EXTENSIONS2 = /* @__PURE__ */ new Set([
10596
11524
  "png",
@@ -10615,7 +11543,7 @@ var IMAGE_MIME_BY_EXT = {
10615
11543
  var MAX_INLINE_BYTES = 4e5;
10616
11544
  var MAX_PREVIEW_BYTES = 5e6;
10617
11545
  function fileExtension(filePath) {
10618
- const base = (0, import_node_path22.basename)(filePath).toLowerCase();
11546
+ const base = (0, import_node_path24.basename)(filePath).toLowerCase();
10619
11547
  return base.includes(".") ? base.split(".").pop() || "" : "";
10620
11548
  }
10621
11549
  function isImageFilePath(filePath) {
@@ -10625,24 +11553,24 @@ function imageMimeType(filePath) {
10625
11553
  return IMAGE_MIME_BY_EXT[fileExtension(filePath)] || "image/png";
10626
11554
  }
10627
11555
  function ensureAttachmentsDir(worktreePath) {
10628
- const dir = (0, import_node_path22.join)(worktreePath, ATTACHMENTS_DIR);
10629
- (0, import_node_fs27.mkdirSync)(dir, { recursive: true });
10630
- const gi = (0, import_node_path22.join)(dir, ".gitignore");
10631
- if (!(0, import_node_fs27.existsSync)(gi)) {
10632
- (0, import_node_fs27.writeFileSync)(gi, attachmentsGitignoreBody(), "utf8");
11556
+ const dir = (0, import_node_path24.join)(worktreePath, ATTACHMENTS_DIR);
11557
+ (0, import_node_fs29.mkdirSync)(dir, { recursive: true });
11558
+ const gi = (0, import_node_path24.join)(dir, ".gitignore");
11559
+ if (!(0, import_node_fs29.existsSync)(gi)) {
11560
+ (0, import_node_fs29.writeFileSync)(gi, attachmentsGitignoreBody(), "utf8");
10633
11561
  }
10634
11562
  return dir;
10635
11563
  }
10636
11564
  function uniqueAttachmentName(dir, originalName) {
10637
11565
  const safe = originalName.replace(/[/\\]/g, "_") || "file";
10638
- if (!(0, import_node_fs27.existsSync)((0, import_node_path22.join)(dir, safe))) return safe;
10639
- const ext = (0, import_node_path22.extname)(safe);
11566
+ if (!(0, import_node_fs29.existsSync)((0, import_node_path24.join)(dir, safe))) return safe;
11567
+ const ext = (0, import_node_path24.extname)(safe);
10640
11568
  const stem = ext ? safe.slice(0, -ext.length) : safe;
10641
11569
  for (let i = 1; i < 1e4; i++) {
10642
11570
  const candidate = `${stem}-${i}${ext}`;
10643
- if (!(0, import_node_fs27.existsSync)((0, import_node_path22.join)(dir, candidate))) return candidate;
11571
+ if (!(0, import_node_fs29.existsSync)((0, import_node_path24.join)(dir, candidate))) return candidate;
10644
11572
  }
10645
- return `${stem}-${(0, import_node_crypto5.randomUUID)()}${ext}`;
11573
+ return `${stem}-${(0, import_node_crypto7.randomUUID)()}${ext}`;
10646
11574
  }
10647
11575
  function previewDataUrlFromBuf(filePath, buf) {
10648
11576
  if (!isImageFilePath(filePath)) return void 0;
@@ -10654,7 +11582,7 @@ function attachmentFromBuffer(name, buf, opts) {
10654
11582
  if (isImageFilePath(name)) {
10655
11583
  const pathHint = opts.path ? `\`${opts.path}\`` : opts.sourceLabel || name;
10656
11584
  return {
10657
- id: (0, import_node_crypto5.randomUUID)(),
11585
+ id: (0, import_node_crypto7.randomUUID)(),
10658
11586
  name,
10659
11587
  kind: "file",
10660
11588
  path: opts.path,
@@ -10667,7 +11595,7 @@ function attachmentFromBuffer(name, buf, opts) {
10667
11595
  }
10668
11596
  if (buf.length > MAX_INLINE_BYTES) {
10669
11597
  return {
10670
- id: (0, import_node_crypto5.randomUUID)(),
11598
+ id: (0, import_node_crypto7.randomUUID)(),
10671
11599
  name,
10672
11600
  kind: "file",
10673
11601
  path: opts.path,
@@ -10676,7 +11604,7 @@ function attachmentFromBuffer(name, buf, opts) {
10676
11604
  }
10677
11605
  if (buf.includes(0)) {
10678
11606
  return {
10679
- id: (0, import_node_crypto5.randomUUID)(),
11607
+ id: (0, import_node_crypto7.randomUUID)(),
10680
11608
  name,
10681
11609
  kind: "file",
10682
11610
  path: opts.path,
@@ -10684,7 +11612,7 @@ function attachmentFromBuffer(name, buf, opts) {
10684
11612
  };
10685
11613
  }
10686
11614
  return {
10687
- id: (0, import_node_crypto5.randomUUID)(),
11615
+ id: (0, import_node_crypto7.randomUUID)(),
10688
11616
  name,
10689
11617
  kind: "file",
10690
11618
  path: opts.path,
@@ -10696,19 +11624,19 @@ function stageAbsolutePathsAsAttachments(worktreePath, absolutePaths) {
10696
11624
  const dir = ensureAttachmentsDir(worktreePath);
10697
11625
  const out = [];
10698
11626
  for (const abs of absolutePaths) {
10699
- const originalName = (0, import_node_path22.basename)(abs);
11627
+ const originalName = (0, import_node_path24.basename)(abs);
10700
11628
  try {
10701
- const st = (0, import_node_fs27.statSync)(abs);
11629
+ const st = (0, import_node_fs29.statSync)(abs);
10702
11630
  if (!st.isFile()) continue;
10703
11631
  const name = uniqueAttachmentName(dir, originalName);
10704
- const destAbs = (0, import_node_path22.join)(dir, name);
10705
- (0, import_node_fs27.copyFileSync)(abs, destAbs);
11632
+ const destAbs = (0, import_node_path24.join)(dir, name);
11633
+ (0, import_node_fs29.copyFileSync)(abs, destAbs);
10706
11634
  const rel = `${ATTACHMENTS_DIR}/${name}`;
10707
- const buf = (0, import_node_fs27.readFileSync)(destAbs);
11635
+ const buf = (0, import_node_fs29.readFileSync)(destAbs);
10708
11636
  out.push(attachmentFromBuffer(name, buf, { path: rel, sourceLabel: abs }));
10709
11637
  } catch (err) {
10710
11638
  out.push({
10711
- id: (0, import_node_crypto5.randomUUID)(),
11639
+ id: (0, import_node_crypto7.randomUUID)(),
10712
11640
  name: originalName,
10713
11641
  kind: "file",
10714
11642
  content: `(could not attach ${abs}: ${err instanceof Error ? err.message : String(err)})`
@@ -10726,13 +11654,13 @@ function stageBuffersAsAttachments(worktreePath, buffers) {
10726
11654
  try {
10727
11655
  const buf = Buffer.from(item.dataBase64, "base64");
10728
11656
  const name = uniqueAttachmentName(dir, originalName);
10729
- const destAbs = (0, import_node_path22.join)(dir, name);
10730
- (0, import_node_fs27.writeFileSync)(destAbs, buf);
11657
+ const destAbs = (0, import_node_path24.join)(dir, name);
11658
+ (0, import_node_fs29.writeFileSync)(destAbs, buf);
10731
11659
  const rel = `${ATTACHMENTS_DIR}/${name}`;
10732
11660
  out.push(attachmentFromBuffer(name, buf, { path: rel }));
10733
11661
  } catch (err) {
10734
11662
  out.push({
10735
- id: (0, import_node_crypto5.randomUUID)(),
11663
+ id: (0, import_node_crypto7.randomUUID)(),
10736
11664
  name: originalName,
10737
11665
  kind: "file",
10738
11666
  content: `(could not attach ${originalName}: ${err instanceof Error ? err.message : String(err)})`
@@ -10746,23 +11674,23 @@ function attachmentsFromWorktreePaths(worktreePath, relativePaths) {
10746
11674
  for (const rel of relativePaths) {
10747
11675
  if (!rel || rel.includes("..") || rel.startsWith("/")) {
10748
11676
  out.push({
10749
- id: (0, import_node_crypto5.randomUUID)(),
10750
- name: (0, import_node_path22.basename)(rel) || "file",
11677
+ id: (0, import_node_crypto7.randomUUID)(),
11678
+ name: (0, import_node_path24.basename)(rel) || "file",
10751
11679
  kind: "file",
10752
11680
  content: `(invalid path: ${rel})`
10753
11681
  });
10754
11682
  continue;
10755
11683
  }
10756
- const name = (0, import_node_path22.basename)(rel);
11684
+ const name = (0, import_node_path24.basename)(rel);
10757
11685
  try {
10758
- const abs = (0, import_node_path22.join)(worktreePath, rel);
10759
- const st = (0, import_node_fs27.statSync)(abs);
11686
+ const abs = (0, import_node_path24.join)(worktreePath, rel);
11687
+ const st = (0, import_node_fs29.statSync)(abs);
10760
11688
  if (!st.isFile()) continue;
10761
- const buf = (0, import_node_fs27.readFileSync)(abs);
11689
+ const buf = (0, import_node_fs29.readFileSync)(abs);
10762
11690
  out.push(attachmentFromBuffer(name, buf, { path: rel, sourceLabel: abs }));
10763
11691
  } catch (err) {
10764
11692
  out.push({
10765
- id: (0, import_node_crypto5.randomUUID)(),
11693
+ id: (0, import_node_crypto7.randomUUID)(),
10766
11694
  name,
10767
11695
  kind: "file",
10768
11696
  content: `(could not read ${rel}: ${err instanceof Error ? err.message : String(err)})`
@@ -10773,8 +11701,8 @@ function attachmentsFromWorktreePaths(worktreePath, relativePaths) {
10773
11701
  }
10774
11702
 
10775
11703
  // src/agents/instructions.ts
10776
- var import_node_fs28 = require("fs");
10777
- var import_node_path23 = require("path");
11704
+ var import_node_fs30 = require("fs");
11705
+ var import_node_path25 = require("path");
10778
11706
  init_worktree_labels();
10779
11707
  function normPath2(p) {
10780
11708
  return p.replace(/\/+$/, "");
@@ -11020,7 +11948,7 @@ var Orchestrator = class {
11020
11948
  }
11021
11949
  continue;
11022
11950
  }
11023
- if (!(0, import_node_fs31.existsSync)(thread.worktreePath)) {
11951
+ if (!(0, import_node_fs33.existsSync)(thread.worktreePath)) {
11024
11952
  setStatus(thread.id, "broken", "Worktree missing on disk");
11025
11953
  this.emit({ type: "status_changed", threadId: thread.id, status: "broken" });
11026
11954
  continue;
@@ -11276,11 +12204,11 @@ var Orchestrator = class {
11276
12204
  return results;
11277
12205
  }
11278
12206
  /** Edit the text of a not-yet-started queued message. */
11279
- async editQueuedMessage(threadRef, index, text) {
12207
+ async editQueuedMessage(threadRef, index, text2) {
11280
12208
  const thread = this.requireThread(threadRef);
11281
12209
  return withThreadLock(thread.id, async () => {
11282
12210
  const current = this.requireThread(thread.id);
11283
- const trimmed = text.trim();
12211
+ const trimmed = text2.trim();
11284
12212
  if (!trimmed || index < 0 || index >= current.queue.length) {
11285
12213
  return current;
11286
12214
  }
@@ -11425,10 +12353,19 @@ var Orchestrator = class {
11425
12353
  attachments: sentAttachments ?? thread.attachments
11426
12354
  }
11427
12355
  );
11428
- const orchestrationReminder = isOrchestratorThread(thread) ? coordinatorTurnReminder({
11429
- parentId: threadId,
11430
- goal: thread.sourceRef || thread.title
11431
- }) : null;
12356
+ const slackInbound = /^(Slack DM|Slack @mention)(?:\n|$)/.test(
12357
+ expandedPrompt.trim()
12358
+ );
12359
+ const orchestrationReminder = isOrchestratorThread(thread) ? [
12360
+ coordinatorTurnReminder({
12361
+ parentId: threadId,
12362
+ goal: thread.sourceRef || thread.title
12363
+ }),
12364
+ slackInbound ? [
12365
+ "This turn is a Slack DM or @mention. Your reply is posted back in Slack \u2014 keep it concise. Sideboard signs it with this Mac's destination name; do not prefix the name yourself.",
12366
+ SLACK_REPLY_FORMATTING
12367
+ ].join("\n") : null
12368
+ ].filter(Boolean).join("\n") : null;
11432
12369
  const artifactReminder = thread.agent !== "brightsy" ? [
11433
12370
  "Sideboard side column (important):",
11434
12371
  "There is no claude.ai Artifact tool here \u2014 that is normal.",
@@ -11488,7 +12425,9 @@ var Orchestrator = class {
11488
12425
  goal: fresh.sourceRef || fresh.title || "Orchestration",
11489
12426
  parentId: fresh.id,
11490
12427
  workspaces: inventory,
11491
- audience: "desktop"
12428
+ audience: /^(Slack DM|Slack @mention)(?:\n|$)/.test(
12429
+ expandedPrompt.trim()
12430
+ ) ? "slack" : "desktop"
11492
12431
  });
11493
12432
  }
11494
12433
  }
@@ -12031,6 +12970,11 @@ var Orchestrator = class {
12031
12970
  return getDiff(thread.worktreePath, thread.repoPath, {
12032
12971
  scope: opts?.scope,
12033
12972
  commitSha: opts?.commitSha,
12973
+ base: opts?.base,
12974
+ includePatches: opts?.includePatches,
12975
+ includeMeta: opts?.includeMeta,
12976
+ includeUntracked: opts?.includeUntracked,
12977
+ path: opts?.path,
12034
12978
  lastTurnBase: this.turnBaselines.get(thread.id) ?? null
12035
12979
  });
12036
12980
  }
@@ -12108,7 +13052,8 @@ var Orchestrator = class {
12108
13052
  isDraft: false,
12109
13053
  reviewDecision: null,
12110
13054
  baseRefName: "",
12111
- headRefName: ""
13055
+ headRefName: "",
13056
+ isInMergeQueue: false
12112
13057
  };
12113
13058
  await this.persistPrMetaAndMaybeArchive(thread, metaLike);
12114
13059
  return { url: metaLike.url, state };
@@ -12423,7 +13368,7 @@ var Orchestrator = class {
12423
13368
  this.emit({ type: "status_changed", threadId: restored2.id, status: restored2.status });
12424
13369
  return restored2;
12425
13370
  }
12426
- if (!(0, import_node_fs31.existsSync)(thread.worktreePath)) {
13371
+ if (!(0, import_node_fs33.existsSync)(thread.worktreePath)) {
12427
13372
  const { createThreadWorktree: createThreadWorktree2 } = await Promise.resolve().then(() => (init_worktree(), worktree_exports));
12428
13373
  const { execa: execa7 } = await import("execa");
12429
13374
  const slug = thread.worktreePath.split("/").pop();
@@ -12504,8 +13449,105 @@ init_worktree();
12504
13449
  init_run();
12505
13450
  init_app_settings();
12506
13451
 
12507
- // src/integrations/linear.ts
13452
+ // src/integrations/linear-oauth.ts
13453
+ var import_node_crypto8 = require("crypto");
13454
+ var import_node_http = require("http");
12508
13455
  init_app_settings();
13456
+
13457
+ // src/integrations/linear-app.ts
13458
+ var BAKED_LINEAR_CLIENT_ID = "39a15abc7ea5bea17c47f47f85ff5fd0";
13459
+ var BAKED_LINEAR_CLIENT_SECRET = "";
13460
+
13461
+ // src/integrations/linear-oauth.ts
13462
+ var LINEAR_OAUTH_PORT = 19848;
13463
+ var LINEAR_OAUTH_REDIRECT = `http://127.0.0.1:${LINEAR_OAUTH_PORT}/callback`;
13464
+ var LINEAR_TOKEN = "https://api.linear.app/oauth/token";
13465
+ var REFRESH_SKEW_MS = 5 * 6e4;
13466
+ function linearOAuthCredentials() {
13467
+ const settings = loadAppSettings();
13468
+ const clientId = process.env.SIDEBOARD_LINEAR_CLIENT_ID?.trim() || settings.integrations.linearClientId?.trim() || BAKED_LINEAR_CLIENT_ID.trim();
13469
+ const clientSecret = process.env.SIDEBOARD_LINEAR_CLIENT_SECRET?.trim() || settings.integrations.linearClientSecret?.trim() || BAKED_LINEAR_CLIENT_SECRET.trim();
13470
+ if (!clientId) {
13471
+ throw new Error(
13472
+ `Linear browser sign-in needs a Linear OAuth app Client ID. Create one at linear.app/settings/api/applications/new with callback ${LINEAR_OAUTH_REDIRECT}, then set SIDEBOARD_LINEAR_CLIENT_ID (PKCE does not require a secret). You can still paste a personal API key.`
13473
+ );
13474
+ }
13475
+ return { clientId, clientSecret };
13476
+ }
13477
+ function linearAuthorizationHeader(token) {
13478
+ const t = token.trim();
13479
+ if (!t) return t;
13480
+ if (/^bearer\s+/i.test(t)) return t;
13481
+ if (t.startsWith("lin_api_")) return t;
13482
+ return `Bearer ${t}`;
13483
+ }
13484
+ async function exchangeLinearToken(body) {
13485
+ const res = await fetch(LINEAR_TOKEN, {
13486
+ method: "POST",
13487
+ headers: { "Content-Type": "application/x-www-form-urlencoded" },
13488
+ body
13489
+ });
13490
+ const data = await res.json().catch(() => ({}));
13491
+ if (!res.ok || data.error || !data.access_token?.trim()) {
13492
+ const detail = data.error_description || data.error || `HTTP ${res.status}`;
13493
+ throw new Error(`Linear OAuth token exchange failed: ${detail}`);
13494
+ }
13495
+ return data;
13496
+ }
13497
+ async function persistLinearTokens(data, viewer) {
13498
+ return saveLinearOAuth({
13499
+ accessToken: data.access_token.trim(),
13500
+ refreshToken: data.refresh_token?.trim(),
13501
+ expiresIn: data.expires_in,
13502
+ viewerName: viewer?.name,
13503
+ organizationName: viewer?.organizationName
13504
+ });
13505
+ }
13506
+ var refreshInFlight = null;
13507
+ async function refreshLinearAccessToken(refreshToken) {
13508
+ const { clientId, clientSecret } = linearOAuthCredentials();
13509
+ const body = new URLSearchParams({
13510
+ grant_type: "refresh_token",
13511
+ refresh_token: refreshToken,
13512
+ client_id: clientId
13513
+ });
13514
+ if (clientSecret) body.set("client_secret", clientSecret);
13515
+ try {
13516
+ const data = await exchangeLinearToken(body);
13517
+ await persistLinearTokens(data);
13518
+ return data.access_token.trim();
13519
+ } catch (err) {
13520
+ const message = err instanceof Error ? err.message : String(err);
13521
+ if (/invalid_grant|invalid_token|unauthorized/i.test(message)) {
13522
+ disconnectLinearConnection();
13523
+ throw new Error("Linear sign-in expired \u2014 connect again in Account settings");
13524
+ }
13525
+ throw err;
13526
+ }
13527
+ }
13528
+ async function getLinearAuthToken(settings = loadAppSettings()) {
13529
+ const access = settings.integrations.linearAccessToken?.trim() || "";
13530
+ const refresh = settings.integrations.linearRefreshToken?.trim() || "";
13531
+ const expiresAt = settings.integrations.linearTokenExpiresAt ?? 0;
13532
+ const apiKey = settings.integrations.linearApiKey?.trim() || "";
13533
+ if (access && (!refresh || Date.now() < expiresAt - REFRESH_SKEW_MS)) {
13534
+ return access;
13535
+ }
13536
+ if (refresh) {
13537
+ if (!refreshInFlight) {
13538
+ refreshInFlight = refreshLinearAccessToken(refresh).finally(() => {
13539
+ refreshInFlight = null;
13540
+ });
13541
+ }
13542
+ return refreshInFlight.catch((err) => {
13543
+ if (apiKey) return apiKey;
13544
+ throw err;
13545
+ });
13546
+ }
13547
+ return apiKey || null;
13548
+ }
13549
+
13550
+ // src/integrations/linear.ts
12509
13551
  var LINEAR_GRAPHQL = "https://api.linear.app/graphql";
12510
13552
  var ASSIGNED_ISSUES_QUERY = `
12511
13553
  query SideboardAssignedIssues($first: Int!) {
@@ -12527,16 +13569,16 @@ query SideboardAssignedIssues($first: Int!) {
12527
13569
  }
12528
13570
  `;
12529
13571
  async function listLinearIssuesDirect(opts) {
12530
- const apiKey = (opts?.apiKey ?? getLinearApiKey())?.trim();
13572
+ const apiKey = (opts?.apiKey ?? await getLinearAuthToken())?.trim();
12531
13573
  if (!apiKey) {
12532
- throw new Error("Linear is not connected \u2014 add an API key in Account settings");
13574
+ throw new Error("Linear is not connected \u2014 sign in from Account settings");
12533
13575
  }
12534
13576
  const first = Math.max(1, Math.min(100, opts?.limit ?? 50));
12535
13577
  const res = await fetch(LINEAR_GRAPHQL, {
12536
13578
  method: "POST",
12537
13579
  headers: {
12538
13580
  "Content-Type": "application/json",
12539
- Authorization: apiKey
13581
+ Authorization: linearAuthorizationHeader(apiKey)
12540
13582
  },
12541
13583
  body: JSON.stringify({
12542
13584
  query: ASSIGNED_ISSUES_QUERY,
@@ -12633,6 +13675,647 @@ function mcpArchiveBlockedReason(thread) {
12633
13675
 
12634
13676
  // src/mcp/server.ts
12635
13677
  init_profile();
13678
+
13679
+ // src/mcp/slack-tools.ts
13680
+ var import_zod = require("zod");
13681
+
13682
+ // src/slack/api.ts
13683
+ var SLACK_API = "https://slack.com/api";
13684
+ var SlackApiError = class extends Error {
13685
+ constructor(method, slackError) {
13686
+ super(`Slack ${method}: ${slackError}`);
13687
+ this.method = method;
13688
+ this.slackError = slackError;
13689
+ this.name = "SlackApiError";
13690
+ }
13691
+ method;
13692
+ slackError;
13693
+ };
13694
+ async function slackApi(token, method, params, fetchImpl) {
13695
+ const doFetch = fetchImpl ?? fetch;
13696
+ const body = new URLSearchParams();
13697
+ if (params) {
13698
+ for (const [key, value] of Object.entries(params)) {
13699
+ if (value === void 0) continue;
13700
+ body.set(key, String(value));
13701
+ }
13702
+ }
13703
+ const res = await doFetch(`${SLACK_API}/${method}`, {
13704
+ method: "POST",
13705
+ headers: {
13706
+ Authorization: `Bearer ${token}`,
13707
+ "Content-Type": "application/x-www-form-urlencoded"
13708
+ },
13709
+ body
13710
+ });
13711
+ const json = await res.json();
13712
+ if (!json.ok) {
13713
+ throw new SlackApiError(method, json.error || `HTTP ${res.status}`);
13714
+ }
13715
+ return json;
13716
+ }
13717
+
13718
+ // src/slack/destination.ts
13719
+ function isChannelId(raw) {
13720
+ return /^[CGD][A-Z0-9]+$/i.test(raw);
13721
+ }
13722
+ function isUserId(raw) {
13723
+ return /^U[A-Z0-9]+$/i.test(raw);
13724
+ }
13725
+ function isEmail(raw) {
13726
+ return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(raw);
13727
+ }
13728
+ function normalizeSlackHandle(raw) {
13729
+ let s = raw.trim();
13730
+ const mention = s.match(/^<@([UW][A-Z0-9]+)(?:\|[^>]+)?>$/i);
13731
+ if (mention?.[1]) return mention[1];
13732
+ const channelMention = s.match(/^<#([CGD][A-Z0-9]+)(?:\|[^>]+)?>$/i);
13733
+ if (channelMention?.[1]) return channelMention[1];
13734
+ if (s.startsWith("#") || s.startsWith("@")) s = s.slice(1);
13735
+ return s.trim();
13736
+ }
13737
+ async function listSlackUsers(token, opts) {
13738
+ const query = opts?.query?.trim().toLowerCase() ?? "";
13739
+ const limit = Math.min(200, Math.max(1, opts?.limit ?? 50));
13740
+ const members = [];
13741
+ let cursor;
13742
+ do {
13743
+ const data = await slackApi(
13744
+ token,
13745
+ "users.list",
13746
+ {
13747
+ limit: 200,
13748
+ cursor: cursor || void 0
13749
+ },
13750
+ opts?.fetchImpl
13751
+ );
13752
+ for (const m of data.members ?? []) {
13753
+ if (!m.id || m.deleted || m.is_bot) continue;
13754
+ members.push(m);
13755
+ }
13756
+ cursor = data.response_metadata?.next_cursor?.trim() || void 0;
13757
+ } while (cursor && members.length < 2e3);
13758
+ const mapped = members.map((m) => ({
13759
+ id: m.id,
13760
+ name: m.name ?? m.id,
13761
+ real_name: m.profile?.real_name,
13762
+ display_name: m.profile?.display_name,
13763
+ email: m.profile?.email
13764
+ }));
13765
+ const filtered = query ? mapped.filter((u) => {
13766
+ const hay = [u.name, u.real_name, u.display_name, u.email, u.id].filter(Boolean).join(" ").toLowerCase();
13767
+ return hay.includes(query);
13768
+ }) : mapped;
13769
+ return filtered.slice(0, limit);
13770
+ }
13771
+ async function findUserId(token, handle, fetchImpl) {
13772
+ if (isUserId(handle)) {
13773
+ return { id: handle, name: handle };
13774
+ }
13775
+ if (isEmail(handle)) {
13776
+ try {
13777
+ const data = await slackApi(
13778
+ token,
13779
+ "users.lookupByEmail",
13780
+ { email: handle },
13781
+ fetchImpl
13782
+ );
13783
+ if (data.user?.id) {
13784
+ return {
13785
+ id: data.user.id,
13786
+ name: data.user.name ?? data.user.id,
13787
+ real_name: data.user.profile?.real_name,
13788
+ display_name: data.user.profile?.display_name,
13789
+ email: data.user.profile?.email
13790
+ };
13791
+ }
13792
+ } catch {
13793
+ }
13794
+ }
13795
+ const users = await listSlackUsers(token, { query: handle, limit: 20, fetchImpl });
13796
+ const exact = users.find(
13797
+ (u) => u.name.toLowerCase() === handle.toLowerCase() || u.display_name?.toLowerCase() === handle.toLowerCase() || u.real_name?.toLowerCase() === handle.toLowerCase() || u.email?.toLowerCase() === handle.toLowerCase()
13798
+ );
13799
+ if (exact) return exact;
13800
+ if (users.length === 1) return users[0];
13801
+ if (users.length === 0) {
13802
+ throw new Error(`No Slack user matching "${handle}"`);
13803
+ }
13804
+ throw new Error(
13805
+ `Ambiguous Slack user "${handle}". Matches: ${users.slice(0, 5).map((u) => `${u.name} (${u.id})`).join(", ")}. Pass a user id (U\u2026) or use slack_list_users.`
13806
+ );
13807
+ }
13808
+ async function openDmChannel(token, userId, fetchImpl) {
13809
+ const data = await slackApi(
13810
+ token,
13811
+ "conversations.open",
13812
+ { users: userId },
13813
+ fetchImpl
13814
+ );
13815
+ const id = data.channel?.id?.trim();
13816
+ if (!id) throw new Error(`Could not open Slack DM with user ${userId}`);
13817
+ return id;
13818
+ }
13819
+ async function resolveChannelByName(token, name, fetchImpl) {
13820
+ const data = await slackApi(
13821
+ token,
13822
+ "conversations.list",
13823
+ {
13824
+ types: "public_channel,private_channel",
13825
+ exclude_archived: true,
13826
+ limit: 200
13827
+ },
13828
+ fetchImpl
13829
+ );
13830
+ const match = (data.channels ?? []).find((c) => c.name?.toLowerCase() === name.toLowerCase());
13831
+ if (!match?.id) throw new Error(`No Slack channel named #${name}`);
13832
+ return match.id;
13833
+ }
13834
+ async function resolveSlackDestination(token, destination, opts) {
13835
+ const raw = destination.trim();
13836
+ if (!raw) throw new Error("destination is required");
13837
+ const hadAt = raw.startsWith("@") || /^<@/i.test(raw);
13838
+ const hadHash = raw.startsWith("#") || /^<#/i.test(raw);
13839
+ const handle = normalizeSlackHandle(raw);
13840
+ if (isChannelId(handle)) {
13841
+ return {
13842
+ channelId: handle,
13843
+ kind: handle.toUpperCase().startsWith("D") ? "dm" : "channel",
13844
+ label: handle
13845
+ };
13846
+ }
13847
+ if (isUserId(handle) || hadAt || isEmail(handle)) {
13848
+ const user = await findUserId(token, handle, opts?.fetchImpl);
13849
+ if (opts?.forRead) {
13850
+ const channelId2 = await openDmChannel(token, user.id, opts?.fetchImpl);
13851
+ return {
13852
+ channelId: channelId2,
13853
+ kind: "user",
13854
+ userId: user.id,
13855
+ label: user.display_name || user.real_name || user.name
13856
+ };
13857
+ }
13858
+ const channelId = await openDmChannel(token, user.id, opts?.fetchImpl);
13859
+ return {
13860
+ channelId,
13861
+ kind: "user",
13862
+ userId: user.id,
13863
+ label: user.display_name || user.real_name || user.name
13864
+ };
13865
+ }
13866
+ if (hadHash) {
13867
+ const channelId = await resolveChannelByName(token, handle, opts?.fetchImpl);
13868
+ return { channelId, kind: "channel", label: `#${handle}` };
13869
+ }
13870
+ try {
13871
+ const channelId = await resolveChannelByName(token, handle, opts?.fetchImpl);
13872
+ return { channelId, kind: "channel", label: `#${handle}` };
13873
+ } catch (channelErr) {
13874
+ try {
13875
+ const user = await findUserId(token, handle, opts?.fetchImpl);
13876
+ const channelId = await openDmChannel(token, user.id, opts?.fetchImpl);
13877
+ return {
13878
+ channelId,
13879
+ kind: "user",
13880
+ userId: user.id,
13881
+ label: user.display_name || user.real_name || user.name
13882
+ };
13883
+ } catch {
13884
+ throw channelErr;
13885
+ }
13886
+ }
13887
+ }
13888
+
13889
+ // src/slack/github-link.ts
13890
+ function labelGithubUrl(url) {
13891
+ const raw = url.trim();
13892
+ if (!raw) return null;
13893
+ let parsed;
13894
+ try {
13895
+ parsed = new URL(raw);
13896
+ } catch {
13897
+ return null;
13898
+ }
13899
+ if (!/(^|\.)github\.com$/i.test(parsed.hostname)) return null;
13900
+ const path = parsed.pathname;
13901
+ const hash = parsed.hash || "";
13902
+ const pr = path.match(/\/([^/]+)\/([^/]+)\/pull\/(\d+)/i);
13903
+ if (pr) {
13904
+ const n = pr[3];
13905
+ if (/#discussion_r\d+/i.test(hash) || /#pullrequestreview-\d+/i.test(hash)) {
13906
+ return { kind: "comment", label: `PR #${n} comment`, url: raw };
13907
+ }
13908
+ if (/#issuecomment-\d+/i.test(hash)) {
13909
+ return { kind: "comment", label: `PR #${n} comment`, url: raw };
13910
+ }
13911
+ return { kind: "pr", label: `PR #${n}`, url: raw };
13912
+ }
13913
+ const issueComment = path.match(/\/([^/]+)\/([^/]+)\/issues\/(\d+)/i);
13914
+ if (issueComment && /#issuecomment-\d+/i.test(hash)) {
13915
+ return { kind: "comment", label: `Issue #${issueComment[3]} comment`, url: raw };
13916
+ }
13917
+ const blob = path.match(/\/([^/]+)\/([^/]+)\/blob\/[^/]+\/(.+)$/i);
13918
+ if (blob) {
13919
+ const filePath = decodeURIComponent(blob[3]);
13920
+ const fileName = filePath.split("/").pop() || filePath;
13921
+ const line = hash.match(/^#L(\d+)(?:-L(\d+))?$/i);
13922
+ if (line) {
13923
+ const start = line[1];
13924
+ const end = line[2];
13925
+ const loc = end && end !== start ? `${start}-${end}` : start;
13926
+ return { kind: "code", label: `${fileName}:${loc}`, url: raw };
13927
+ }
13928
+ return { kind: "code", label: fileName, url: raw };
13929
+ }
13930
+ return { kind: "other", label: "GitHub", url: raw };
13931
+ }
13932
+ function appendGithubLink(text2, githubUrl) {
13933
+ const body = text2.trimEnd();
13934
+ if (!githubUrl?.trim()) return body;
13935
+ const labeled = labelGithubUrl(githubUrl);
13936
+ if (!labeled) {
13937
+ return body ? `${body}
13938
+ ${githubUrl.trim()}` : githubUrl.trim();
13939
+ }
13940
+ const link = `<${labeled.url}|${labeled.label}>`;
13941
+ return body ? `${body}
13942
+ ${link}` : link;
13943
+ }
13944
+
13945
+ // src/slack/outbound-watch.ts
13946
+ var import_node_fs34 = require("fs");
13947
+ var import_node_path29 = require("path");
13948
+ init_paths();
13949
+ init_private_file();
13950
+ init_secure_file();
13951
+
13952
+ // src/slack/workspaces.ts
13953
+ var import_node_path28 = require("path");
13954
+ init_paths();
13955
+ init_secure_file();
13956
+ function storePath2() {
13957
+ return (0, import_node_path28.join)(appDataDir(), "slack-workspaces.json");
13958
+ }
13959
+ function readStore2() {
13960
+ try {
13961
+ const path = storePath2();
13962
+ const wasEncrypted = isSecureFileEncrypted(path);
13963
+ const parsed = readSecureJson(path);
13964
+ const workspaces = Array.isArray(parsed?.workspaces) ? parsed.workspaces : [];
13965
+ if (workspaces.length > 0 && !wasEncrypted && resolveVaultKey()) {
13966
+ writeSecureJson(path, { workspaces });
13967
+ }
13968
+ return workspaces;
13969
+ } catch {
13970
+ return [];
13971
+ }
13972
+ }
13973
+ function toInfo(ws) {
13974
+ return {
13975
+ team_id: ws.team_id,
13976
+ team_name: ws.team_name,
13977
+ user_id: ws.user_id,
13978
+ has_bot_token: Boolean(ws.bot_token),
13979
+ has_user_token: Boolean(ws.user_token),
13980
+ connected_at: ws.connected_at
13981
+ };
13982
+ }
13983
+ function listSlackWorkspaces() {
13984
+ return readStore2().map(toInfo).sort((a, b) => a.team_name.localeCompare(b.team_name));
13985
+ }
13986
+ function getSlackWorkspace(teamId) {
13987
+ const id = teamId.trim();
13988
+ if (!id) return null;
13989
+ return readStore2().find(
13990
+ (ws) => ws.team_id === id || ws.team_name.toLowerCase() === id.toLowerCase()
13991
+ ) ?? null;
13992
+ }
13993
+ function slackTokenFor(ws, kind = "read") {
13994
+ if (kind === "search") {
13995
+ const token2 = ws.user_token?.trim();
13996
+ if (!token2) {
13997
+ throw new Error(
13998
+ `Slack search needs a user token for ${ws.team_name}. Reconnect via Account \u2192 Slack (browser) or paste an xoxp- token.`
13999
+ );
14000
+ }
14001
+ return token2;
14002
+ }
14003
+ const token = (kind === "write" ? ws.bot_token || ws.user_token : ws.user_token || ws.bot_token)?.trim();
14004
+ if (!token) {
14005
+ throw new Error(`Slack workspace ${ws.team_name} has no token`);
14006
+ }
14007
+ return token;
14008
+ }
14009
+ function requireSlackWorkspace(teamId) {
14010
+ const ws = getSlackWorkspace(teamId);
14011
+ if (!ws) {
14012
+ const connected = listSlackWorkspaces();
14013
+ const hint = connected.length === 0 ? "Connect a workspace in Account \u2192 Slack workspaces." : `Connected: ${connected.map((t) => `${t.team_name} (${t.team_id})`).join(", ")}`;
14014
+ throw new Error(`Unknown Slack team_id "${teamId}". ${hint}`);
14015
+ }
14016
+ return ws;
14017
+ }
14018
+
14019
+ // src/slack/outbound-watch.ts
14020
+ var MAX_WATCHES = 40;
14021
+ var WATCH_TTL_MS = 7 * 24 * 60 * 60 * 1e3;
14022
+ function storePath3() {
14023
+ return (0, import_node_path29.join)(appDataDir(), "slack-outbound-watch.json");
14024
+ }
14025
+ function watchId(teamId, channelId, ts) {
14026
+ return `${teamId}:${channelId}:${ts}`;
14027
+ }
14028
+ function slackArchiveUrl(channelId, ts) {
14029
+ return `https://slack.com/archives/${channelId}/p${ts.replace(".", "")}`;
14030
+ }
14031
+ function readStore3() {
14032
+ const path = storePath3();
14033
+ if (!(0, import_node_fs34.existsSync)(path)) return [];
14034
+ try {
14035
+ const parsed = isSecureFileEncrypted(path) ? readSecureJson(path) : JSON.parse((0, import_node_fs34.readFileSync)(path, "utf8"));
14036
+ return Array.isArray(parsed?.watches) ? parsed.watches : [];
14037
+ } catch {
14038
+ return [];
14039
+ }
14040
+ }
14041
+ function writeStore2(watches) {
14042
+ writePrivateFile(storePath3(), `${JSON.stringify({ watches }, null, 2)}
14043
+ `);
14044
+ return watches;
14045
+ }
14046
+ function pruneWatches(watches, nowMs = Date.now()) {
14047
+ const cutoff = nowMs - WATCH_TTL_MS;
14048
+ const kept = watches.filter((w) => {
14049
+ const posted = Date.parse(w.postedAt);
14050
+ return Number.isFinite(posted) ? posted >= cutoff : true;
14051
+ });
14052
+ if (kept.length <= MAX_WATCHES) return kept;
14053
+ return kept.slice().sort((a, b) => b.postedAt.localeCompare(a.postedAt)).slice(0, MAX_WATCHES);
14054
+ }
14055
+ function recordSlackOutboundWatch(input) {
14056
+ const ts = input.ts.trim();
14057
+ const channelId = input.channelId.trim();
14058
+ const teamId = input.teamId.trim();
14059
+ if (!ts || !channelId || !teamId) return null;
14060
+ const owner = input.ownerUserId?.trim();
14061
+ const toUser = input.toUserId?.trim();
14062
+ if (toUser && owner && toUser === owner) return null;
14063
+ const id = watchId(teamId, channelId, ts);
14064
+ const next = {
14065
+ id,
14066
+ teamId,
14067
+ channelId,
14068
+ ts,
14069
+ threadTs: input.threadTs?.trim() || ts,
14070
+ kind: input.kind === "dm" ? "dm" : "channel",
14071
+ toUserId: toUser,
14072
+ toLabel: input.toLabel.trim() || toUser || channelId,
14073
+ ownerUserId: owner,
14074
+ postedAt: (/* @__PURE__ */ new Date()).toISOString(),
14075
+ lastSeenTs: ts,
14076
+ unread: false,
14077
+ permalink: slackArchiveUrl(channelId, ts)
14078
+ };
14079
+ const watches = pruneWatches(readStore3().filter((w) => w.id !== id));
14080
+ watches.unshift(next);
14081
+ writeStore2(pruneWatches(watches));
14082
+ return next;
14083
+ }
14084
+
14085
+ // src/mcp/slack-tools.ts
14086
+ function text(payload, isError = false) {
14087
+ return {
14088
+ content: [{ type: "text", text: JSON.stringify(payload, null, 2) }],
14089
+ ...isError ? { isError: true } : {}
14090
+ };
14091
+ }
14092
+ function fail(err) {
14093
+ return text(
14094
+ { error: err instanceof Error ? err.message : String(err) },
14095
+ true
14096
+ );
14097
+ }
14098
+ function registerSlackTools(server) {
14099
+ server.tool(
14100
+ "list_teams",
14101
+ "List Slack workspaces connected in Sideboard Account settings. Each row is team_id + name. Pass team_id to slack_list_channels, slack_list_users, slack_search, slack_read, and slack_post.",
14102
+ {},
14103
+ async () => {
14104
+ const teams = listSlackWorkspaces();
14105
+ if (teams.length === 0) {
14106
+ return text({
14107
+ teams: [],
14108
+ hint: "No Slack workspaces connected. Add one in Account \u2192 Slack workspaces (paste xoxb-/xoxp- or browser sign-in)."
14109
+ });
14110
+ }
14111
+ return text({
14112
+ teams: teams.map((t) => ({
14113
+ team_id: t.team_id,
14114
+ name: t.team_name,
14115
+ search: t.has_user_token
14116
+ }))
14117
+ });
14118
+ }
14119
+ );
14120
+ server.tool(
14121
+ "slack_list_channels",
14122
+ "List channels in a connected Slack workspace. Pass team_id from list_teams.",
14123
+ {
14124
+ team_id: import_zod.z.string(),
14125
+ limit: import_zod.z.number().optional()
14126
+ },
14127
+ async ({ team_id, limit }) => {
14128
+ try {
14129
+ const ws = requireSlackWorkspace(team_id);
14130
+ const token = slackTokenFor(ws, "read");
14131
+ const data = await slackApi(token, "conversations.list", {
14132
+ types: "public_channel,private_channel",
14133
+ exclude_archived: true,
14134
+ limit: Math.min(200, Math.max(1, limit ?? 100))
14135
+ });
14136
+ return text({
14137
+ team_id: ws.team_id,
14138
+ team_name: ws.team_name,
14139
+ channels: (data.channels ?? []).map((c) => ({
14140
+ id: c.id,
14141
+ name: c.name,
14142
+ private: Boolean(c.is_private),
14143
+ member: Boolean(c.is_member)
14144
+ }))
14145
+ });
14146
+ } catch (err) {
14147
+ return fail(err);
14148
+ }
14149
+ }
14150
+ );
14151
+ server.tool(
14152
+ "slack_list_users",
14153
+ "List or search people in a connected Slack workspace (for DMs / @mentions). Pass team_id from list_teams. Optional query matches name, display name, real name, or email.",
14154
+ {
14155
+ team_id: import_zod.z.string(),
14156
+ query: import_zod.z.string().optional(),
14157
+ limit: import_zod.z.number().optional()
14158
+ },
14159
+ async ({ team_id, query, limit }) => {
14160
+ try {
14161
+ const ws = requireSlackWorkspace(team_id);
14162
+ const token = slackTokenFor(ws, "read");
14163
+ const users = await listSlackUsers(token, { query, limit });
14164
+ return text({
14165
+ team_id: ws.team_id,
14166
+ team_name: ws.team_name,
14167
+ query: query?.trim() || void 0,
14168
+ users
14169
+ });
14170
+ } catch (err) {
14171
+ return fail(err);
14172
+ }
14173
+ }
14174
+ );
14175
+ server.tool(
14176
+ "slack_search",
14177
+ "Search messages in a connected Slack workspace (needs a user token). Pass team_id from list_teams.",
14178
+ {
14179
+ team_id: import_zod.z.string(),
14180
+ query: import_zod.z.string(),
14181
+ count: import_zod.z.number().optional()
14182
+ },
14183
+ async ({ team_id, query, count }) => {
14184
+ try {
14185
+ const ws = requireSlackWorkspace(team_id);
14186
+ const token = slackTokenFor(ws, "search");
14187
+ const data = await slackApi(token, "search.messages", {
14188
+ query,
14189
+ count: Math.min(50, Math.max(1, count ?? 10)),
14190
+ sort: "timestamp"
14191
+ });
14192
+ return text({
14193
+ team_id: ws.team_id,
14194
+ team_name: ws.team_name,
14195
+ query,
14196
+ matches: (data.messages?.matches ?? []).map((m) => ({
14197
+ channel: m.channel?.name || m.channel?.id,
14198
+ user: m.username || m.user,
14199
+ ts: m.ts,
14200
+ text: m.text,
14201
+ permalink: m.permalink
14202
+ }))
14203
+ });
14204
+ } catch (err) {
14205
+ return fail(err);
14206
+ }
14207
+ }
14208
+ );
14209
+ server.tool(
14210
+ "slack_read",
14211
+ "Read recent messages in a Slack channel or DM, or a thread when ts is set. Pass team_id from list_teams and #channel, @user, or a C\u2026/D\u2026/U\u2026 id.",
14212
+ {
14213
+ team_id: import_zod.z.string(),
14214
+ channel: import_zod.z.string(),
14215
+ ts: import_zod.z.string().optional(),
14216
+ limit: import_zod.z.number().optional()
14217
+ },
14218
+ async ({ team_id, channel, ts, limit }) => {
14219
+ try {
14220
+ const ws = requireSlackWorkspace(team_id);
14221
+ const token = slackTokenFor(ws, "read");
14222
+ const dest = await resolveSlackDestination(token, channel, { forRead: true });
14223
+ const n = Math.min(100, Math.max(1, limit ?? 20));
14224
+ if (ts?.trim()) {
14225
+ const data2 = await slackApi(token, "conversations.replies", {
14226
+ channel: dest.channelId,
14227
+ ts: ts.trim(),
14228
+ limit: n
14229
+ });
14230
+ return text({
14231
+ team_id: ws.team_id,
14232
+ channel: dest.channelId,
14233
+ label: dest.label,
14234
+ thread_ts: ts.trim(),
14235
+ messages: data2.messages ?? []
14236
+ });
14237
+ }
14238
+ const data = await slackApi(token, "conversations.history", {
14239
+ channel: dest.channelId,
14240
+ limit: n
14241
+ });
14242
+ return text({
14243
+ team_id: ws.team_id,
14244
+ channel: dest.channelId,
14245
+ label: dest.label,
14246
+ messages: data.messages ?? []
14247
+ });
14248
+ } catch (err) {
14249
+ return fail(err);
14250
+ }
14251
+ }
14252
+ );
14253
+ server.tool(
14254
+ "slack_post",
14255
+ "Post a message to a Slack channel or DM (as the Sideboard bot). Pass team_id from list_teams. Use to or channel for #name, @user, or C\u2026/D\u2026/U\u2026 ids. Optional github_url appends a PR / code / comment link. Only notify when the user asks. Thread with thread_ts when set.",
14256
+ {
14257
+ team_id: import_zod.z.string(),
14258
+ channel: import_zod.z.string().optional(),
14259
+ to: import_zod.z.string().optional(),
14260
+ text: import_zod.z.string(),
14261
+ github_url: import_zod.z.string().optional(),
14262
+ thread_ts: import_zod.z.string().optional()
14263
+ },
14264
+ async ({ team_id, channel, to, text: message, github_url, thread_ts }) => {
14265
+ try {
14266
+ const destRaw = (to ?? channel)?.trim();
14267
+ if (!destRaw) {
14268
+ return fail(new Error("slack_post needs `to` or `channel` (#name, @user, or Slack id)"));
14269
+ }
14270
+ const ws = requireSlackWorkspace(team_id);
14271
+ const token = slackTokenFor(ws, "write");
14272
+ const dest = await resolveSlackDestination(token, destRaw);
14273
+ const body = appendGithubLink(message, github_url);
14274
+ const data = await slackApi(
14275
+ token,
14276
+ "chat.postMessage",
14277
+ {
14278
+ channel: dest.channelId,
14279
+ text: body,
14280
+ thread_ts: thread_ts?.trim() || void 0,
14281
+ unfurl_links: true,
14282
+ unfurl_media: true
14283
+ }
14284
+ );
14285
+ const postedTs = data.ts?.trim();
14286
+ const postedChannel = (data.channel || dest.channelId).trim();
14287
+ if (postedTs && postedChannel) {
14288
+ try {
14289
+ recordSlackOutboundWatch({
14290
+ teamId: ws.team_id,
14291
+ channelId: postedChannel,
14292
+ ts: postedTs,
14293
+ threadTs: thread_ts?.trim() || postedTs,
14294
+ kind: dest.kind === "channel" ? "channel" : "dm",
14295
+ toUserId: dest.userId,
14296
+ toLabel: dest.label,
14297
+ ownerUserId: ws.user_id
14298
+ });
14299
+ } catch {
14300
+ }
14301
+ }
14302
+ return text({
14303
+ ok: true,
14304
+ team_id: ws.team_id,
14305
+ channel: postedChannel,
14306
+ label: dest.label,
14307
+ kind: dest.kind,
14308
+ user_id: dest.userId,
14309
+ ts: postedTs
14310
+ });
14311
+ } catch (err) {
14312
+ return fail(err);
14313
+ }
14314
+ }
14315
+ );
14316
+ }
14317
+
14318
+ // src/mcp/server.ts
12636
14319
  var MAX_ORCH_THREADS = 5;
12637
14320
  var CREATE_THREAD_TIMEOUT_MS = 9e4;
12638
14321
  function withTimeout(promise, ms, label) {
@@ -12697,7 +14380,7 @@ async function startMcpServer() {
12697
14380
  async () => {
12698
14381
  const threads = orch.getThreads(true);
12699
14382
  const lines = threads.map((t) => {
12700
- const repo = t.repoPath === GLOBAL_WORKSPACE_ID ? "Orchestration" : (0, import_node_path26.basename)(t.repoPath) || t.repoPath;
14383
+ const repo = t.repoPath === GLOBAL_WORKSPACE_ID ? "Orchestration" : (0, import_node_path31.basename)(t.repoPath) || t.repoPath;
12701
14384
  return `${t.id.slice(0, 8)} ${t.status.padEnd(9)} ${t.agent.padEnd(8)} ${repo} ${t.sourceType}:${t.sourceRef} ${t.title} sideboard://thread/${t.id}${t.devPort ? ` http://localhost:${t.devPort}` : ""}`;
12702
14385
  });
12703
14386
  return {
@@ -12708,7 +14391,7 @@ async function startMcpServer() {
12708
14391
  server.tool(
12709
14392
  "get_thread",
12710
14393
  "Get a compact thread summary by id/ref",
12711
- { ref: import_zod.z.string() },
14394
+ { ref: import_zod2.z.string() },
12712
14395
  async ({ ref }) => {
12713
14396
  const t = orch.getThread(ref);
12714
14397
  if (!t) {
@@ -12737,14 +14420,14 @@ async function startMcpServer() {
12737
14420
  "present_artifact",
12738
14421
  "Show an HTML, SVG, markdown, or React document in Sideboard\u2019s Claude-style side column (desktop). Use instead of claude.ai\u2019s artifact tool \u2014 pass the full document content. Prefer type=html for interactive pages. For type=react, pass a single component module that `export default`s a component (JSX/TSX ok) \u2014 Sideboard bootstraps React/ReactDOM/Babel and renders it; only `react`/`react-dom` imports are available, no other npm packages.",
12739
14422
  {
12740
- title: import_zod.z.string().describe("Short title shown in the artifact pane header"),
12741
- type: import_zod.z.enum(["html", "svg", "markdown", "react"]).describe(
14423
+ title: import_zod2.z.string().describe("Short title shown in the artifact pane header"),
14424
+ type: import_zod2.z.enum(["html", "svg", "markdown", "react"]).describe(
12742
14425
  "Artifact kind \u2014 html opens an iframe preview; react transpiles and renders a default-exported component in a sandboxed iframe"
12743
14426
  ),
12744
- content: import_zod.z.string().describe(
14427
+ content: import_zod2.z.string().describe(
12745
14428
  "Full document body (complete HTML page, SVG markup, markdown, or a React component module with a default export)"
12746
14429
  ),
12747
- artifact_id: import_zod.z.string().optional().describe("Stable id when updating the same artifact across turns")
14430
+ artifact_id: import_zod2.z.string().optional().describe("Stable id when updating the same artifact across turns")
12748
14431
  },
12749
14432
  async ({ title, type, artifact_id }) => {
12750
14433
  const id = artifact_id?.trim() || `artifact_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 8)}`;
@@ -12762,15 +14445,15 @@ async function startMcpServer() {
12762
14445
  "ask_user",
12763
14446
  "Ask the user clarifying multiple-choice questions in Sideboard\u2019s composer (plan mode). Before calling, write a short chat message explaining the decision and what each option means. Include a description on every option. Use for approach forks and requirements \u2014 not for \u201Cis the plan ready?\u201D. After calling, stop and wait for their next message with answers.",
12764
14447
  {
12765
- questions: import_zod.z.array(
12766
- import_zod.z.object({
12767
- question: import_zod.z.string().describe("Full question text ending with ?"),
12768
- header: import_zod.z.string().max(24).optional().describe("Short label shown above the question"),
12769
- multiSelect: import_zod.z.boolean().optional().describe("Allow selecting multiple options"),
12770
- options: import_zod.z.array(
12771
- import_zod.z.object({
12772
- label: import_zod.z.string(),
12773
- description: import_zod.z.string().optional().describe("What this option means / when to choose it (strongly preferred)")
14448
+ questions: import_zod2.z.array(
14449
+ import_zod2.z.object({
14450
+ question: import_zod2.z.string().describe("Full question text ending with ?"),
14451
+ header: import_zod2.z.string().max(24).optional().describe("Short label shown above the question"),
14452
+ multiSelect: import_zod2.z.boolean().optional().describe("Allow selecting multiple options"),
14453
+ options: import_zod2.z.array(
14454
+ import_zod2.z.object({
14455
+ label: import_zod2.z.string(),
14456
+ description: import_zod2.z.string().optional().describe("What this option means / when to choose it (strongly preferred)")
12774
14457
  })
12775
14458
  ).min(2).max(6).describe("2\u20136 choices (Sideboard also offers Other)")
12776
14459
  })
@@ -12789,9 +14472,9 @@ async function startMcpServer() {
12789
14472
  "present_plan",
12790
14473
  "Save the implementation plan as markdown to .context/attachments/plan.md and show it in Sideboard chat for user approval (Copy / Hand off / Approve). Call this when the plan is ready \u2014 required in plan mode. Pass the full plan body in content. Then Claude should call ExitPlanMode.",
12791
14474
  {
12792
- title: import_zod.z.string().optional().describe("Short plan title (defaults to Plan)"),
12793
- content: import_zod.z.string().min(1).describe("Full plan markdown (headings, steps, risks, open questions)"),
12794
- thread_id: import_zod.z.string().optional().describe("Sideboard thread id when cwd is not the worktree")
14475
+ title: import_zod2.z.string().optional().describe("Short plan title (defaults to Plan)"),
14476
+ content: import_zod2.z.string().min(1).describe("Full plan markdown (headings, steps, risks, open questions)"),
14477
+ thread_id: import_zod2.z.string().optional().describe("Sideboard thread id when cwd is not the worktree")
12795
14478
  },
12796
14479
  async ({ title, content, thread_id }) => {
12797
14480
  const { writePlanFile: writePlanFile2 } = await Promise.resolve().then(() => (init_plan_file(), plan_file_exports));
@@ -12812,17 +14495,17 @@ async function startMcpServer() {
12812
14495
  );
12813
14496
  server.tool(
12814
14497
  "present_schema",
12815
- "Open Sideboard\u2019s schema-driven CMS side column (filterable table and/or form). Pass JSON Schema + schemaUi (Brightsy extensions supported). Use datasource=brightsy with resource_id (record type UUID) when logged into Brightsy; use datasource=inline with embedded resource/records for any other source.",
14498
+ "Open Sideboard\u2019s schema-driven side column (filterable table and/or form). Pass JSON Schema + optional schemaUi. Prefer datasource=inline with embedded resource/records. Use datasource=brightsy with resource_id only when the user is logged into Brightsy.",
12816
14499
  {
12817
- title: import_zod.z.string().describe("Pane title"),
12818
- mode: import_zod.z.enum(["table", "form"]).optional().describe("table = list/filter records; form = edit one record"),
12819
- datasource: import_zod.z.enum(["brightsy", "inline"]).optional().describe("brightsy resolves via login; inline uses embedded resource/records"),
12820
- resource_id: import_zod.z.string().optional().describe("Brightsy record type UUID (or generic resource id)"),
12821
- record_id: import_zod.z.string().optional().describe("Record id when opening form mode"),
12822
- resource: import_zod.z.record(import_zod.z.unknown()).optional().describe("Inline { id, title, schema, schemaUi?, slug? }"),
12823
- record: import_zod.z.record(import_zod.z.unknown()).optional().describe("Inline record { id, data, published_at? }"),
12824
- records: import_zod.z.array(import_zod.z.record(import_zod.z.unknown())).optional().describe("Inline records for table mode"),
12825
- pane_id: import_zod.z.string().optional().describe("Stable pane id across updates")
14500
+ title: import_zod2.z.string().describe("Pane title"),
14501
+ mode: import_zod2.z.enum(["table", "form"]).optional().describe("table = list/filter records; form = edit one record"),
14502
+ datasource: import_zod2.z.enum(["brightsy", "inline"]).optional().describe("brightsy resolves via login; inline uses embedded resource/records"),
14503
+ resource_id: import_zod2.z.string().optional().describe("Brightsy record type UUID (or generic resource id)"),
14504
+ record_id: import_zod2.z.string().optional().describe("Record id when opening form mode"),
14505
+ resource: import_zod2.z.record(import_zod2.z.unknown()).optional().describe("Inline { id, title, schema, schemaUi?, slug? }"),
14506
+ record: import_zod2.z.record(import_zod2.z.unknown()).optional().describe("Inline record { id, data, published_at? }"),
14507
+ records: import_zod2.z.array(import_zod2.z.record(import_zod2.z.unknown())).optional().describe("Inline records for table mode"),
14508
+ pane_id: import_zod2.z.string().optional().describe("Stable pane id across updates")
12826
14509
  },
12827
14510
  async (args) => {
12828
14511
  const id = args.pane_id?.trim() || `schema_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 8)}`;
@@ -12843,10 +14526,10 @@ async function startMcpServer() {
12843
14526
  "present_files",
12844
14527
  "Open Sideboard\u2019s Files column (CMS-style file manager: browse, upload, pick). Use datasource=brightsy when the user is logged into Brightsy account storage; datasource=memory for a session-local demo store. Prefer this over claiming a file manager UI is unavailable. Pair with present_schema when editing records that need media.",
12845
14528
  {
12846
- title: import_zod.z.string().optional().describe("Pane title (default: Files)"),
12847
- datasource: import_zod.z.enum(["brightsy", "memory"]).optional().describe("brightsy = account storage via login; memory = session demo store"),
12848
- path: import_zod.z.string().optional().describe("Initial folder path (e.g. public)"),
12849
- pane_id: import_zod.z.string().optional().describe("Stable pane id across updates")
14529
+ title: import_zod2.z.string().optional().describe("Pane title (default: Files)"),
14530
+ datasource: import_zod2.z.enum(["brightsy", "memory"]).optional().describe("brightsy = account storage via login; memory = session demo store"),
14531
+ path: import_zod2.z.string().optional().describe("Initial folder path (e.g. public)"),
14532
+ pane_id: import_zod2.z.string().optional().describe("Stable pane id across updates")
12850
14533
  },
12851
14534
  async (args) => {
12852
14535
  const id = args.pane_id?.trim() || `files_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 8)}`;
@@ -12861,23 +14544,62 @@ async function startMcpServer() {
12861
14544
  return { content: [{ type: "text", text: JSON.stringify(payload) }] };
12862
14545
  }
12863
14546
  );
14547
+ registerSlackTools(server);
12864
14548
  if (sideboardMcpProfile() !== "worktree") {
14549
+ const { getCaffeinateHold: getCaffeinateHold2, setCaffeinateHold: setCaffeinateHold2 } = await Promise.resolve().then(() => (init_caffeinate_hold(), caffeinate_hold_exports));
12865
14550
  const { resolveNewThreadOptions: resolveNewThreadOptions2, resolveThreadDefaults: resolveThreadDefaults2 } = await Promise.resolve().then(() => (init_app_settings(), app_settings_exports));
12866
14551
  const accountDefaults = resolveThreadDefaults2();
12867
14552
  const accountDefaultsHint = `Account defaults: agent=${accountDefaults.agent}, model=${accountDefaults.model?.trim() || "Auto"}, effort=${accountDefaults.effort}`;
14553
+ server.tool(
14554
+ "set_caffeinate",
14555
+ "Keep this Mac awake with caffeinate (like Claude Code) across turns \u2014 independent of Settings toggles. Turn ON when the user will be away from the keyboard, is driving work from Slack, or asks you to keep the machine awake. Turn OFF when they say they are done, wrapping up, going to sleep, or no longer need the Mac awake. macOS only.",
14556
+ {
14557
+ enabled: import_zod2.z.boolean().describe("true = hold caffeinate on; false = release and let the Mac sleep")
14558
+ },
14559
+ async ({ enabled }) => {
14560
+ const state = setCaffeinateHold2(enabled);
14561
+ if (enabled && !state.held) {
14562
+ return {
14563
+ content: [
14564
+ {
14565
+ type: "text",
14566
+ text: JSON.stringify({
14567
+ ...state,
14568
+ ok: false,
14569
+ message: state.platform === "darwin" ? "Could not start caffeinate." : "Caffeinate is macOS only."
14570
+ })
14571
+ }
14572
+ ],
14573
+ isError: true
14574
+ };
14575
+ }
14576
+ return {
14577
+ content: [
14578
+ {
14579
+ type: "text",
14580
+ text: JSON.stringify({
14581
+ ...getCaffeinateHold2(),
14582
+ ok: true,
14583
+ message: state.held ? "Mac will stay awake until you call set_caffeinate with enabled=false (or the user says they are done)." : "Caffeinate hold released. The Mac can sleep (unless Settings caffeinate toggles are on)."
14584
+ })
14585
+ }
14586
+ ]
14587
+ };
14588
+ }
14589
+ );
12868
14590
  server.tool(
12869
14591
  "create_thread",
12870
14592
  `Create a new worktree thread (chat) from branch, pr, or ticket. Pass repoPath from list_workspaces. From an orchestration chat, omit parentThreadId (Sideboard binds the child to this chat) or pass the exact id from the turn reminder \u2014 never invent a uuid. Prefer omitting agent/model so Sideboard applies ${accountDefaultsHint}. Then use send_to_thread to chat.`,
12871
14593
  {
12872
- sourceType: import_zod.z.enum(["branch", "pr", "ticket"]),
12873
- sourceRef: import_zod.z.string(),
12874
- agent: import_zod.z.enum(["claude", "codex", "opencode", "brightsy", "cursor"]).optional().describe(`Omit to use Account default agent (${accountDefaults.agent})`),
12875
- model: import_zod.z.string().nullable().optional().describe(
14594
+ sourceType: import_zod2.z.enum(["branch", "pr", "ticket"]),
14595
+ sourceRef: import_zod2.z.string(),
14596
+ agent: import_zod2.z.enum(["claude", "codex", "opencode", "brightsy", "cursor"]).optional().describe(`Omit to use Account default agent (${accountDefaults.agent})`),
14597
+ model: import_zod2.z.string().nullable().optional().describe(
12876
14598
  `Usually omit to use Account default model (${accountDefaults.model?.trim() || "Auto"}). Pass null only to force Auto / agent-default.`
12877
14599
  ),
12878
- repoPath: import_zod.z.string(),
12879
- title: import_zod.z.string().optional(),
12880
- parentThreadId: import_zod.z.string().optional().describe(
14600
+ repoPath: import_zod2.z.string(),
14601
+ title: import_zod2.z.string().optional(),
14602
+ parentThreadId: import_zod2.z.string().optional().describe(
12881
14603
  "Orchestration: omit (preferred) or pass YOUR chat id from the turn reminder / AGENTS.md. Do not invent uuids."
12882
14604
  )
12883
14605
  },
@@ -12984,9 +14706,9 @@ async function startMcpServer() {
12984
14706
  "send_to_thread",
12985
14707
  "Queue a prompt on a worktree thread chat (runs under concurrency cap). Use after create_thread to start or continue a conversation. Set force_stop=true to interrupt an in-flight/queued turn (kill + clear queue) before queueing this prompt \u2014 use when the thread is mid-turn or has stale queued prompts you need to replace.",
12986
14708
  {
12987
- ref: import_zod.z.string(),
12988
- prompt: import_zod.z.string(),
12989
- force_stop: import_zod.z.boolean().optional()
14709
+ ref: import_zod2.z.string(),
14710
+ prompt: import_zod2.z.string(),
14711
+ force_stop: import_zod2.z.boolean().optional()
12990
14712
  },
12991
14713
  async ({ ref, prompt, force_stop }) => {
12992
14714
  if (force_stop) {
@@ -13015,8 +14737,8 @@ async function startMcpServer() {
13015
14737
  "wait_for_turn",
13016
14738
  "Block until the thread finishes its current/queued turn (avoids polling). Use after send_to_thread to read the agent reply.",
13017
14739
  {
13018
- ref: import_zod.z.string(),
13019
- timeoutMs: import_zod.z.number().optional()
14740
+ ref: import_zod2.z.string(),
14741
+ timeoutMs: import_zod2.z.number().optional()
13020
14742
  },
13021
14743
  async ({ ref, timeoutMs }) => {
13022
14744
  const thread = await orch.waitForTurn(ref, timeoutMs ?? 6e5);
@@ -13038,7 +14760,7 @@ async function startMcpServer() {
13038
14760
  server.tool(
13039
14761
  "get_turn_result",
13040
14762
  "Final assistant message only (not full transcript)",
13041
- { ref: import_zod.z.string() },
14763
+ { ref: import_zod2.z.string() },
13042
14764
  async ({ ref }) => {
13043
14765
  const result = orch.getTurnResult(ref);
13044
14766
  return { content: [{ type: "text", text: JSON.stringify(result) }] };
@@ -13048,8 +14770,8 @@ async function startMcpServer() {
13048
14770
  "stop_thread",
13049
14771
  "Force-stop a thread: kill any in-flight agent turn AND clear queued prompts so drainQueue cannot continue. Does not archive the worktree. Optional force defaults to true.",
13050
14772
  {
13051
- ref: import_zod.z.string(),
13052
- force: import_zod.z.boolean().optional()
14773
+ ref: import_zod2.z.string(),
14774
+ force: import_zod2.z.boolean().optional()
13053
14775
  },
13054
14776
  async ({ ref, force }) => {
13055
14777
  const t = orch.getThread(ref);
@@ -13078,8 +14800,8 @@ async function startMcpServer() {
13078
14800
  );
13079
14801
  server.tool(
13080
14802
  "archive_thread",
13081
- "Archive a thread (stops agent/dev, runs archive script, removes worktree when last chat tab). Cannot archive the cloud coordinator \u2014 use the Sideboard UI for that.",
13082
- { ref: import_zod.z.string() },
14803
+ "Archive a thread (stops agent/dev, runs archive script, removes worktree when last chat tab). Coordinators open PRs only by asking the worktree agent.",
14804
+ { ref: import_zod2.z.string() },
13083
14805
  async ({ ref }) => {
13084
14806
  const t = orch.getThread(ref);
13085
14807
  if (!t) {
@@ -13112,7 +14834,7 @@ async function startMcpServer() {
13112
14834
  server.tool(
13113
14835
  "restore_thread",
13114
14836
  "Restore an archived thread (recreates worktree from branch when needed)",
13115
- { ref: import_zod.z.string() },
14837
+ { ref: import_zod2.z.string() },
13116
14838
  async ({ ref }) => {
13117
14839
  try {
13118
14840
  const restored = await orch.restore(ref);
@@ -13138,8 +14860,8 @@ async function startMcpServer() {
13138
14860
  "get_diff",
13139
14861
  "Compact diff summary (capped hunks, paginated)",
13140
14862
  {
13141
- ref: import_zod.z.string(),
13142
- maxFiles: import_zod.z.number().optional()
14863
+ ref: import_zod2.z.string(),
14864
+ maxFiles: import_zod2.z.number().optional()
13143
14865
  },
13144
14866
  async ({ ref, maxFiles }) => {
13145
14867
  const summary = await orch.diffSummary(ref);
@@ -13159,7 +14881,7 @@ async function startMcpServer() {
13159
14881
  server.tool(
13160
14882
  "request_review",
13161
14883
  'Start a merge-readiness Review on a worktree agent thread (same as the desktop Review button). Opens a new Review chat tab, attaches .sideboard/review.md when present (else local Review request.md / stock template), and sends "Review changes in this workspace." Expect Approve / Approve with nits / Request changes / Needs more information. Pass a worktree thread ref \u2014 not the orchestrator. Then wait_for_turn / get_turn_result on the returned review tab id.',
13162
- { ref: import_zod.z.string().describe("Worktree thread id/ref to review") },
14884
+ { ref: import_zod2.z.string().describe("Worktree thread id/ref to review") },
13163
14885
  async ({ ref }) => {
13164
14886
  try {
13165
14887
  const tab = await orch.requestReview(ref);
@@ -13184,7 +14906,7 @@ async function startMcpServer() {
13184
14906
  }
13185
14907
  }
13186
14908
  );
13187
- const agentEnum = import_zod.z.enum(["claude", "codex", "opencode", "brightsy", "cursor"]);
14909
+ const agentEnum = import_zod2.z.enum(["claude", "codex", "opencode", "brightsy", "cursor"]);
13188
14910
  server.tool(
13189
14911
  "list_models",
13190
14912
  "List models for an agent. Prefer Auto: do not call this unless you have a reason to pin a specific model (user request, cost/latency, capability). Omit agent to list all.",
@@ -13207,11 +14929,11 @@ async function startMcpServer() {
13207
14929
  "fork_worktree",
13208
14930
  "Fork a worktree agent chat into a NEW git worktree + chat (desktop \u201CFork to new workspace\u201D). Seeds a transcript (through through_index, default all). Optional agent override. Leave model unset for Auto (default) \u2014 only pass model when you have a reason. Not for the orchestrator. Then send_to_thread / wait_for_turn on the returned id.",
13209
14931
  {
13210
- ref: import_zod.z.string().describe("Worktree thread id/ref to fork"),
13211
- through_index: import_zod.z.number().optional().describe("Inclusive message index to include in the transcript (default: all)"),
14932
+ ref: import_zod2.z.string().describe("Worktree thread id/ref to fork"),
14933
+ through_index: import_zod2.z.number().optional().describe("Inclusive message index to include in the transcript (default: all)"),
13212
14934
  agent: agentEnum.optional().describe("Agent for the forked chat (default: same as source)"),
13213
- model: import_zod.z.string().nullable().optional().describe("Usually omit (Auto). Only set from list_models when you need a specific model"),
13214
- title: import_zod.z.string().optional()
14935
+ model: import_zod2.z.string().nullable().optional().describe("Usually omit (Auto). Only set from list_models when you need a specific model"),
14936
+ title: import_zod2.z.string().optional()
13215
14937
  },
13216
14938
  async ({ ref, through_index, agent, model, title }) => {
13217
14939
  try {
@@ -13250,13 +14972,13 @@ async function startMcpServer() {
13250
14972
  );
13251
14973
  server.tool(
13252
14974
  "fork_chat",
13253
- "Fork a chat into a NEW tab on the SAME workspace: worktree agent \u2192 same worktree tab; Global orchestration chat \u2192 new orchestration chat (same synthetic home). Seeds a transcript; optional agent override. Leave model unset for Auto unless you have a reason. Orchestration forks require an MCP-capable agent (claude, cursor, codex, opencode \u2014 not brightsy). Remote coordinators use this to continue an orchestration chat on another agent after session limits. Then send_to_thread / wait_for_turn on the returned id. Use fork_worktree only for worktree agents that need a new git worktree.",
14975
+ "Fork a chat into a NEW tab on the SAME workspace: worktree agent \u2192 same worktree tab; Global orchestration chat \u2192 new orchestration chat (same synthetic home). Seeds a transcript; optional agent override. Leave model unset for Auto unless you have a reason. Orchestration forks require an MCP-capable agent (claude, cursor, codex, opencode \u2014 not brightsy). Slack / Global orchestrators use this to continue an orchestration chat on another agent after session limits. Then send_to_thread / wait_for_turn on the returned id. Use fork_worktree only for worktree agents that need a new git worktree.",
13254
14976
  {
13255
- ref: import_zod.z.string().describe("Thread id/ref to fork (worktree agent or orchestration chat)"),
13256
- through_index: import_zod.z.number().optional().describe("Inclusive message index to include in the transcript (default: all)"),
14977
+ ref: import_zod2.z.string().describe("Thread id/ref to fork (worktree agent or orchestration chat)"),
14978
+ through_index: import_zod2.z.number().optional().describe("Inclusive message index to include in the transcript (default: all)"),
13257
14979
  agent: agentEnum.optional().describe("Agent for the forked chat (default: same as source)"),
13258
- model: import_zod.z.string().nullable().optional().describe("Usually omit (Auto). Only set from list_models when you need a specific model"),
13259
- title: import_zod.z.string().optional()
14980
+ model: import_zod2.z.string().nullable().optional().describe("Usually omit (Auto). Only set from list_models when you need a specific model"),
14981
+ title: import_zod2.z.string().optional()
13260
14982
  },
13261
14983
  async ({ ref, through_index, agent, model, title }) => {
13262
14984
  try {
@@ -13302,8 +15024,8 @@ async function startMcpServer() {
13302
15024
  "run_dev_script",
13303
15025
  "Start a .sideboard/.conductor run script for a thread (default script if name omitted); returns port",
13304
15026
  {
13305
- ref: import_zod.z.string(),
13306
- name: import_zod.z.string().optional()
15027
+ ref: import_zod2.z.string(),
15028
+ name: import_zod2.z.string().optional()
13307
15029
  },
13308
15030
  async ({ ref, name }) => {
13309
15031
  const result = await orch.startDev(ref, name);
@@ -13325,7 +15047,7 @@ async function startMcpServer() {
13325
15047
  server.tool(
13326
15048
  "list_run_scripts",
13327
15049
  "List named run scripts available for a thread",
13328
- { ref: import_zod.z.string() },
15050
+ { ref: import_zod2.z.string() },
13329
15051
  async ({ ref }) => {
13330
15052
  const scripts = orch.listThreadRunScripts(ref);
13331
15053
  const active = orch.getActiveRuns(ref);
@@ -13343,8 +15065,8 @@ async function startMcpServer() {
13343
15065
  "stop_dev_script",
13344
15066
  "Stop a running script for a thread (all scripts if name omitted)",
13345
15067
  {
13346
- ref: import_zod.z.string(),
13347
- name: import_zod.z.string().optional()
15068
+ ref: import_zod2.z.string(),
15069
+ name: import_zod2.z.string().optional()
13348
15070
  },
13349
15071
  async ({ ref, name }) => {
13350
15072
  orch.stopDev(ref, name);
@@ -13354,7 +15076,7 @@ async function startMcpServer() {
13354
15076
  server.tool(
13355
15077
  "run_setup",
13356
15078
  "Re-run workspace setup (Sideboard/Conductor settings or .cursor/worktrees.json)",
13357
- { ref: import_zod.z.string() },
15079
+ { ref: import_zod2.z.string() },
13358
15080
  async ({ ref }) => {
13359
15081
  const result = await orch.runSetup(ref);
13360
15082
  return {
@@ -13365,7 +15087,7 @@ async function startMcpServer() {
13365
15087
  server.tool(
13366
15088
  "add_workspace",
13367
15089
  "Register a git repo as a Sideboard workspace",
13368
- { repoPath: import_zod.z.string() },
15090
+ { repoPath: import_zod2.z.string() },
13369
15091
  async ({ repoPath }) => {
13370
15092
  const ws = await orch.addWorkspace(repoPath);
13371
15093
  return { content: [{ type: "text", text: JSON.stringify(ws) }] };
@@ -13374,7 +15096,7 @@ async function startMcpServer() {
13374
15096
  server.tool(
13375
15097
  "remove_workspace",
13376
15098
  "Unregister a Sideboard workspace (does not archive threads)",
13377
- { repoPath: import_zod.z.string() },
15099
+ { repoPath: import_zod2.z.string() },
13378
15100
  async ({ repoPath }) => {
13379
15101
  orch.removeWorkspace(repoPath);
13380
15102
  return { content: [{ type: "text", text: "ok" }] };
@@ -13384,12 +15106,12 @@ async function startMcpServer() {
13384
15106
  "fanout",
13385
15107
  "Best-of-n: create one thread per agent with the same prompt (parallel attempts)",
13386
15108
  {
13387
- prompt: import_zod.z.string(),
13388
- agents: import_zod.z.array(import_zod.z.enum(["claude", "codex", "opencode", "brightsy", "cursor"])),
13389
- repoPath: import_zod.z.string(),
13390
- sourceType: import_zod.z.enum(["branch", "pr", "ticket"]).optional(),
13391
- sourceRef: import_zod.z.string().optional(),
13392
- title: import_zod.z.string().optional()
15109
+ prompt: import_zod2.z.string(),
15110
+ agents: import_zod2.z.array(import_zod2.z.enum(["claude", "codex", "opencode", "brightsy", "cursor"])),
15111
+ repoPath: import_zod2.z.string(),
15112
+ sourceType: import_zod2.z.enum(["branch", "pr", "ticket"]).optional(),
15113
+ sourceRef: import_zod2.z.string().optional(),
15114
+ title: import_zod2.z.string().optional()
13393
15115
  },
13394
15116
  async (args) => {
13395
15117
  const threads = await orch.bestOfN(args);
@@ -13416,8 +15138,8 @@ async function startMcpServer() {
13416
15138
  "list_branches",
13417
15139
  "List git branches in a registered workspace. Pass repoPath from list_workspaces (unmerged into the default branch by default \u2014 for create_thread sourceType=branch).",
13418
15140
  {
13419
- repoPath: import_zod.z.string(),
13420
- unmergedOnly: import_zod.z.boolean().optional()
15141
+ repoPath: import_zod2.z.string(),
15142
+ unmergedOnly: import_zod2.z.boolean().optional()
13421
15143
  },
13422
15144
  async ({ repoPath, unmergedOnly }) => {
13423
15145
  const root = await resolveRepoRoot(repoPath);
@@ -13437,7 +15159,7 @@ async function startMcpServer() {
13437
15159
  server.tool(
13438
15160
  "list_prs",
13439
15161
  "List open GitHub PRs for a registered workspace. Pass repoPath from list_workspaces (uses gh against that repo remote). Then create_thread with sourceType=pr.",
13440
- { repoPath: import_zod.z.string() },
15162
+ { repoPath: import_zod2.z.string() },
13441
15163
  async ({ repoPath }) => {
13442
15164
  const root = await resolveRepoRoot(repoPath);
13443
15165
  const prs = await listPrs(root);
@@ -13456,7 +15178,7 @@ async function startMcpServer() {
13456
15178
  server.tool(
13457
15179
  "get_pr_stack",
13458
15180
  "Load the GitHub PR stack for a thread worktree (`gh stack view --json`). Returns null JSON when the branch is not stacked. Prefer this before mergePr on stacked PRs.",
13459
- { ref: import_zod.z.string() },
15181
+ { ref: import_zod2.z.string() },
13460
15182
  async ({ ref }) => {
13461
15183
  const stack = await orch.getPrStack(ref);
13462
15184
  return {
@@ -13468,8 +15190,8 @@ async function startMcpServer() {
13468
15190
  "open_pr_stack_layers",
13469
15191
  "Materialize one worktree+thread per stack layer (or a single 1-based layer). Pass a thread ref already on the stack.",
13470
15192
  {
13471
- ref: import_zod.z.string(),
13472
- layer: import_zod.z.number().int().positive().optional()
15193
+ ref: import_zod2.z.string(),
15194
+ layer: import_zod2.z.number().int().positive().optional()
13473
15195
  },
13474
15196
  async ({ ref, layer }) => {
13475
15197
  const result = await orch.openPrStackLayers(ref, { layer });
@@ -13503,9 +15225,9 @@ async function startMcpServer() {
13503
15225
  "add_stack_layer",
13504
15226
  "Add a branch on top of the current stack (`gh stack add`) and open a worktree+thread for it.",
13505
15227
  {
13506
- ref: import_zod.z.string(),
13507
- branchName: import_zod.z.string(),
13508
- title: import_zod.z.string().optional()
15228
+ ref: import_zod2.z.string(),
15229
+ branchName: import_zod2.z.string(),
15230
+ title: import_zod2.z.string().optional()
13509
15231
  },
13510
15232
  async ({ ref, branchName, title }) => {
13511
15233
  const result = await orch.addStackLayer(ref, branchName, { title });
@@ -13534,11 +15256,11 @@ async function startMcpServer() {
13534
15256
  "create_pr_stack",
13535
15257
  "Create a new GitHub PR stack with one Sideboard worktree per layer (bottom\u2192top branch names). Requires `gh extension install github/gh-stack`.",
13536
15258
  {
13537
- repoPath: import_zod.z.string(),
13538
- branches: import_zod.z.array(import_zod.z.string()).min(1),
13539
- agent: import_zod.z.enum(["claude", "codex", "opencode", "brightsy", "cursor"]),
13540
- base: import_zod.z.string().optional(),
13541
- title: import_zod.z.string().optional()
15259
+ repoPath: import_zod2.z.string(),
15260
+ branches: import_zod2.z.array(import_zod2.z.string()).min(1),
15261
+ agent: import_zod2.z.enum(["claude", "codex", "opencode", "brightsy", "cursor"]),
15262
+ base: import_zod2.z.string().optional(),
15263
+ title: import_zod2.z.string().optional()
13542
15264
  },
13543
15265
  async (args) => {
13544
15266
  const result = await orch.createPrStack({
@@ -13576,7 +15298,7 @@ async function startMcpServer() {
13576
15298
  server.tool(
13577
15299
  "list_issues",
13578
15300
  "List issues from Sideboard Account connections (Linear API or GitHub Issues; Linear\u2192GitHub fallback when Linear is not connected). Pass repoPath from list_workspaces \u2014 GitHub Issues are scoped to that repo. Then create_thread with sourceType=ticket.",
13579
- { repoPath: import_zod.z.string() },
15301
+ { repoPath: import_zod2.z.string() },
13580
15302
  async ({ repoPath }) => {
13581
15303
  const root = await resolveRepoRoot(repoPath);
13582
15304
  const result = await listIssues(root);
@@ -13587,8 +15309,8 @@ async function startMcpServer() {
13587
15309
  "list_linear_issues",
13588
15310
  "Deprecated: prefer list_issues. Lists assigned Linear issues via the chosen agent MCP connector",
13589
15311
  {
13590
- agent: import_zod.z.enum(["claude", "codex", "opencode"]),
13591
- repoPath: import_zod.z.string()
15312
+ agent: import_zod2.z.enum(["claude", "codex", "opencode"]),
15313
+ repoPath: import_zod2.z.string()
13592
15314
  },
13593
15315
  async ({ agent, repoPath }) => {
13594
15316
  const issues = await listLinearIssues(agent, repoPath);