@sideboard-ai/core 0.1.68 → 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 (36) hide show
  1. package/dist/{agents-PE3RHQS5.js → agents-GB7XL2LI.js} +6 -5
  2. package/dist/{agents-JBD7IAGK.js → agents-TCAK3MXN.js} +6 -5
  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-DPDGMMGI.js → chunk-3CY6WJ7O.js} +2 -2
  8. package/dist/{chunk-ROXEENBT.js → chunk-57ROTAFD.js} +62 -3
  9. package/dist/{chunk-N5LMEP65.js → chunk-7NCIHRAU.js} +22 -4
  10. package/dist/{chunk-OICHV4DH.js → chunk-DGPUAZA4.js} +62 -3
  11. package/dist/{chunk-VX6ACNXS.js → chunk-H2IXNFQ2.js} +22 -4
  12. package/dist/chunk-JAWEDVVA.js +106 -0
  13. package/dist/chunk-JT7R45JB.js +20 -0
  14. package/dist/{chunk-EEBDVLDK.js → chunk-MF2YMEGD.js} +61 -9
  15. package/dist/{chunk-47RBLBRK.js → chunk-NG7S3OPX.js} +10 -4
  16. package/dist/chunk-NSTQ6QKD.js +23 -0
  17. package/dist/{chunk-REKGFVUX.js → chunk-OE7YBB5X.js} +61 -9
  18. package/dist/{chunk-BPOTVI5J.js → chunk-PTJJIQK7.js} +472 -18
  19. package/dist/{chunk-6Q7H6SMX.js → chunk-S25IHL5H.js} +10 -4
  20. package/dist/{chunk-QQC2D4MY.js → chunk-UH57WVFP.js} +2 -2
  21. package/dist/{chunk-MHR4OGZ4.js → chunk-W7YOTDVO.js} +493 -18
  22. package/dist/{coordinator-prompt-YZYZ5P6P.js → coordinator-prompt-2J4PIMUF.js} +6 -3
  23. package/dist/{coordinator-prompt-HD6IMCKS.js → coordinator-prompt-QYYG2IBB.js} +6 -3
  24. package/dist/{global-workspace-Z6GF7D2K.js → global-workspace-KHIFQUUM.js} +11 -4
  25. package/dist/{global-workspace-7P5NG3JZ.js → global-workspace-SVRYNJZA.js} +11 -4
  26. package/dist/index.cjs +4184 -850
  27. package/dist/index.d.cts +543 -21
  28. package/dist/index.d.ts +543 -21
  29. package/dist/index.js +3285 -748
  30. package/dist/mcp/run-stdio.cjs +1995 -482
  31. package/dist/mcp/run-stdio.js +943 -151
  32. package/dist/{workspaces-BWJNGEE6.js → workspaces-AHFTHVBT.js} +6 -5
  33. package/dist/{workspaces-O6EZGKQK.js → workspaces-SVQIEVIZ.js} +6 -5
  34. package/dist/{worktree-VDXLUOWR.js → worktree-KWXHMKRN.js} +1 -1
  35. package/dist/{worktree-ZPSKEZFE.js → worktree-VOCH3WS3.js} +1 -1
  36. package/package.json +3 -1
@@ -16,14 +16,14 @@ import {
16
16
  resolveQuotaFallbackAgent,
17
17
  sideboardMcpProfile,
18
18
  summarizeTurnStderr
19
- } from "../chunk-47RBLBRK.js";
19
+ } from "../chunk-NG7S3OPX.js";
20
20
  import "../chunk-VROPG6QF.js";
21
21
  import {
22
22
  addWorkspace,
23
23
  ensureWorkspace,
24
24
  removeWorkspace,
25
25
  syncWorkspacesFromThreads
26
- } from "../chunk-QQC2D4MY.js";
26
+ } from "../chunk-UH57WVFP.js";
27
27
  import {
28
28
  extractPresentedPlan,
29
29
  readPlanFile,
@@ -38,13 +38,14 @@ import {
38
38
  isGlobalThread,
39
39
  isOrchestratorThread,
40
40
  orchestratorSessionPoisonedByBuiltins
41
- } from "../chunk-OICHV4DH.js";
41
+ } from "../chunk-DGPUAZA4.js";
42
42
  import {
43
+ SLACK_REPLY_FORMATTING,
43
44
  coordinatorSystemPrompt,
44
45
  coordinatorTurnReminder,
45
46
  enrichWorkspacesWithGithub,
46
47
  ensureGlobalCoordinatorCwd
47
- } from "../chunk-N5LMEP65.js";
48
+ } from "../chunk-7NCIHRAU.js";
48
49
  import {
49
50
  addPrStackLayer,
50
51
  allocateTeamName,
@@ -81,7 +82,7 @@ import {
81
82
  takenSlugsFromThread,
82
83
  threadDisplayLabel,
83
84
  worktreeNameFromPath
84
- } from "../chunk-REKGFVUX.js";
85
+ } from "../chunk-OE7YBB5X.js";
85
86
  import {
86
87
  ATTACHMENTS_DIR,
87
88
  LEGACY_ATTACHMENTS_DIR,
@@ -108,17 +109,26 @@ import {
108
109
  } from "../chunk-AE5VBOFE.js";
109
110
  import {
110
111
  childEnvWithAppSettings,
111
- getLinearApiKey,
112
+ disconnectLinearConnection,
112
113
  isLinearConnected,
114
+ isSecureFileEncrypted,
113
115
  loadAppSettings,
114
116
  orchestrationQuotaFallbackAgent,
115
117
  orchestrationQuotaOnLimit,
118
+ readSecureJson,
116
119
  resolveEffectiveIssueSource,
117
120
  resolveNewThreadOptions,
118
- updateAdvancedSettings
119
- } from "../chunk-BPOTVI5J.js";
121
+ resolveVaultKey,
122
+ saveLinearOAuth,
123
+ updateAdvancedSettings,
124
+ writeSecureJson
125
+ } from "../chunk-PTJJIQK7.js";
126
+ import {
127
+ writePrivateFile
128
+ } from "../chunk-NSTQ6QKD.js";
120
129
  import "../chunk-KGZBYWZ3.js";
121
130
  import {
131
+ appDataDir,
122
132
  loadRepoSettings,
123
133
  loadWorkspaceSettings,
124
134
  sideboardReposDir,
@@ -130,7 +140,7 @@ import {
130
140
  // src/mcp/server.ts
131
141
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
132
142
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
133
- import { z } from "zod";
143
+ import { z as z2 } from "zod";
134
144
  import { basename as basename4 } from "path";
135
145
 
136
146
  // src/orchestrator/orchestrator.ts
@@ -215,9 +225,9 @@ function toolFilePath(input) {
215
225
  if (!input) return void 0;
216
226
  return str(input.file_path) ?? str(input.path) ?? str(input.filePath) ?? str(input.filename);
217
227
  }
218
- function countLines(text) {
219
- if (!text) return 0;
220
- return text.split("\n").length;
228
+ function countLines(text2) {
229
+ if (!text2) return 0;
230
+ return text2.split("\n").length;
221
231
  }
222
232
  function diffFromInput(input) {
223
233
  if (!input) return {};
@@ -344,9 +354,9 @@ function applyAgentEvent(parts, event) {
344
354
  function partsToAssistantText(parts) {
345
355
  return parts.filter((p) => p.type === "text").map((p) => p.text).join("").trim();
346
356
  }
347
- function stripBrightsyNdjsonNoise(text) {
348
- if (!text || !text.includes('"type"')) return text;
349
- let out = text;
357
+ function stripBrightsyNdjsonNoise(text2) {
358
+ if (!text2 || !text2.includes('"type"')) return text2;
359
+ let out = text2;
350
360
  out = out.replace(
351
361
  /\{"type":"(?:tool_use|tool_result|tool|thinking|usage|done|error)"[\s\S]*?\}\s*(?=\{"type":"|$|(?=[A-Za-z*#]))/g,
352
362
  ""
@@ -391,9 +401,9 @@ async function spawnAgentTurn(thread, input, onEvent) {
391
401
  `Cannot spawn ${thread.agent}: thread ${thread.id} has no worktreePath`
392
402
  );
393
403
  }
394
- const { isGlobalThread: isGlobalThread2 } = await import("../global-workspace-Z6GF7D2K.js");
404
+ const { isGlobalThread: isGlobalThread2 } = await import("../global-workspace-KHIFQUUM.js");
395
405
  if (isGlobalThread2(thread)) {
396
- const { ensureGlobalCoordinatorCwd: ensureGlobalCoordinatorCwd2 } = await import("../coordinator-prompt-YZYZ5P6P.js");
406
+ const { ensureGlobalCoordinatorCwd: ensureGlobalCoordinatorCwd2 } = await import("../coordinator-prompt-2J4PIMUF.js");
397
407
  ensureGlobalCoordinatorCwd2(
398
408
  isOrchestratorThread(thread) ? { orchestratorThreadId: thread.id } : void 0
399
409
  );
@@ -480,8 +490,8 @@ async function spawnAgentTurn(thread, input, onEvent) {
480
490
  onEvent({ type: "exit", data: exitCode });
481
491
  const finalized = finalizeParts(parts);
482
492
  const rawText = assistantText.trim() || partsToAssistantText(finalized);
483
- const text = thread.agent === "brightsy" ? stripBrightsyNdjsonNoise(rawText) : rawText;
484
- return { exitCode, sessionId, assistantText: text, parts: finalized, usage };
493
+ const text2 = thread.agent === "brightsy" ? stripBrightsyNdjsonNoise(rawText) : rawText;
494
+ return { exitCode, sessionId, assistantText: text2, parts: finalized, usage };
485
495
  });
486
496
  return {
487
497
  pid: child.pid,
@@ -1209,7 +1219,7 @@ async function createThread(input, _onSetupLine) {
1209
1219
  return readThread(thread.id) ?? thread;
1210
1220
  }
1211
1221
  async function listLinearIssues(agent, repoPath) {
1212
- const { getAdapter: getAdapter2 } = await import("../agents-PE3RHQS5.js");
1222
+ const { getAdapter: getAdapter2 } = await import("../agents-GB7XL2LI.js");
1213
1223
  await requireAgent(agent, { requireLinear: true });
1214
1224
  const adapter = getAdapter2(agent);
1215
1225
  if (!adapter.listLinearIssues) {
@@ -1265,9 +1275,9 @@ async function tryClaudeSummary(transcript, opts) {
1265
1275
  { cwd: opts?.cwd, reject: false }
1266
1276
  );
1267
1277
  if (exitCode !== 0) return null;
1268
- const text = stdout.trim();
1269
- if (text.length < 40) return null;
1270
- return text;
1278
+ const text2 = stdout.trim();
1279
+ if (text2.length < 40) return null;
1280
+ return text2;
1271
1281
  } catch {
1272
1282
  return null;
1273
1283
  }
@@ -1316,9 +1326,9 @@ function extractiveSummary(transcript) {
1316
1326
  );
1317
1327
  return parts.join("\n");
1318
1328
  }
1319
- function clipSummary(text) {
1320
- if (text.length <= MAX_SUMMARY_CHARS) return text;
1321
- return `${text.slice(0, MAX_SUMMARY_CHARS)}
1329
+ function clipSummary(text2) {
1330
+ if (text2.length <= MAX_SUMMARY_CHARS) return text2;
1331
+ return `${text2.slice(0, MAX_SUMMARY_CHARS)}
1322
1332
 
1323
1333
  [\u2026summary truncated\u2026]`;
1324
1334
  }
@@ -1931,8 +1941,8 @@ function buildQuotaHandoffAttachment(from, limitText, fallbackAgent) {
1931
1941
  );
1932
1942
  const recent = from.messages.slice(-8).map((m) => {
1933
1943
  const role = m.role === "user" ? "User" : m.role === "agent" ? "Agent" : "Summary";
1934
- const text = m.text.trim().replace(/\s+/g, " ").slice(0, 280);
1935
- return text ? `- ${role}: ${text}` : null;
1944
+ const text2 = m.text.trim().replace(/\s+/g, " ").slice(0, 280);
1945
+ return text2 ? `- ${role}: ${text2}` : null;
1936
1946
  }).filter(Boolean);
1937
1947
  const body = [
1938
1948
  `# Orchestration handoff`,
@@ -2028,13 +2038,13 @@ function resolveConductorCursorAgentId(workspacePath) {
2028
2038
  for (const hash of hashes) {
2029
2039
  const agentsFile = join6(CURSOR_SDK_STORE, hash, "agents.ndjson");
2030
2040
  if (!existsSync7(agentsFile)) continue;
2031
- let text;
2041
+ let text2;
2032
2042
  try {
2033
- text = readFileSync4(agentsFile, "utf8");
2043
+ text2 = readFileSync4(agentsFile, "utf8");
2034
2044
  } catch {
2035
2045
  continue;
2036
2046
  }
2037
- for (const line of text.split("\n")) {
2047
+ for (const line of text2.split("\n")) {
2038
2048
  const trimmed = line.trim();
2039
2049
  if (!trimmed) continue;
2040
2050
  try {
@@ -2073,7 +2083,7 @@ async function adoptThread(input) {
2073
2083
  messages: input.messages ?? []
2074
2084
  });
2075
2085
  writeThread(thread);
2076
- const { ensureWorkspace: ensureWorkspace2 } = await import("../workspaces-O6EZGKQK.js");
2086
+ const { ensureWorkspace: ensureWorkspace2 } = await import("../workspaces-SVQIEVIZ.js");
2077
2087
  await ensureWorkspace2(repoPath);
2078
2088
  return thread;
2079
2089
  }
@@ -2797,13 +2807,13 @@ function formatStat(files) {
2797
2807
  return `${n} file${n === 1 ? "" : "s"} changed, ${additions} insertions(+), ${deletions} deletions(-)`;
2798
2808
  }
2799
2809
  function emptyScopeStats() {
2800
- const z2 = emptyStat();
2810
+ const z3 = emptyStat();
2801
2811
  return {
2802
- commits: z2,
2803
- uncommitted: z2,
2804
- staged: z2,
2805
- unstaged: z2,
2806
- last_turn: z2
2812
+ commits: z3,
2813
+ uncommitted: z3,
2814
+ staged: z3,
2815
+ unstaged: z3,
2816
+ last_turn: z3
2807
2817
  };
2808
2818
  }
2809
2819
  function filesFromDiff(nameStatus, numstat, combinedDiff, maxHunk) {
@@ -4112,7 +4122,7 @@ var Orchestrator = class {
4112
4122
  orphans: orphans.map((o) => ({ path: o.path, repoPath: o.repoPath }))
4113
4123
  });
4114
4124
  }
4115
- const { autoCleanupOrphansEnabled } = await import("../app-settings-GJVZGU3M.js");
4125
+ const { autoCleanupOrphansEnabled } = await import("../app-settings-P42Z3VOB.js");
4116
4126
  if (autoCleanupOrphansEnabled() && shouldRunWorktreeCleanup() && orphans.length > 0) {
4117
4127
  await cleanupOrphanWorktrees({ repoPaths });
4118
4128
  }
@@ -4269,7 +4279,7 @@ var Orchestrator = class {
4269
4279
  }
4270
4280
  async finishCreateThread(threadId, prompt) {
4271
4281
  await this.runSetupAfterCreate(threadId);
4272
- const { autoRunAfterSetupEnabled } = await import("../app-settings-GJVZGU3M.js");
4282
+ const { autoRunAfterSetupEnabled } = await import("../app-settings-P42Z3VOB.js");
4273
4283
  if (autoRunAfterSetupEnabled()) {
4274
4284
  try {
4275
4285
  await this.startDev(threadId);
@@ -4349,11 +4359,11 @@ var Orchestrator = class {
4349
4359
  return results;
4350
4360
  }
4351
4361
  /** Edit the text of a not-yet-started queued message. */
4352
- async editQueuedMessage(threadRef, index, text) {
4362
+ async editQueuedMessage(threadRef, index, text2) {
4353
4363
  const thread = this.requireThread(threadRef);
4354
4364
  return withThreadLock(thread.id, async () => {
4355
4365
  const current = this.requireThread(thread.id);
4356
- const trimmed = text.trim();
4366
+ const trimmed = text2.trim();
4357
4367
  if (!trimmed || index < 0 || index >= current.queue.length) {
4358
4368
  return current;
4359
4369
  }
@@ -4498,10 +4508,19 @@ var Orchestrator = class {
4498
4508
  attachments: sentAttachments ?? thread.attachments
4499
4509
  }
4500
4510
  );
4501
- const orchestrationReminder = isOrchestratorThread(thread) ? coordinatorTurnReminder({
4502
- parentId: threadId,
4503
- goal: thread.sourceRef || thread.title
4504
- }) : null;
4511
+ const slackInbound = /^(Slack DM|Slack @mention)(?:\n|$)/.test(
4512
+ expandedPrompt.trim()
4513
+ );
4514
+ const orchestrationReminder = isOrchestratorThread(thread) ? [
4515
+ coordinatorTurnReminder({
4516
+ parentId: threadId,
4517
+ goal: thread.sourceRef || thread.title
4518
+ }),
4519
+ slackInbound ? [
4520
+ "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.",
4521
+ SLACK_REPLY_FORMATTING
4522
+ ].join("\n") : null
4523
+ ].filter(Boolean).join("\n") : null;
4505
4524
  const artifactReminder = thread.agent !== "brightsy" ? [
4506
4525
  "Sideboard side column (important):",
4507
4526
  "There is no claude.ai Artifact tool here \u2014 that is normal.",
@@ -4541,7 +4560,7 @@ var Orchestrator = class {
4541
4560
  });
4542
4561
  const artifactDirective = isBrightsy ? null : formatArtifactDirective();
4543
4562
  const settings = loadWorkspaceSettings(fresh.worktreePath, fresh.repoPath);
4544
- const { autoRenameBranchEnabled } = await import("../app-settings-GJVZGU3M.js");
4563
+ const { autoRenameBranchEnabled } = await import("../app-settings-P42Z3VOB.js");
4545
4564
  const renameBranchDirective = !isBrightsy && !isOrchestration && autoRenameBranchEnabled() ? formatRenameBranchDirective(fresh, {
4546
4565
  customPrompt: settings?.prompts?.renameBranch
4547
4566
  }) : null;
@@ -4561,7 +4580,9 @@ var Orchestrator = class {
4561
4580
  goal: fresh.sourceRef || fresh.title || "Orchestration",
4562
4581
  parentId: fresh.id,
4563
4582
  workspaces: inventory,
4564
- audience: "desktop"
4583
+ audience: /^(Slack DM|Slack @mention)(?:\n|$)/.test(
4584
+ expandedPrompt.trim()
4585
+ ) ? "slack" : "desktop"
4565
4586
  });
4566
4587
  }
4567
4588
  }
@@ -5186,7 +5207,8 @@ var Orchestrator = class {
5186
5207
  isDraft: false,
5187
5208
  reviewDecision: null,
5188
5209
  baseRefName: "",
5189
- headRefName: ""
5210
+ headRefName: "",
5211
+ isInMergeQueue: false
5190
5212
  };
5191
5213
  await this.persistPrMetaAndMaybeArchive(thread, metaLike);
5192
5214
  return { url: metaLike.url, state };
@@ -5236,7 +5258,7 @@ var Orchestrator = class {
5236
5258
  updateThread(thread.id, { title: meta.title });
5237
5259
  }
5238
5260
  }
5239
- const { autoArchiveOnMergeEnabled } = await import("../app-settings-GJVZGU3M.js");
5261
+ const { autoArchiveOnMergeEnabled } = await import("../app-settings-P42Z3VOB.js");
5240
5262
  const latest = this.requireThread(thread.id);
5241
5263
  if (!shouldAutoArchiveOnPrMerge({
5242
5264
  previousPrState: prevState || null,
@@ -5460,7 +5482,7 @@ var Orchestrator = class {
5460
5482
  this.emit({ type: "status_changed", threadId: archived.id, status: "archived" });
5461
5483
  if (thread.repoPath && !isGlobalRepoPath(thread.repoPath)) {
5462
5484
  try {
5463
- const { ensureWorkspace: ensureWorkspace2 } = await import("../workspaces-O6EZGKQK.js");
5485
+ const { ensureWorkspace: ensureWorkspace2 } = await import("../workspaces-SVQIEVIZ.js");
5464
5486
  await ensureWorkspace2(thread.repoPath);
5465
5487
  } catch {
5466
5488
  }
@@ -5481,7 +5503,7 @@ var Orchestrator = class {
5481
5503
  await runArchiveScript(thread.repoPath, thread.worktreePath);
5482
5504
  } catch {
5483
5505
  }
5484
- const { deleteBranchOnPurgeEnabled } = await import("../app-settings-GJVZGU3M.js");
5506
+ const { deleteBranchOnPurgeEnabled } = await import("../app-settings-P42Z3VOB.js");
5485
5507
  const deleteBranch = opts?.deleteBranch ?? deleteBranchOnPurgeEnabled();
5486
5508
  await removeWorktree(thread.repoPath, thread.worktreePath, {
5487
5509
  deleteBranch: deleteBranch ? thread.branchName : void 0
@@ -5502,7 +5524,7 @@ var Orchestrator = class {
5502
5524
  return restored2;
5503
5525
  }
5504
5526
  if (!existsSync13(thread.worktreePath)) {
5505
- const { createThreadWorktree: createThreadWorktree2 } = await import("../worktree-VDXLUOWR.js");
5527
+ const { createThreadWorktree: createThreadWorktree2 } = await import("../worktree-KWXHMKRN.js");
5506
5528
  const { execa: execa6 } = await import("execa");
5507
5529
  const slug = thread.worktreePath.split("/").pop();
5508
5530
  const dest = thread.worktreePath;
@@ -5574,6 +5596,103 @@ function getOrchestrator() {
5574
5596
  return singleton;
5575
5597
  }
5576
5598
 
5599
+ // src/integrations/linear-oauth.ts
5600
+ import { createHash, randomBytes } from "crypto";
5601
+ import { createServer as createServer2 } from "http";
5602
+
5603
+ // src/integrations/linear-app.ts
5604
+ var BAKED_LINEAR_CLIENT_ID = "39a15abc7ea5bea17c47f47f85ff5fd0";
5605
+ var BAKED_LINEAR_CLIENT_SECRET = "";
5606
+
5607
+ // src/integrations/linear-oauth.ts
5608
+ var LINEAR_OAUTH_PORT = 19848;
5609
+ var LINEAR_OAUTH_REDIRECT = `http://127.0.0.1:${LINEAR_OAUTH_PORT}/callback`;
5610
+ var LINEAR_TOKEN = "https://api.linear.app/oauth/token";
5611
+ var REFRESH_SKEW_MS = 5 * 6e4;
5612
+ function linearOAuthCredentials() {
5613
+ const settings = loadAppSettings();
5614
+ const clientId = process.env.SIDEBOARD_LINEAR_CLIENT_ID?.trim() || settings.integrations.linearClientId?.trim() || BAKED_LINEAR_CLIENT_ID.trim();
5615
+ const clientSecret = process.env.SIDEBOARD_LINEAR_CLIENT_SECRET?.trim() || settings.integrations.linearClientSecret?.trim() || BAKED_LINEAR_CLIENT_SECRET.trim();
5616
+ if (!clientId) {
5617
+ throw new Error(
5618
+ `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.`
5619
+ );
5620
+ }
5621
+ return { clientId, clientSecret };
5622
+ }
5623
+ function linearAuthorizationHeader(token) {
5624
+ const t = token.trim();
5625
+ if (!t) return t;
5626
+ if (/^bearer\s+/i.test(t)) return t;
5627
+ if (t.startsWith("lin_api_")) return t;
5628
+ return `Bearer ${t}`;
5629
+ }
5630
+ async function exchangeLinearToken(body) {
5631
+ const res = await fetch(LINEAR_TOKEN, {
5632
+ method: "POST",
5633
+ headers: { "Content-Type": "application/x-www-form-urlencoded" },
5634
+ body
5635
+ });
5636
+ const data = await res.json().catch(() => ({}));
5637
+ if (!res.ok || data.error || !data.access_token?.trim()) {
5638
+ const detail = data.error_description || data.error || `HTTP ${res.status}`;
5639
+ throw new Error(`Linear OAuth token exchange failed: ${detail}`);
5640
+ }
5641
+ return data;
5642
+ }
5643
+ async function persistLinearTokens(data, viewer) {
5644
+ return saveLinearOAuth({
5645
+ accessToken: data.access_token.trim(),
5646
+ refreshToken: data.refresh_token?.trim(),
5647
+ expiresIn: data.expires_in,
5648
+ viewerName: viewer?.name,
5649
+ organizationName: viewer?.organizationName
5650
+ });
5651
+ }
5652
+ var refreshInFlight = null;
5653
+ async function refreshLinearAccessToken(refreshToken) {
5654
+ const { clientId, clientSecret } = linearOAuthCredentials();
5655
+ const body = new URLSearchParams({
5656
+ grant_type: "refresh_token",
5657
+ refresh_token: refreshToken,
5658
+ client_id: clientId
5659
+ });
5660
+ if (clientSecret) body.set("client_secret", clientSecret);
5661
+ try {
5662
+ const data = await exchangeLinearToken(body);
5663
+ await persistLinearTokens(data);
5664
+ return data.access_token.trim();
5665
+ } catch (err) {
5666
+ const message = err instanceof Error ? err.message : String(err);
5667
+ if (/invalid_grant|invalid_token|unauthorized/i.test(message)) {
5668
+ disconnectLinearConnection();
5669
+ throw new Error("Linear sign-in expired \u2014 connect again in Account settings");
5670
+ }
5671
+ throw err;
5672
+ }
5673
+ }
5674
+ async function getLinearAuthToken(settings = loadAppSettings()) {
5675
+ const access = settings.integrations.linearAccessToken?.trim() || "";
5676
+ const refresh = settings.integrations.linearRefreshToken?.trim() || "";
5677
+ const expiresAt = settings.integrations.linearTokenExpiresAt ?? 0;
5678
+ const apiKey = settings.integrations.linearApiKey?.trim() || "";
5679
+ if (access && (!refresh || Date.now() < expiresAt - REFRESH_SKEW_MS)) {
5680
+ return access;
5681
+ }
5682
+ if (refresh) {
5683
+ if (!refreshInFlight) {
5684
+ refreshInFlight = refreshLinearAccessToken(refresh).finally(() => {
5685
+ refreshInFlight = null;
5686
+ });
5687
+ }
5688
+ return refreshInFlight.catch((err) => {
5689
+ if (apiKey) return apiKey;
5690
+ throw err;
5691
+ });
5692
+ }
5693
+ return apiKey || null;
5694
+ }
5695
+
5577
5696
  // src/integrations/linear.ts
5578
5697
  var LINEAR_GRAPHQL = "https://api.linear.app/graphql";
5579
5698
  var ASSIGNED_ISSUES_QUERY = `
@@ -5596,16 +5715,16 @@ query SideboardAssignedIssues($first: Int!) {
5596
5715
  }
5597
5716
  `;
5598
5717
  async function listLinearIssuesDirect(opts) {
5599
- const apiKey = (opts?.apiKey ?? getLinearApiKey())?.trim();
5718
+ const apiKey = (opts?.apiKey ?? await getLinearAuthToken())?.trim();
5600
5719
  if (!apiKey) {
5601
- throw new Error("Linear is not connected \u2014 add an API key in Account settings");
5720
+ throw new Error("Linear is not connected \u2014 sign in from Account settings");
5602
5721
  }
5603
5722
  const first = Math.max(1, Math.min(100, opts?.limit ?? 50));
5604
5723
  const res = await fetch(LINEAR_GRAPHQL, {
5605
5724
  method: "POST",
5606
5725
  headers: {
5607
5726
  "Content-Type": "application/json",
5608
- Authorization: apiKey
5727
+ Authorization: linearAuthorizationHeader(apiKey)
5609
5728
  },
5610
5729
  body: JSON.stringify({
5611
5730
  query: ASSIGNED_ISSUES_QUERY,
@@ -5695,6 +5814,640 @@ function mcpArchiveBlockedReason(thread) {
5695
5814
  return null;
5696
5815
  }
5697
5816
 
5817
+ // src/mcp/slack-tools.ts
5818
+ import { z } from "zod";
5819
+
5820
+ // src/slack/api.ts
5821
+ var SLACK_API = "https://slack.com/api";
5822
+ var SlackApiError = class extends Error {
5823
+ constructor(method, slackError) {
5824
+ super(`Slack ${method}: ${slackError}`);
5825
+ this.method = method;
5826
+ this.slackError = slackError;
5827
+ this.name = "SlackApiError";
5828
+ }
5829
+ method;
5830
+ slackError;
5831
+ };
5832
+ async function slackApi(token, method, params, fetchImpl) {
5833
+ const doFetch = fetchImpl ?? fetch;
5834
+ const body = new URLSearchParams();
5835
+ if (params) {
5836
+ for (const [key, value] of Object.entries(params)) {
5837
+ if (value === void 0) continue;
5838
+ body.set(key, String(value));
5839
+ }
5840
+ }
5841
+ const res = await doFetch(`${SLACK_API}/${method}`, {
5842
+ method: "POST",
5843
+ headers: {
5844
+ Authorization: `Bearer ${token}`,
5845
+ "Content-Type": "application/x-www-form-urlencoded"
5846
+ },
5847
+ body
5848
+ });
5849
+ const json = await res.json();
5850
+ if (!json.ok) {
5851
+ throw new SlackApiError(method, json.error || `HTTP ${res.status}`);
5852
+ }
5853
+ return json;
5854
+ }
5855
+
5856
+ // src/slack/destination.ts
5857
+ function isChannelId(raw) {
5858
+ return /^[CGD][A-Z0-9]+$/i.test(raw);
5859
+ }
5860
+ function isUserId(raw) {
5861
+ return /^U[A-Z0-9]+$/i.test(raw);
5862
+ }
5863
+ function isEmail(raw) {
5864
+ return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(raw);
5865
+ }
5866
+ function normalizeSlackHandle(raw) {
5867
+ let s = raw.trim();
5868
+ const mention = s.match(/^<@([UW][A-Z0-9]+)(?:\|[^>]+)?>$/i);
5869
+ if (mention?.[1]) return mention[1];
5870
+ const channelMention = s.match(/^<#([CGD][A-Z0-9]+)(?:\|[^>]+)?>$/i);
5871
+ if (channelMention?.[1]) return channelMention[1];
5872
+ if (s.startsWith("#") || s.startsWith("@")) s = s.slice(1);
5873
+ return s.trim();
5874
+ }
5875
+ async function listSlackUsers(token, opts) {
5876
+ const query = opts?.query?.trim().toLowerCase() ?? "";
5877
+ const limit = Math.min(200, Math.max(1, opts?.limit ?? 50));
5878
+ const members = [];
5879
+ let cursor;
5880
+ do {
5881
+ const data = await slackApi(
5882
+ token,
5883
+ "users.list",
5884
+ {
5885
+ limit: 200,
5886
+ cursor: cursor || void 0
5887
+ },
5888
+ opts?.fetchImpl
5889
+ );
5890
+ for (const m of data.members ?? []) {
5891
+ if (!m.id || m.deleted || m.is_bot) continue;
5892
+ members.push(m);
5893
+ }
5894
+ cursor = data.response_metadata?.next_cursor?.trim() || void 0;
5895
+ } while (cursor && members.length < 2e3);
5896
+ const mapped = members.map((m) => ({
5897
+ id: m.id,
5898
+ name: m.name ?? m.id,
5899
+ real_name: m.profile?.real_name,
5900
+ display_name: m.profile?.display_name,
5901
+ email: m.profile?.email
5902
+ }));
5903
+ const filtered = query ? mapped.filter((u) => {
5904
+ const hay = [u.name, u.real_name, u.display_name, u.email, u.id].filter(Boolean).join(" ").toLowerCase();
5905
+ return hay.includes(query);
5906
+ }) : mapped;
5907
+ return filtered.slice(0, limit);
5908
+ }
5909
+ async function findUserId(token, handle, fetchImpl) {
5910
+ if (isUserId(handle)) {
5911
+ return { id: handle, name: handle };
5912
+ }
5913
+ if (isEmail(handle)) {
5914
+ try {
5915
+ const data = await slackApi(
5916
+ token,
5917
+ "users.lookupByEmail",
5918
+ { email: handle },
5919
+ fetchImpl
5920
+ );
5921
+ if (data.user?.id) {
5922
+ return {
5923
+ id: data.user.id,
5924
+ name: data.user.name ?? data.user.id,
5925
+ real_name: data.user.profile?.real_name,
5926
+ display_name: data.user.profile?.display_name,
5927
+ email: data.user.profile?.email
5928
+ };
5929
+ }
5930
+ } catch {
5931
+ }
5932
+ }
5933
+ const users = await listSlackUsers(token, { query: handle, limit: 20, fetchImpl });
5934
+ const exact = users.find(
5935
+ (u) => u.name.toLowerCase() === handle.toLowerCase() || u.display_name?.toLowerCase() === handle.toLowerCase() || u.real_name?.toLowerCase() === handle.toLowerCase() || u.email?.toLowerCase() === handle.toLowerCase()
5936
+ );
5937
+ if (exact) return exact;
5938
+ if (users.length === 1) return users[0];
5939
+ if (users.length === 0) {
5940
+ throw new Error(`No Slack user matching "${handle}"`);
5941
+ }
5942
+ throw new Error(
5943
+ `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.`
5944
+ );
5945
+ }
5946
+ async function openDmChannel(token, userId, fetchImpl) {
5947
+ const data = await slackApi(
5948
+ token,
5949
+ "conversations.open",
5950
+ { users: userId },
5951
+ fetchImpl
5952
+ );
5953
+ const id = data.channel?.id?.trim();
5954
+ if (!id) throw new Error(`Could not open Slack DM with user ${userId}`);
5955
+ return id;
5956
+ }
5957
+ async function resolveChannelByName(token, name, fetchImpl) {
5958
+ const data = await slackApi(
5959
+ token,
5960
+ "conversations.list",
5961
+ {
5962
+ types: "public_channel,private_channel",
5963
+ exclude_archived: true,
5964
+ limit: 200
5965
+ },
5966
+ fetchImpl
5967
+ );
5968
+ const match = (data.channels ?? []).find((c) => c.name?.toLowerCase() === name.toLowerCase());
5969
+ if (!match?.id) throw new Error(`No Slack channel named #${name}`);
5970
+ return match.id;
5971
+ }
5972
+ async function resolveSlackDestination(token, destination, opts) {
5973
+ const raw = destination.trim();
5974
+ if (!raw) throw new Error("destination is required");
5975
+ const hadAt = raw.startsWith("@") || /^<@/i.test(raw);
5976
+ const hadHash = raw.startsWith("#") || /^<#/i.test(raw);
5977
+ const handle = normalizeSlackHandle(raw);
5978
+ if (isChannelId(handle)) {
5979
+ return {
5980
+ channelId: handle,
5981
+ kind: handle.toUpperCase().startsWith("D") ? "dm" : "channel",
5982
+ label: handle
5983
+ };
5984
+ }
5985
+ if (isUserId(handle) || hadAt || isEmail(handle)) {
5986
+ const user = await findUserId(token, handle, opts?.fetchImpl);
5987
+ if (opts?.forRead) {
5988
+ const channelId2 = await openDmChannel(token, user.id, opts?.fetchImpl);
5989
+ return {
5990
+ channelId: channelId2,
5991
+ kind: "user",
5992
+ userId: user.id,
5993
+ label: user.display_name || user.real_name || user.name
5994
+ };
5995
+ }
5996
+ const channelId = await openDmChannel(token, user.id, opts?.fetchImpl);
5997
+ return {
5998
+ channelId,
5999
+ kind: "user",
6000
+ userId: user.id,
6001
+ label: user.display_name || user.real_name || user.name
6002
+ };
6003
+ }
6004
+ if (hadHash) {
6005
+ const channelId = await resolveChannelByName(token, handle, opts?.fetchImpl);
6006
+ return { channelId, kind: "channel", label: `#${handle}` };
6007
+ }
6008
+ try {
6009
+ const channelId = await resolveChannelByName(token, handle, opts?.fetchImpl);
6010
+ return { channelId, kind: "channel", label: `#${handle}` };
6011
+ } catch (channelErr) {
6012
+ try {
6013
+ const user = await findUserId(token, handle, opts?.fetchImpl);
6014
+ const channelId = await openDmChannel(token, user.id, opts?.fetchImpl);
6015
+ return {
6016
+ channelId,
6017
+ kind: "user",
6018
+ userId: user.id,
6019
+ label: user.display_name || user.real_name || user.name
6020
+ };
6021
+ } catch {
6022
+ throw channelErr;
6023
+ }
6024
+ }
6025
+ }
6026
+
6027
+ // src/slack/github-link.ts
6028
+ function labelGithubUrl(url) {
6029
+ const raw = url.trim();
6030
+ if (!raw) return null;
6031
+ let parsed;
6032
+ try {
6033
+ parsed = new URL(raw);
6034
+ } catch {
6035
+ return null;
6036
+ }
6037
+ if (!/(^|\.)github\.com$/i.test(parsed.hostname)) return null;
6038
+ const path = parsed.pathname;
6039
+ const hash = parsed.hash || "";
6040
+ const pr = path.match(/\/([^/]+)\/([^/]+)\/pull\/(\d+)/i);
6041
+ if (pr) {
6042
+ const n = pr[3];
6043
+ if (/#discussion_r\d+/i.test(hash) || /#pullrequestreview-\d+/i.test(hash)) {
6044
+ return { kind: "comment", label: `PR #${n} comment`, url: raw };
6045
+ }
6046
+ if (/#issuecomment-\d+/i.test(hash)) {
6047
+ return { kind: "comment", label: `PR #${n} comment`, url: raw };
6048
+ }
6049
+ return { kind: "pr", label: `PR #${n}`, url: raw };
6050
+ }
6051
+ const issueComment = path.match(/\/([^/]+)\/([^/]+)\/issues\/(\d+)/i);
6052
+ if (issueComment && /#issuecomment-\d+/i.test(hash)) {
6053
+ return { kind: "comment", label: `Issue #${issueComment[3]} comment`, url: raw };
6054
+ }
6055
+ const blob = path.match(/\/([^/]+)\/([^/]+)\/blob\/[^/]+\/(.+)$/i);
6056
+ if (blob) {
6057
+ const filePath = decodeURIComponent(blob[3]);
6058
+ const fileName = filePath.split("/").pop() || filePath;
6059
+ const line = hash.match(/^#L(\d+)(?:-L(\d+))?$/i);
6060
+ if (line) {
6061
+ const start = line[1];
6062
+ const end = line[2];
6063
+ const loc = end && end !== start ? `${start}-${end}` : start;
6064
+ return { kind: "code", label: `${fileName}:${loc}`, url: raw };
6065
+ }
6066
+ return { kind: "code", label: fileName, url: raw };
6067
+ }
6068
+ return { kind: "other", label: "GitHub", url: raw };
6069
+ }
6070
+ function appendGithubLink(text2, githubUrl) {
6071
+ const body = text2.trimEnd();
6072
+ if (!githubUrl?.trim()) return body;
6073
+ const labeled = labelGithubUrl(githubUrl);
6074
+ if (!labeled) {
6075
+ return body ? `${body}
6076
+ ${githubUrl.trim()}` : githubUrl.trim();
6077
+ }
6078
+ const link = `<${labeled.url}|${labeled.label}>`;
6079
+ return body ? `${body}
6080
+ ${link}` : link;
6081
+ }
6082
+
6083
+ // src/slack/outbound-watch.ts
6084
+ import { existsSync as existsSync14, readFileSync as readFileSync9 } from "fs";
6085
+ import { join as join12 } from "path";
6086
+
6087
+ // src/slack/workspaces.ts
6088
+ import { join as join11 } from "path";
6089
+ function storePath() {
6090
+ return join11(appDataDir(), "slack-workspaces.json");
6091
+ }
6092
+ function readStore() {
6093
+ try {
6094
+ const path = storePath();
6095
+ const wasEncrypted = isSecureFileEncrypted(path);
6096
+ const parsed = readSecureJson(path);
6097
+ const workspaces = Array.isArray(parsed?.workspaces) ? parsed.workspaces : [];
6098
+ if (workspaces.length > 0 && !wasEncrypted && resolveVaultKey()) {
6099
+ writeSecureJson(path, { workspaces });
6100
+ }
6101
+ return workspaces;
6102
+ } catch {
6103
+ return [];
6104
+ }
6105
+ }
6106
+ function toInfo(ws) {
6107
+ return {
6108
+ team_id: ws.team_id,
6109
+ team_name: ws.team_name,
6110
+ user_id: ws.user_id,
6111
+ has_bot_token: Boolean(ws.bot_token),
6112
+ has_user_token: Boolean(ws.user_token),
6113
+ connected_at: ws.connected_at
6114
+ };
6115
+ }
6116
+ function listSlackWorkspaces() {
6117
+ return readStore().map(toInfo).sort((a, b) => a.team_name.localeCompare(b.team_name));
6118
+ }
6119
+ function getSlackWorkspace(teamId) {
6120
+ const id = teamId.trim();
6121
+ if (!id) return null;
6122
+ return readStore().find(
6123
+ (ws) => ws.team_id === id || ws.team_name.toLowerCase() === id.toLowerCase()
6124
+ ) ?? null;
6125
+ }
6126
+ function slackTokenFor(ws, kind = "read") {
6127
+ if (kind === "search") {
6128
+ const token2 = ws.user_token?.trim();
6129
+ if (!token2) {
6130
+ throw new Error(
6131
+ `Slack search needs a user token for ${ws.team_name}. Reconnect via Account \u2192 Slack (browser) or paste an xoxp- token.`
6132
+ );
6133
+ }
6134
+ return token2;
6135
+ }
6136
+ const token = (kind === "write" ? ws.bot_token || ws.user_token : ws.user_token || ws.bot_token)?.trim();
6137
+ if (!token) {
6138
+ throw new Error(`Slack workspace ${ws.team_name} has no token`);
6139
+ }
6140
+ return token;
6141
+ }
6142
+ function requireSlackWorkspace(teamId) {
6143
+ const ws = getSlackWorkspace(teamId);
6144
+ if (!ws) {
6145
+ const connected = listSlackWorkspaces();
6146
+ const hint = connected.length === 0 ? "Connect a workspace in Account \u2192 Slack workspaces." : `Connected: ${connected.map((t) => `${t.team_name} (${t.team_id})`).join(", ")}`;
6147
+ throw new Error(`Unknown Slack team_id "${teamId}". ${hint}`);
6148
+ }
6149
+ return ws;
6150
+ }
6151
+
6152
+ // src/slack/outbound-watch.ts
6153
+ var MAX_WATCHES = 40;
6154
+ var WATCH_TTL_MS = 7 * 24 * 60 * 60 * 1e3;
6155
+ function storePath2() {
6156
+ return join12(appDataDir(), "slack-outbound-watch.json");
6157
+ }
6158
+ function watchId(teamId, channelId, ts) {
6159
+ return `${teamId}:${channelId}:${ts}`;
6160
+ }
6161
+ function slackArchiveUrl(channelId, ts) {
6162
+ return `https://slack.com/archives/${channelId}/p${ts.replace(".", "")}`;
6163
+ }
6164
+ function readStore2() {
6165
+ const path = storePath2();
6166
+ if (!existsSync14(path)) return [];
6167
+ try {
6168
+ const parsed = isSecureFileEncrypted(path) ? readSecureJson(path) : JSON.parse(readFileSync9(path, "utf8"));
6169
+ return Array.isArray(parsed?.watches) ? parsed.watches : [];
6170
+ } catch {
6171
+ return [];
6172
+ }
6173
+ }
6174
+ function writeStore(watches) {
6175
+ writePrivateFile(storePath2(), `${JSON.stringify({ watches }, null, 2)}
6176
+ `);
6177
+ return watches;
6178
+ }
6179
+ function pruneWatches(watches, nowMs = Date.now()) {
6180
+ const cutoff = nowMs - WATCH_TTL_MS;
6181
+ const kept = watches.filter((w) => {
6182
+ const posted = Date.parse(w.postedAt);
6183
+ return Number.isFinite(posted) ? posted >= cutoff : true;
6184
+ });
6185
+ if (kept.length <= MAX_WATCHES) return kept;
6186
+ return kept.slice().sort((a, b) => b.postedAt.localeCompare(a.postedAt)).slice(0, MAX_WATCHES);
6187
+ }
6188
+ function recordSlackOutboundWatch(input) {
6189
+ const ts = input.ts.trim();
6190
+ const channelId = input.channelId.trim();
6191
+ const teamId = input.teamId.trim();
6192
+ if (!ts || !channelId || !teamId) return null;
6193
+ const owner = input.ownerUserId?.trim();
6194
+ const toUser = input.toUserId?.trim();
6195
+ if (toUser && owner && toUser === owner) return null;
6196
+ const id = watchId(teamId, channelId, ts);
6197
+ const next = {
6198
+ id,
6199
+ teamId,
6200
+ channelId,
6201
+ ts,
6202
+ threadTs: input.threadTs?.trim() || ts,
6203
+ kind: input.kind === "dm" ? "dm" : "channel",
6204
+ toUserId: toUser,
6205
+ toLabel: input.toLabel.trim() || toUser || channelId,
6206
+ ownerUserId: owner,
6207
+ postedAt: (/* @__PURE__ */ new Date()).toISOString(),
6208
+ lastSeenTs: ts,
6209
+ unread: false,
6210
+ permalink: slackArchiveUrl(channelId, ts)
6211
+ };
6212
+ const watches = pruneWatches(readStore2().filter((w) => w.id !== id));
6213
+ watches.unshift(next);
6214
+ writeStore(pruneWatches(watches));
6215
+ return next;
6216
+ }
6217
+
6218
+ // src/mcp/slack-tools.ts
6219
+ function text(payload, isError = false) {
6220
+ return {
6221
+ content: [{ type: "text", text: JSON.stringify(payload, null, 2) }],
6222
+ ...isError ? { isError: true } : {}
6223
+ };
6224
+ }
6225
+ function fail(err) {
6226
+ return text(
6227
+ { error: err instanceof Error ? err.message : String(err) },
6228
+ true
6229
+ );
6230
+ }
6231
+ function registerSlackTools(server) {
6232
+ server.tool(
6233
+ "list_teams",
6234
+ "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.",
6235
+ {},
6236
+ async () => {
6237
+ const teams = listSlackWorkspaces();
6238
+ if (teams.length === 0) {
6239
+ return text({
6240
+ teams: [],
6241
+ hint: "No Slack workspaces connected. Add one in Account \u2192 Slack workspaces (paste xoxb-/xoxp- or browser sign-in)."
6242
+ });
6243
+ }
6244
+ return text({
6245
+ teams: teams.map((t) => ({
6246
+ team_id: t.team_id,
6247
+ name: t.team_name,
6248
+ search: t.has_user_token
6249
+ }))
6250
+ });
6251
+ }
6252
+ );
6253
+ server.tool(
6254
+ "slack_list_channels",
6255
+ "List channels in a connected Slack workspace. Pass team_id from list_teams.",
6256
+ {
6257
+ team_id: z.string(),
6258
+ limit: z.number().optional()
6259
+ },
6260
+ async ({ team_id, limit }) => {
6261
+ try {
6262
+ const ws = requireSlackWorkspace(team_id);
6263
+ const token = slackTokenFor(ws, "read");
6264
+ const data = await slackApi(token, "conversations.list", {
6265
+ types: "public_channel,private_channel",
6266
+ exclude_archived: true,
6267
+ limit: Math.min(200, Math.max(1, limit ?? 100))
6268
+ });
6269
+ return text({
6270
+ team_id: ws.team_id,
6271
+ team_name: ws.team_name,
6272
+ channels: (data.channels ?? []).map((c) => ({
6273
+ id: c.id,
6274
+ name: c.name,
6275
+ private: Boolean(c.is_private),
6276
+ member: Boolean(c.is_member)
6277
+ }))
6278
+ });
6279
+ } catch (err) {
6280
+ return fail(err);
6281
+ }
6282
+ }
6283
+ );
6284
+ server.tool(
6285
+ "slack_list_users",
6286
+ "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.",
6287
+ {
6288
+ team_id: z.string(),
6289
+ query: z.string().optional(),
6290
+ limit: z.number().optional()
6291
+ },
6292
+ async ({ team_id, query, limit }) => {
6293
+ try {
6294
+ const ws = requireSlackWorkspace(team_id);
6295
+ const token = slackTokenFor(ws, "read");
6296
+ const users = await listSlackUsers(token, { query, limit });
6297
+ return text({
6298
+ team_id: ws.team_id,
6299
+ team_name: ws.team_name,
6300
+ query: query?.trim() || void 0,
6301
+ users
6302
+ });
6303
+ } catch (err) {
6304
+ return fail(err);
6305
+ }
6306
+ }
6307
+ );
6308
+ server.tool(
6309
+ "slack_search",
6310
+ "Search messages in a connected Slack workspace (needs a user token). Pass team_id from list_teams.",
6311
+ {
6312
+ team_id: z.string(),
6313
+ query: z.string(),
6314
+ count: z.number().optional()
6315
+ },
6316
+ async ({ team_id, query, count }) => {
6317
+ try {
6318
+ const ws = requireSlackWorkspace(team_id);
6319
+ const token = slackTokenFor(ws, "search");
6320
+ const data = await slackApi(token, "search.messages", {
6321
+ query,
6322
+ count: Math.min(50, Math.max(1, count ?? 10)),
6323
+ sort: "timestamp"
6324
+ });
6325
+ return text({
6326
+ team_id: ws.team_id,
6327
+ team_name: ws.team_name,
6328
+ query,
6329
+ matches: (data.messages?.matches ?? []).map((m) => ({
6330
+ channel: m.channel?.name || m.channel?.id,
6331
+ user: m.username || m.user,
6332
+ ts: m.ts,
6333
+ text: m.text,
6334
+ permalink: m.permalink
6335
+ }))
6336
+ });
6337
+ } catch (err) {
6338
+ return fail(err);
6339
+ }
6340
+ }
6341
+ );
6342
+ server.tool(
6343
+ "slack_read",
6344
+ "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.",
6345
+ {
6346
+ team_id: z.string(),
6347
+ channel: z.string(),
6348
+ ts: z.string().optional(),
6349
+ limit: z.number().optional()
6350
+ },
6351
+ async ({ team_id, channel, ts, limit }) => {
6352
+ try {
6353
+ const ws = requireSlackWorkspace(team_id);
6354
+ const token = slackTokenFor(ws, "read");
6355
+ const dest = await resolveSlackDestination(token, channel, { forRead: true });
6356
+ const n = Math.min(100, Math.max(1, limit ?? 20));
6357
+ if (ts?.trim()) {
6358
+ const data2 = await slackApi(token, "conversations.replies", {
6359
+ channel: dest.channelId,
6360
+ ts: ts.trim(),
6361
+ limit: n
6362
+ });
6363
+ return text({
6364
+ team_id: ws.team_id,
6365
+ channel: dest.channelId,
6366
+ label: dest.label,
6367
+ thread_ts: ts.trim(),
6368
+ messages: data2.messages ?? []
6369
+ });
6370
+ }
6371
+ const data = await slackApi(token, "conversations.history", {
6372
+ channel: dest.channelId,
6373
+ limit: n
6374
+ });
6375
+ return text({
6376
+ team_id: ws.team_id,
6377
+ channel: dest.channelId,
6378
+ label: dest.label,
6379
+ messages: data.messages ?? []
6380
+ });
6381
+ } catch (err) {
6382
+ return fail(err);
6383
+ }
6384
+ }
6385
+ );
6386
+ server.tool(
6387
+ "slack_post",
6388
+ "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.",
6389
+ {
6390
+ team_id: z.string(),
6391
+ channel: z.string().optional(),
6392
+ to: z.string().optional(),
6393
+ text: z.string(),
6394
+ github_url: z.string().optional(),
6395
+ thread_ts: z.string().optional()
6396
+ },
6397
+ async ({ team_id, channel, to, text: message, github_url, thread_ts }) => {
6398
+ try {
6399
+ const destRaw = (to ?? channel)?.trim();
6400
+ if (!destRaw) {
6401
+ return fail(new Error("slack_post needs `to` or `channel` (#name, @user, or Slack id)"));
6402
+ }
6403
+ const ws = requireSlackWorkspace(team_id);
6404
+ const token = slackTokenFor(ws, "write");
6405
+ const dest = await resolveSlackDestination(token, destRaw);
6406
+ const body = appendGithubLink(message, github_url);
6407
+ const data = await slackApi(
6408
+ token,
6409
+ "chat.postMessage",
6410
+ {
6411
+ channel: dest.channelId,
6412
+ text: body,
6413
+ thread_ts: thread_ts?.trim() || void 0,
6414
+ unfurl_links: true,
6415
+ unfurl_media: true
6416
+ }
6417
+ );
6418
+ const postedTs = data.ts?.trim();
6419
+ const postedChannel = (data.channel || dest.channelId).trim();
6420
+ if (postedTs && postedChannel) {
6421
+ try {
6422
+ recordSlackOutboundWatch({
6423
+ teamId: ws.team_id,
6424
+ channelId: postedChannel,
6425
+ ts: postedTs,
6426
+ threadTs: thread_ts?.trim() || postedTs,
6427
+ kind: dest.kind === "channel" ? "channel" : "dm",
6428
+ toUserId: dest.userId,
6429
+ toLabel: dest.label,
6430
+ ownerUserId: ws.user_id
6431
+ });
6432
+ } catch {
6433
+ }
6434
+ }
6435
+ return text({
6436
+ ok: true,
6437
+ team_id: ws.team_id,
6438
+ channel: postedChannel,
6439
+ label: dest.label,
6440
+ kind: dest.kind,
6441
+ user_id: dest.userId,
6442
+ ts: postedTs
6443
+ });
6444
+ } catch (err) {
6445
+ return fail(err);
6446
+ }
6447
+ }
6448
+ );
6449
+ }
6450
+
5698
6451
  // src/mcp/server.ts
5699
6452
  var MAX_ORCH_THREADS = 5;
5700
6453
  var CREATE_THREAD_TIMEOUT_MS = 9e4;
@@ -5718,7 +6471,7 @@ function withTimeout(promise, ms, label) {
5718
6471
  async function startMcpServer() {
5719
6472
  const orch = getOrchestrator();
5720
6473
  try {
5721
- const { maxConcurrentAgents } = await import("../app-settings-GJVZGU3M.js");
6474
+ const { maxConcurrentAgents } = await import("../app-settings-P42Z3VOB.js");
5722
6475
  orch.setMaxConcurrent(maxConcurrentAgents());
5723
6476
  } catch {
5724
6477
  }
@@ -5771,7 +6524,7 @@ async function startMcpServer() {
5771
6524
  server.tool(
5772
6525
  "get_thread",
5773
6526
  "Get a compact thread summary by id/ref",
5774
- { ref: z.string() },
6527
+ { ref: z2.string() },
5775
6528
  async ({ ref }) => {
5776
6529
  const t = orch.getThread(ref);
5777
6530
  if (!t) {
@@ -5800,14 +6553,14 @@ async function startMcpServer() {
5800
6553
  "present_artifact",
5801
6554
  "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.",
5802
6555
  {
5803
- title: z.string().describe("Short title shown in the artifact pane header"),
5804
- type: z.enum(["html", "svg", "markdown", "react"]).describe(
6556
+ title: z2.string().describe("Short title shown in the artifact pane header"),
6557
+ type: z2.enum(["html", "svg", "markdown", "react"]).describe(
5805
6558
  "Artifact kind \u2014 html opens an iframe preview; react transpiles and renders a default-exported component in a sandboxed iframe"
5806
6559
  ),
5807
- content: z.string().describe(
6560
+ content: z2.string().describe(
5808
6561
  "Full document body (complete HTML page, SVG markup, markdown, or a React component module with a default export)"
5809
6562
  ),
5810
- artifact_id: z.string().optional().describe("Stable id when updating the same artifact across turns")
6563
+ artifact_id: z2.string().optional().describe("Stable id when updating the same artifact across turns")
5811
6564
  },
5812
6565
  async ({ title, type, artifact_id }) => {
5813
6566
  const id = artifact_id?.trim() || `artifact_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 8)}`;
@@ -5825,15 +6578,15 @@ async function startMcpServer() {
5825
6578
  "ask_user",
5826
6579
  "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.",
5827
6580
  {
5828
- questions: z.array(
5829
- z.object({
5830
- question: z.string().describe("Full question text ending with ?"),
5831
- header: z.string().max(24).optional().describe("Short label shown above the question"),
5832
- multiSelect: z.boolean().optional().describe("Allow selecting multiple options"),
5833
- options: z.array(
5834
- z.object({
5835
- label: z.string(),
5836
- description: z.string().optional().describe("What this option means / when to choose it (strongly preferred)")
6581
+ questions: z2.array(
6582
+ z2.object({
6583
+ question: z2.string().describe("Full question text ending with ?"),
6584
+ header: z2.string().max(24).optional().describe("Short label shown above the question"),
6585
+ multiSelect: z2.boolean().optional().describe("Allow selecting multiple options"),
6586
+ options: z2.array(
6587
+ z2.object({
6588
+ label: z2.string(),
6589
+ description: z2.string().optional().describe("What this option means / when to choose it (strongly preferred)")
5837
6590
  })
5838
6591
  ).min(2).max(6).describe("2\u20136 choices (Sideboard also offers Other)")
5839
6592
  })
@@ -5852,9 +6605,9 @@ async function startMcpServer() {
5852
6605
  "present_plan",
5853
6606
  "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.",
5854
6607
  {
5855
- title: z.string().optional().describe("Short plan title (defaults to Plan)"),
5856
- content: z.string().min(1).describe("Full plan markdown (headings, steps, risks, open questions)"),
5857
- thread_id: z.string().optional().describe("Sideboard thread id when cwd is not the worktree")
6608
+ title: z2.string().optional().describe("Short plan title (defaults to Plan)"),
6609
+ content: z2.string().min(1).describe("Full plan markdown (headings, steps, risks, open questions)"),
6610
+ thread_id: z2.string().optional().describe("Sideboard thread id when cwd is not the worktree")
5858
6611
  },
5859
6612
  async ({ title, content, thread_id }) => {
5860
6613
  const { writePlanFile: writePlanFile2 } = await import("../plan-file-PHVKUAEE.js");
@@ -5875,17 +6628,17 @@ async function startMcpServer() {
5875
6628
  );
5876
6629
  server.tool(
5877
6630
  "present_schema",
5878
- "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.",
6631
+ "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.",
5879
6632
  {
5880
- title: z.string().describe("Pane title"),
5881
- mode: z.enum(["table", "form"]).optional().describe("table = list/filter records; form = edit one record"),
5882
- datasource: z.enum(["brightsy", "inline"]).optional().describe("brightsy resolves via login; inline uses embedded resource/records"),
5883
- resource_id: z.string().optional().describe("Brightsy record type UUID (or generic resource id)"),
5884
- record_id: z.string().optional().describe("Record id when opening form mode"),
5885
- resource: z.record(z.unknown()).optional().describe("Inline { id, title, schema, schemaUi?, slug? }"),
5886
- record: z.record(z.unknown()).optional().describe("Inline record { id, data, published_at? }"),
5887
- records: z.array(z.record(z.unknown())).optional().describe("Inline records for table mode"),
5888
- pane_id: z.string().optional().describe("Stable pane id across updates")
6633
+ title: z2.string().describe("Pane title"),
6634
+ mode: z2.enum(["table", "form"]).optional().describe("table = list/filter records; form = edit one record"),
6635
+ datasource: z2.enum(["brightsy", "inline"]).optional().describe("brightsy resolves via login; inline uses embedded resource/records"),
6636
+ resource_id: z2.string().optional().describe("Brightsy record type UUID (or generic resource id)"),
6637
+ record_id: z2.string().optional().describe("Record id when opening form mode"),
6638
+ resource: z2.record(z2.unknown()).optional().describe("Inline { id, title, schema, schemaUi?, slug? }"),
6639
+ record: z2.record(z2.unknown()).optional().describe("Inline record { id, data, published_at? }"),
6640
+ records: z2.array(z2.record(z2.unknown())).optional().describe("Inline records for table mode"),
6641
+ pane_id: z2.string().optional().describe("Stable pane id across updates")
5889
6642
  },
5890
6643
  async (args) => {
5891
6644
  const id = args.pane_id?.trim() || `schema_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 8)}`;
@@ -5906,10 +6659,10 @@ async function startMcpServer() {
5906
6659
  "present_files",
5907
6660
  "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.",
5908
6661
  {
5909
- title: z.string().optional().describe("Pane title (default: Files)"),
5910
- datasource: z.enum(["brightsy", "memory"]).optional().describe("brightsy = account storage via login; memory = session demo store"),
5911
- path: z.string().optional().describe("Initial folder path (e.g. public)"),
5912
- pane_id: z.string().optional().describe("Stable pane id across updates")
6662
+ title: z2.string().optional().describe("Pane title (default: Files)"),
6663
+ datasource: z2.enum(["brightsy", "memory"]).optional().describe("brightsy = account storage via login; memory = session demo store"),
6664
+ path: z2.string().optional().describe("Initial folder path (e.g. public)"),
6665
+ pane_id: z2.string().optional().describe("Stable pane id across updates")
5913
6666
  },
5914
6667
  async (args) => {
5915
6668
  const id = args.pane_id?.trim() || `files_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 8)}`;
@@ -5924,23 +6677,62 @@ async function startMcpServer() {
5924
6677
  return { content: [{ type: "text", text: JSON.stringify(payload) }] };
5925
6678
  }
5926
6679
  );
6680
+ registerSlackTools(server);
5927
6681
  if (sideboardMcpProfile() !== "worktree") {
5928
- const { resolveNewThreadOptions: resolveNewThreadOptions2, resolveThreadDefaults } = await import("../app-settings-GJVZGU3M.js");
6682
+ const { getCaffeinateHold, setCaffeinateHold } = await import("../caffeinate-hold-BEJIYPJ7.js");
6683
+ const { resolveNewThreadOptions: resolveNewThreadOptions2, resolveThreadDefaults } = await import("../app-settings-P42Z3VOB.js");
5929
6684
  const accountDefaults = resolveThreadDefaults();
5930
6685
  const accountDefaultsHint = `Account defaults: agent=${accountDefaults.agent}, model=${accountDefaults.model?.trim() || "Auto"}, effort=${accountDefaults.effort}`;
6686
+ server.tool(
6687
+ "set_caffeinate",
6688
+ "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.",
6689
+ {
6690
+ enabled: z2.boolean().describe("true = hold caffeinate on; false = release and let the Mac sleep")
6691
+ },
6692
+ async ({ enabled }) => {
6693
+ const state = setCaffeinateHold(enabled);
6694
+ if (enabled && !state.held) {
6695
+ return {
6696
+ content: [
6697
+ {
6698
+ type: "text",
6699
+ text: JSON.stringify({
6700
+ ...state,
6701
+ ok: false,
6702
+ message: state.platform === "darwin" ? "Could not start caffeinate." : "Caffeinate is macOS only."
6703
+ })
6704
+ }
6705
+ ],
6706
+ isError: true
6707
+ };
6708
+ }
6709
+ return {
6710
+ content: [
6711
+ {
6712
+ type: "text",
6713
+ text: JSON.stringify({
6714
+ ...getCaffeinateHold(),
6715
+ ok: true,
6716
+ 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)."
6717
+ })
6718
+ }
6719
+ ]
6720
+ };
6721
+ }
6722
+ );
5931
6723
  server.tool(
5932
6724
  "create_thread",
5933
6725
  `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.`,
5934
6726
  {
5935
- sourceType: z.enum(["branch", "pr", "ticket"]),
5936
- sourceRef: z.string(),
5937
- agent: z.enum(["claude", "codex", "opencode", "brightsy", "cursor"]).optional().describe(`Omit to use Account default agent (${accountDefaults.agent})`),
5938
- model: z.string().nullable().optional().describe(
6727
+ sourceType: z2.enum(["branch", "pr", "ticket"]),
6728
+ sourceRef: z2.string(),
6729
+ agent: z2.enum(["claude", "codex", "opencode", "brightsy", "cursor"]).optional().describe(`Omit to use Account default agent (${accountDefaults.agent})`),
6730
+ model: z2.string().nullable().optional().describe(
5939
6731
  `Usually omit to use Account default model (${accountDefaults.model?.trim() || "Auto"}). Pass null only to force Auto / agent-default.`
5940
6732
  ),
5941
- repoPath: z.string(),
5942
- title: z.string().optional(),
5943
- parentThreadId: z.string().optional().describe(
6733
+ repoPath: z2.string(),
6734
+ title: z2.string().optional(),
6735
+ parentThreadId: z2.string().optional().describe(
5944
6736
  "Orchestration: omit (preferred) or pass YOUR chat id from the turn reminder / AGENTS.md. Do not invent uuids."
5945
6737
  )
5946
6738
  },
@@ -6047,9 +6839,9 @@ async function startMcpServer() {
6047
6839
  "send_to_thread",
6048
6840
  "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.",
6049
6841
  {
6050
- ref: z.string(),
6051
- prompt: z.string(),
6052
- force_stop: z.boolean().optional()
6842
+ ref: z2.string(),
6843
+ prompt: z2.string(),
6844
+ force_stop: z2.boolean().optional()
6053
6845
  },
6054
6846
  async ({ ref, prompt, force_stop }) => {
6055
6847
  if (force_stop) {
@@ -6078,8 +6870,8 @@ async function startMcpServer() {
6078
6870
  "wait_for_turn",
6079
6871
  "Block until the thread finishes its current/queued turn (avoids polling). Use after send_to_thread to read the agent reply.",
6080
6872
  {
6081
- ref: z.string(),
6082
- timeoutMs: z.number().optional()
6873
+ ref: z2.string(),
6874
+ timeoutMs: z2.number().optional()
6083
6875
  },
6084
6876
  async ({ ref, timeoutMs }) => {
6085
6877
  const thread = await orch.waitForTurn(ref, timeoutMs ?? 6e5);
@@ -6101,7 +6893,7 @@ async function startMcpServer() {
6101
6893
  server.tool(
6102
6894
  "get_turn_result",
6103
6895
  "Final assistant message only (not full transcript)",
6104
- { ref: z.string() },
6896
+ { ref: z2.string() },
6105
6897
  async ({ ref }) => {
6106
6898
  const result = orch.getTurnResult(ref);
6107
6899
  return { content: [{ type: "text", text: JSON.stringify(result) }] };
@@ -6111,8 +6903,8 @@ async function startMcpServer() {
6111
6903
  "stop_thread",
6112
6904
  "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.",
6113
6905
  {
6114
- ref: z.string(),
6115
- force: z.boolean().optional()
6906
+ ref: z2.string(),
6907
+ force: z2.boolean().optional()
6116
6908
  },
6117
6909
  async ({ ref, force }) => {
6118
6910
  const t = orch.getThread(ref);
@@ -6141,8 +6933,8 @@ async function startMcpServer() {
6141
6933
  );
6142
6934
  server.tool(
6143
6935
  "archive_thread",
6144
- "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.",
6145
- { ref: z.string() },
6936
+ "Archive a thread (stops agent/dev, runs archive script, removes worktree when last chat tab). Coordinators open PRs only by asking the worktree agent.",
6937
+ { ref: z2.string() },
6146
6938
  async ({ ref }) => {
6147
6939
  const t = orch.getThread(ref);
6148
6940
  if (!t) {
@@ -6175,7 +6967,7 @@ async function startMcpServer() {
6175
6967
  server.tool(
6176
6968
  "restore_thread",
6177
6969
  "Restore an archived thread (recreates worktree from branch when needed)",
6178
- { ref: z.string() },
6970
+ { ref: z2.string() },
6179
6971
  async ({ ref }) => {
6180
6972
  try {
6181
6973
  const restored = await orch.restore(ref);
@@ -6201,8 +6993,8 @@ async function startMcpServer() {
6201
6993
  "get_diff",
6202
6994
  "Compact diff summary (capped hunks, paginated)",
6203
6995
  {
6204
- ref: z.string(),
6205
- maxFiles: z.number().optional()
6996
+ ref: z2.string(),
6997
+ maxFiles: z2.number().optional()
6206
6998
  },
6207
6999
  async ({ ref, maxFiles }) => {
6208
7000
  const summary = await orch.diffSummary(ref);
@@ -6222,7 +7014,7 @@ async function startMcpServer() {
6222
7014
  server.tool(
6223
7015
  "request_review",
6224
7016
  '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.',
6225
- { ref: z.string().describe("Worktree thread id/ref to review") },
7017
+ { ref: z2.string().describe("Worktree thread id/ref to review") },
6226
7018
  async ({ ref }) => {
6227
7019
  try {
6228
7020
  const tab = await orch.requestReview(ref);
@@ -6247,7 +7039,7 @@ async function startMcpServer() {
6247
7039
  }
6248
7040
  }
6249
7041
  );
6250
- const agentEnum = z.enum(["claude", "codex", "opencode", "brightsy", "cursor"]);
7042
+ const agentEnum = z2.enum(["claude", "codex", "opencode", "brightsy", "cursor"]);
6251
7043
  server.tool(
6252
7044
  "list_models",
6253
7045
  "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.",
@@ -6270,11 +7062,11 @@ async function startMcpServer() {
6270
7062
  "fork_worktree",
6271
7063
  "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.",
6272
7064
  {
6273
- ref: z.string().describe("Worktree thread id/ref to fork"),
6274
- through_index: z.number().optional().describe("Inclusive message index to include in the transcript (default: all)"),
7065
+ ref: z2.string().describe("Worktree thread id/ref to fork"),
7066
+ through_index: z2.number().optional().describe("Inclusive message index to include in the transcript (default: all)"),
6275
7067
  agent: agentEnum.optional().describe("Agent for the forked chat (default: same as source)"),
6276
- model: z.string().nullable().optional().describe("Usually omit (Auto). Only set from list_models when you need a specific model"),
6277
- title: z.string().optional()
7068
+ model: z2.string().nullable().optional().describe("Usually omit (Auto). Only set from list_models when you need a specific model"),
7069
+ title: z2.string().optional()
6278
7070
  },
6279
7071
  async ({ ref, through_index, agent, model, title }) => {
6280
7072
  try {
@@ -6313,13 +7105,13 @@ async function startMcpServer() {
6313
7105
  );
6314
7106
  server.tool(
6315
7107
  "fork_chat",
6316
- "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.",
7108
+ "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.",
6317
7109
  {
6318
- ref: z.string().describe("Thread id/ref to fork (worktree agent or orchestration chat)"),
6319
- through_index: z.number().optional().describe("Inclusive message index to include in the transcript (default: all)"),
7110
+ ref: z2.string().describe("Thread id/ref to fork (worktree agent or orchestration chat)"),
7111
+ through_index: z2.number().optional().describe("Inclusive message index to include in the transcript (default: all)"),
6320
7112
  agent: agentEnum.optional().describe("Agent for the forked chat (default: same as source)"),
6321
- model: z.string().nullable().optional().describe("Usually omit (Auto). Only set from list_models when you need a specific model"),
6322
- title: z.string().optional()
7113
+ model: z2.string().nullable().optional().describe("Usually omit (Auto). Only set from list_models when you need a specific model"),
7114
+ title: z2.string().optional()
6323
7115
  },
6324
7116
  async ({ ref, through_index, agent, model, title }) => {
6325
7117
  try {
@@ -6365,8 +7157,8 @@ async function startMcpServer() {
6365
7157
  "run_dev_script",
6366
7158
  "Start a .sideboard/.conductor run script for a thread (default script if name omitted); returns port",
6367
7159
  {
6368
- ref: z.string(),
6369
- name: z.string().optional()
7160
+ ref: z2.string(),
7161
+ name: z2.string().optional()
6370
7162
  },
6371
7163
  async ({ ref, name }) => {
6372
7164
  const result = await orch.startDev(ref, name);
@@ -6388,7 +7180,7 @@ async function startMcpServer() {
6388
7180
  server.tool(
6389
7181
  "list_run_scripts",
6390
7182
  "List named run scripts available for a thread",
6391
- { ref: z.string() },
7183
+ { ref: z2.string() },
6392
7184
  async ({ ref }) => {
6393
7185
  const scripts = orch.listThreadRunScripts(ref);
6394
7186
  const active = orch.getActiveRuns(ref);
@@ -6406,8 +7198,8 @@ async function startMcpServer() {
6406
7198
  "stop_dev_script",
6407
7199
  "Stop a running script for a thread (all scripts if name omitted)",
6408
7200
  {
6409
- ref: z.string(),
6410
- name: z.string().optional()
7201
+ ref: z2.string(),
7202
+ name: z2.string().optional()
6411
7203
  },
6412
7204
  async ({ ref, name }) => {
6413
7205
  orch.stopDev(ref, name);
@@ -6417,7 +7209,7 @@ async function startMcpServer() {
6417
7209
  server.tool(
6418
7210
  "run_setup",
6419
7211
  "Re-run workspace setup (Sideboard/Conductor settings or .cursor/worktrees.json)",
6420
- { ref: z.string() },
7212
+ { ref: z2.string() },
6421
7213
  async ({ ref }) => {
6422
7214
  const result = await orch.runSetup(ref);
6423
7215
  return {
@@ -6428,7 +7220,7 @@ async function startMcpServer() {
6428
7220
  server.tool(
6429
7221
  "add_workspace",
6430
7222
  "Register a git repo as a Sideboard workspace",
6431
- { repoPath: z.string() },
7223
+ { repoPath: z2.string() },
6432
7224
  async ({ repoPath }) => {
6433
7225
  const ws = await orch.addWorkspace(repoPath);
6434
7226
  return { content: [{ type: "text", text: JSON.stringify(ws) }] };
@@ -6437,7 +7229,7 @@ async function startMcpServer() {
6437
7229
  server.tool(
6438
7230
  "remove_workspace",
6439
7231
  "Unregister a Sideboard workspace (does not archive threads)",
6440
- { repoPath: z.string() },
7232
+ { repoPath: z2.string() },
6441
7233
  async ({ repoPath }) => {
6442
7234
  orch.removeWorkspace(repoPath);
6443
7235
  return { content: [{ type: "text", text: "ok" }] };
@@ -6447,12 +7239,12 @@ async function startMcpServer() {
6447
7239
  "fanout",
6448
7240
  "Best-of-n: create one thread per agent with the same prompt (parallel attempts)",
6449
7241
  {
6450
- prompt: z.string(),
6451
- agents: z.array(z.enum(["claude", "codex", "opencode", "brightsy", "cursor"])),
6452
- repoPath: z.string(),
6453
- sourceType: z.enum(["branch", "pr", "ticket"]).optional(),
6454
- sourceRef: z.string().optional(),
6455
- title: z.string().optional()
7242
+ prompt: z2.string(),
7243
+ agents: z2.array(z2.enum(["claude", "codex", "opencode", "brightsy", "cursor"])),
7244
+ repoPath: z2.string(),
7245
+ sourceType: z2.enum(["branch", "pr", "ticket"]).optional(),
7246
+ sourceRef: z2.string().optional(),
7247
+ title: z2.string().optional()
6456
7248
  },
6457
7249
  async (args) => {
6458
7250
  const threads = await orch.bestOfN(args);
@@ -6479,8 +7271,8 @@ async function startMcpServer() {
6479
7271
  "list_branches",
6480
7272
  "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).",
6481
7273
  {
6482
- repoPath: z.string(),
6483
- unmergedOnly: z.boolean().optional()
7274
+ repoPath: z2.string(),
7275
+ unmergedOnly: z2.boolean().optional()
6484
7276
  },
6485
7277
  async ({ repoPath, unmergedOnly }) => {
6486
7278
  const root = await resolveRepoRoot(repoPath);
@@ -6500,7 +7292,7 @@ async function startMcpServer() {
6500
7292
  server.tool(
6501
7293
  "list_prs",
6502
7294
  "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.",
6503
- { repoPath: z.string() },
7295
+ { repoPath: z2.string() },
6504
7296
  async ({ repoPath }) => {
6505
7297
  const root = await resolveRepoRoot(repoPath);
6506
7298
  const prs = await listPrs(root);
@@ -6519,7 +7311,7 @@ async function startMcpServer() {
6519
7311
  server.tool(
6520
7312
  "get_pr_stack",
6521
7313
  "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.",
6522
- { ref: z.string() },
7314
+ { ref: z2.string() },
6523
7315
  async ({ ref }) => {
6524
7316
  const stack = await orch.getPrStack(ref);
6525
7317
  return {
@@ -6531,8 +7323,8 @@ async function startMcpServer() {
6531
7323
  "open_pr_stack_layers",
6532
7324
  "Materialize one worktree+thread per stack layer (or a single 1-based layer). Pass a thread ref already on the stack.",
6533
7325
  {
6534
- ref: z.string(),
6535
- layer: z.number().int().positive().optional()
7326
+ ref: z2.string(),
7327
+ layer: z2.number().int().positive().optional()
6536
7328
  },
6537
7329
  async ({ ref, layer }) => {
6538
7330
  const result = await orch.openPrStackLayers(ref, { layer });
@@ -6566,9 +7358,9 @@ async function startMcpServer() {
6566
7358
  "add_stack_layer",
6567
7359
  "Add a branch on top of the current stack (`gh stack add`) and open a worktree+thread for it.",
6568
7360
  {
6569
- ref: z.string(),
6570
- branchName: z.string(),
6571
- title: z.string().optional()
7361
+ ref: z2.string(),
7362
+ branchName: z2.string(),
7363
+ title: z2.string().optional()
6572
7364
  },
6573
7365
  async ({ ref, branchName, title }) => {
6574
7366
  const result = await orch.addStackLayer(ref, branchName, { title });
@@ -6597,11 +7389,11 @@ async function startMcpServer() {
6597
7389
  "create_pr_stack",
6598
7390
  "Create a new GitHub PR stack with one Sideboard worktree per layer (bottom\u2192top branch names). Requires `gh extension install github/gh-stack`.",
6599
7391
  {
6600
- repoPath: z.string(),
6601
- branches: z.array(z.string()).min(1),
6602
- agent: z.enum(["claude", "codex", "opencode", "brightsy", "cursor"]),
6603
- base: z.string().optional(),
6604
- title: z.string().optional()
7392
+ repoPath: z2.string(),
7393
+ branches: z2.array(z2.string()).min(1),
7394
+ agent: z2.enum(["claude", "codex", "opencode", "brightsy", "cursor"]),
7395
+ base: z2.string().optional(),
7396
+ title: z2.string().optional()
6605
7397
  },
6606
7398
  async (args) => {
6607
7399
  const result = await orch.createPrStack({
@@ -6639,7 +7431,7 @@ async function startMcpServer() {
6639
7431
  server.tool(
6640
7432
  "list_issues",
6641
7433
  "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.",
6642
- { repoPath: z.string() },
7434
+ { repoPath: z2.string() },
6643
7435
  async ({ repoPath }) => {
6644
7436
  const root = await resolveRepoRoot(repoPath);
6645
7437
  const result = await listIssues(root);
@@ -6650,8 +7442,8 @@ async function startMcpServer() {
6650
7442
  "list_linear_issues",
6651
7443
  "Deprecated: prefer list_issues. Lists assigned Linear issues via the chosen agent MCP connector",
6652
7444
  {
6653
- agent: z.enum(["claude", "codex", "opencode"]),
6654
- repoPath: z.string()
7445
+ agent: z2.enum(["claude", "codex", "opencode"]),
7446
+ repoPath: z2.string()
6655
7447
  },
6656
7448
  async ({ agent, repoPath }) => {
6657
7449
  const issues = await listLinearIssues(agent, repoPath);