@sideboard-ai/core 0.1.74 → 0.1.77

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 (29) hide show
  1. package/dist/{agents-2S2JVLZ6.js → agents-LKUHPPEQ.js} +3 -3
  2. package/dist/{agents-H7M5CQY2.js → agents-XJV6J3K2.js} +3 -3
  3. package/dist/{caffeinate-hold-KLC6SABD.js → caffeinate-hold-EAZNWJBE.js} +5 -1
  4. package/dist/caffeinate-hold-OHAUWOA6.js +20 -0
  5. package/dist/{chunk-KBKPVKYZ.js → chunk-2FJI5JXX.js} +2 -2
  6. package/dist/{chunk-GJ2LZQJI.js → chunk-2ZIPJRJE.js} +1 -1
  7. package/dist/{chunk-DG3S2UXP.js → chunk-75U65MBE.js} +1 -1
  8. package/dist/{chunk-HCWAIEBU.js → chunk-EEKSZTMU.js} +3 -2
  9. package/dist/{chunk-2NKSHIRI.js → chunk-GWTLKXXP.js} +1 -1
  10. package/dist/chunk-MFF2RE3I.js +168 -0
  11. package/dist/{chunk-5KJMNNCB.js → chunk-QD37IILI.js} +2 -2
  12. package/dist/chunk-RV6DBEDQ.js +170 -0
  13. package/dist/{chunk-77W62MTX.js → chunk-VERLUKN2.js} +1 -1
  14. package/dist/{chunk-XA2FQJTN.js → chunk-XJNCWYFR.js} +3 -2
  15. package/dist/{coordinator-prompt-3XXSG7M2.js → coordinator-prompt-LLFJUO2R.js} +1 -1
  16. package/dist/{coordinator-prompt-RTUCCPCI.js → coordinator-prompt-MRCMMRSF.js} +1 -1
  17. package/dist/{global-workspace-PJU6HISJ.js → global-workspace-TWHMYLTZ.js} +2 -2
  18. package/dist/{global-workspace-CSIS62Z4.js → global-workspace-YNMNO4CL.js} +2 -2
  19. package/dist/index.cjs +1050 -438
  20. package/dist/index.d.cts +144 -21
  21. package/dist/index.d.ts +144 -21
  22. package/dist/index.js +926 -389
  23. package/dist/mcp/run-stdio.cjs +709 -276
  24. package/dist/mcp/run-stdio.js +543 -171
  25. package/dist/{workspaces-L4TXMUNM.js → workspaces-GQ4XKBD3.js} +3 -3
  26. package/dist/{workspaces-W72KZL4B.js → workspaces-O3U5BENH.js} +3 -3
  27. package/package.json +1 -1
  28. package/dist/caffeinate-hold-BEJIYPJ7.js +0 -107
  29. package/dist/chunk-JAWEDVVA.js +0 -106
@@ -16,19 +16,22 @@ import {
16
16
  resolveQuotaFallbackAgent,
17
17
  sideboardMcpProfile,
18
18
  summarizeTurnStderr
19
- } from "../chunk-KBKPVKYZ.js";
19
+ } from "../chunk-2FJI5JXX.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-GJ2LZQJI.js";
26
+ } from "../chunk-2ZIPJRJE.js";
27
27
  import {
28
28
  extractPresentedPlan,
29
29
  readPlanFile,
30
30
  writePlanFile
31
31
  } from "../chunk-N5PM7HGQ.js";
32
+ import {
33
+ releaseCaffeinateHoldForThread
34
+ } from "../chunk-RV6DBEDQ.js";
32
35
  import {
33
36
  GLOBAL_WORKSPACE_ID,
34
37
  assertOrchestratorCapableAgent,
@@ -38,14 +41,14 @@ import {
38
41
  isGlobalThread,
39
42
  isOrchestratorThread,
40
43
  orchestratorSessionPoisonedByBuiltins
41
- } from "../chunk-2NKSHIRI.js";
44
+ } from "../chunk-GWTLKXXP.js";
42
45
  import {
43
46
  SLACK_REPLY_FORMATTING,
44
47
  coordinatorSystemPrompt,
45
48
  coordinatorTurnReminder,
46
49
  enrichWorkspacesWithGithub,
47
50
  ensureGlobalCoordinatorCwd
48
- } from "../chunk-XA2FQJTN.js";
51
+ } from "../chunk-XJNCWYFR.js";
49
52
  import {
50
53
  addPrStackLayer,
51
54
  allocateTeamName,
@@ -140,7 +143,7 @@ import {
140
143
  // src/mcp/server.ts
141
144
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
142
145
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
143
- import { z as z2 } from "zod";
146
+ import { z as z3 } from "zod";
144
147
  import { basename as basename4 } from "path";
145
148
 
146
149
  // src/orchestrator/orchestrator.ts
@@ -341,8 +344,8 @@ ${input.permalink}` : "";
341
344
 
342
345
  ${body}${link}`;
343
346
  }
344
- function isSlackExternalReplyPrompt(text2) {
345
- return text2.startsWith("Slack reply from ") && text2.includes("not a command");
347
+ function isSlackExternalReplyPrompt(text3) {
348
+ return text3.startsWith("Slack reply from ") && text3.includes("not a command");
346
349
  }
347
350
  function pendingSlackExternalReplies(messages) {
348
351
  let i = messages.length - 1;
@@ -366,9 +369,9 @@ function formatSlackRepliesForTurn(replies) {
366
369
  function listSlackOutboundWatches() {
367
370
  return pruneWatches(readStore3());
368
371
  }
369
- function formatOwnerSlackFyi(userName, text2) {
372
+ function formatOwnerSlackFyi(userName, text3) {
370
373
  const who = userName.trim() || "Someone";
371
- const body = text2.trim() || "(no text)";
374
+ const body = text3.trim() || "(no text)";
372
375
  return `${who} replied in Slack:
373
376
  ${body}`;
374
377
  }
@@ -383,7 +386,7 @@ async function relayExternalReply(opts) {
383
386
  const thread = readThread(threadId);
384
387
  if (!thread || thread.status === "archived") return true;
385
388
  try {
386
- const text2 = formatSlackExternalReplyPrompt({
389
+ const text3 = formatSlackExternalReplyPrompt({
387
390
  userName: opts.reply.userName,
388
391
  kind: opts.watch.kind,
389
392
  toLabel: opts.watch.toLabel,
@@ -392,7 +395,7 @@ async function relayExternalReply(opts) {
392
395
  });
393
396
  appendMessage(threadId, {
394
397
  role: "agent",
395
- text: text2,
398
+ text: text3,
396
399
  ts: (/* @__PURE__ */ new Date()).toISOString()
397
400
  });
398
401
  } catch {
@@ -576,7 +579,7 @@ async function refreshSlackReplyBadges(opts) {
576
579
  const messages = await fetchMessages(token, watch, opts?.fetchImpl);
577
580
  const replies = messages.filter((m) => isHumanReply(m, watch)).sort((a, b) => Number(a.ts) - Number(b.ts));
578
581
  if (replies.length === 0) continue;
579
- const injected = new Set(watch.injectedReplyTs ?? []);
582
+ const injected2 = new Set(watch.injectedReplyTs ?? []);
580
583
  const collected = [...watch.replies ?? []];
581
584
  let lastSeenTs = watch.lastSeenTs;
582
585
  let latestUser;
@@ -610,7 +613,7 @@ async function refreshSlackReplyBadges(opts) {
610
613
  latestName = replyUserName;
611
614
  latestText = reply.text;
612
615
  latestPermalink = permalink;
613
- if (injected.has(ts)) {
616
+ if (injected2.has(ts)) {
614
617
  lastSeenTs = ts;
615
618
  continue;
616
619
  }
@@ -621,7 +624,7 @@ async function refreshSlackReplyBadges(opts) {
621
624
  fetchImpl: opts?.fetchImpl
622
625
  });
623
626
  if (!delivered) break;
624
- injected.add(ts);
627
+ injected2.add(ts);
625
628
  lastSeenTs = ts;
626
629
  }
627
630
  watches[i] = {
@@ -633,7 +636,7 @@ async function refreshSlackReplyBadges(opts) {
633
636
  replyTs: lastSeenTs,
634
637
  replyPreview: latestText.slice(0, 140),
635
638
  permalink: latestPermalink,
636
- injectedReplyTs: [...injected],
639
+ injectedReplyTs: [...injected2],
637
640
  replies: collected.slice(-MAX_REPLIES_PER_WATCH)
638
641
  };
639
642
  changed = true;
@@ -723,9 +726,9 @@ function toolFilePath(input) {
723
726
  if (!input) return void 0;
724
727
  return str(input.file_path) ?? str(input.path) ?? str(input.filePath) ?? str(input.filename);
725
728
  }
726
- function countLines(text2) {
727
- if (!text2) return 0;
728
- return text2.split("\n").length;
729
+ function countLines(text3) {
730
+ if (!text3) return 0;
731
+ return text3.split("\n").length;
729
732
  }
730
733
  function diffFromInput(input) {
731
734
  if (!input) return {};
@@ -852,9 +855,9 @@ function applyAgentEvent(parts, event) {
852
855
  function partsToAssistantText(parts) {
853
856
  return parts.filter((p) => p.type === "text").map((p) => p.text).join("").trim();
854
857
  }
855
- function stripBrightsyNdjsonNoise(text2) {
856
- if (!text2 || !text2.includes('"type"')) return text2;
857
- let out = text2;
858
+ function stripBrightsyNdjsonNoise(text3) {
859
+ if (!text3 || !text3.includes('"type"')) return text3;
860
+ let out = text3;
858
861
  out = out.replace(
859
862
  /\{"type":"(?:tool_use|tool_result|tool|thinking|usage|done|error)"[\s\S]*?\}\s*(?=\{"type":"|$|(?=[A-Za-z*#]))/g,
860
863
  ""
@@ -913,9 +916,9 @@ async function spawnAgentTurn(thread, input, onEvent) {
913
916
  `Cannot spawn ${thread.agent}: thread ${thread.id} has no worktreePath`
914
917
  );
915
918
  }
916
- const { isGlobalThread: isGlobalThread2 } = await import("../global-workspace-PJU6HISJ.js");
919
+ const { isGlobalThread: isGlobalThread2 } = await import("../global-workspace-TWHMYLTZ.js");
917
920
  if (isGlobalThread2(thread)) {
918
- const { ensureGlobalCoordinatorCwd: ensureGlobalCoordinatorCwd2 } = await import("../coordinator-prompt-RTUCCPCI.js");
921
+ const { ensureGlobalCoordinatorCwd: ensureGlobalCoordinatorCwd2 } = await import("../coordinator-prompt-MRCMMRSF.js");
919
922
  ensureGlobalCoordinatorCwd2(
920
923
  isOrchestratorThread(thread) ? { orchestratorThreadId: thread.id } : void 0
921
924
  );
@@ -1002,8 +1005,8 @@ async function spawnAgentTurn(thread, input, onEvent) {
1002
1005
  onEvent({ type: "exit", data: exitCode });
1003
1006
  const finalized = finalizeParts(parts);
1004
1007
  const rawText = assistantText.trim() || partsToAssistantText(finalized);
1005
- const text2 = thread.agent === "brightsy" ? stripBrightsyNdjsonNoise(rawText) : rawText;
1006
- return { exitCode, sessionId, assistantText: text2, parts: finalized, usage };
1008
+ const text3 = thread.agent === "brightsy" ? stripBrightsyNdjsonNoise(rawText) : rawText;
1009
+ return { exitCode, sessionId, assistantText: text3, parts: finalized, usage };
1007
1010
  });
1008
1011
  return {
1009
1012
  pid: child.pid,
@@ -1756,7 +1759,7 @@ async function createThread(input, _onSetupLine) {
1756
1759
  return readThread(thread.id) ?? thread;
1757
1760
  }
1758
1761
  async function listLinearIssues(agent, repoPath) {
1759
- const { getAdapter: getAdapter2 } = await import("../agents-2S2JVLZ6.js");
1762
+ const { getAdapter: getAdapter2 } = await import("../agents-LKUHPPEQ.js");
1760
1763
  await requireAgent(agent, { requireLinear: true });
1761
1764
  const adapter = getAdapter2(agent);
1762
1765
  if (!adapter.listLinearIssues) {
@@ -1812,9 +1815,9 @@ async function tryClaudeSummary(transcript, opts) {
1812
1815
  { cwd: opts?.cwd, reject: false }
1813
1816
  );
1814
1817
  if (exitCode !== 0) return null;
1815
- const text2 = stdout.trim();
1816
- if (text2.length < 40) return null;
1817
- return text2;
1818
+ const text3 = stdout.trim();
1819
+ if (text3.length < 40) return null;
1820
+ return text3;
1818
1821
  } catch {
1819
1822
  return null;
1820
1823
  }
@@ -1863,9 +1866,9 @@ function extractiveSummary(transcript) {
1863
1866
  );
1864
1867
  return parts.join("\n");
1865
1868
  }
1866
- function clipSummary(text2) {
1867
- if (text2.length <= MAX_SUMMARY_CHARS) return text2;
1868
- return `${text2.slice(0, MAX_SUMMARY_CHARS)}
1869
+ function clipSummary(text3) {
1870
+ if (text3.length <= MAX_SUMMARY_CHARS) return text3;
1871
+ return `${text3.slice(0, MAX_SUMMARY_CHARS)}
1869
1872
 
1870
1873
  [\u2026summary truncated\u2026]`;
1871
1874
  }
@@ -2478,8 +2481,8 @@ function buildQuotaHandoffAttachment(from, limitText, fallbackAgent) {
2478
2481
  );
2479
2482
  const recent = from.messages.slice(-8).map((m) => {
2480
2483
  const role = m.role === "user" ? "User" : m.role === "agent" ? "Agent" : "Summary";
2481
- const text2 = m.text.trim().replace(/\s+/g, " ").slice(0, 280);
2482
- return text2 ? `- ${role}: ${text2}` : null;
2484
+ const text3 = m.text.trim().replace(/\s+/g, " ").slice(0, 280);
2485
+ return text3 ? `- ${role}: ${text3}` : null;
2483
2486
  }).filter(Boolean);
2484
2487
  const body = [
2485
2488
  `# Orchestration handoff`,
@@ -2575,13 +2578,13 @@ function resolveConductorCursorAgentId(workspacePath) {
2575
2578
  for (const hash of hashes) {
2576
2579
  const agentsFile = join9(CURSOR_SDK_STORE, hash, "agents.ndjson");
2577
2580
  if (!existsSync9(agentsFile)) continue;
2578
- let text2;
2581
+ let text3;
2579
2582
  try {
2580
- text2 = readFileSync6(agentsFile, "utf8");
2583
+ text3 = readFileSync6(agentsFile, "utf8");
2581
2584
  } catch {
2582
2585
  continue;
2583
2586
  }
2584
- for (const line of text2.split("\n")) {
2587
+ for (const line of text3.split("\n")) {
2585
2588
  const trimmed = line.trim();
2586
2589
  if (!trimmed) continue;
2587
2590
  try {
@@ -2620,7 +2623,7 @@ async function adoptThread(input) {
2620
2623
  messages: input.messages ?? []
2621
2624
  });
2622
2625
  writeThread(thread);
2623
- const { ensureWorkspace: ensureWorkspace2 } = await import("../workspaces-L4TXMUNM.js");
2626
+ const { ensureWorkspace: ensureWorkspace2 } = await import("../workspaces-GQ4XKBD3.js");
2624
2627
  await ensureWorkspace2(repoPath);
2625
2628
  return thread;
2626
2629
  }
@@ -3344,13 +3347,13 @@ function formatStat(files) {
3344
3347
  return `${n} file${n === 1 ? "" : "s"} changed, ${additions} insertions(+), ${deletions} deletions(-)`;
3345
3348
  }
3346
3349
  function emptyScopeStats() {
3347
- const z3 = emptyStat();
3350
+ const z4 = emptyStat();
3348
3351
  return {
3349
- commits: z3,
3350
- uncommitted: z3,
3351
- staged: z3,
3352
- unstaged: z3,
3353
- last_turn: z3
3352
+ commits: z4,
3353
+ uncommitted: z4,
3354
+ staged: z4,
3355
+ unstaged: z4,
3356
+ last_turn: z4
3354
3357
  };
3355
3358
  }
3356
3359
  function filesFromDiff(nameStatus, numstat, combinedDiff, maxHunk) {
@@ -4897,11 +4900,11 @@ var Orchestrator = class {
4897
4900
  return results;
4898
4901
  }
4899
4902
  /** Edit the text of a not-yet-started queued message. */
4900
- async editQueuedMessage(threadRef, index, text2) {
4903
+ async editQueuedMessage(threadRef, index, text3) {
4901
4904
  const thread = this.requireThread(threadRef);
4902
4905
  return withThreadLock(thread.id, async () => {
4903
4906
  const current = this.requireThread(thread.id);
4904
- const trimmed = text2.trim();
4907
+ const trimmed = text3.trim();
4905
4908
  if (!trimmed || index < 0 || index >= current.queue.length) {
4906
4909
  return current;
4907
4910
  }
@@ -5661,6 +5664,14 @@ var Orchestrator = class {
5661
5664
  throw new Error(`${action} is not available on the global coordinator`);
5662
5665
  }
5663
5666
  }
5667
+ /** MCP set_caffeinate is a detached hold — closing the chat must not leave the Mac awake. */
5668
+ releaseOrchestratorCaffeinate(thread) {
5669
+ if (!isOrchestratorThread(thread)) return;
5670
+ try {
5671
+ releaseCaffeinateHoldForThread(thread.id);
5672
+ } catch {
5673
+ }
5674
+ }
5664
5675
  async diff(threadRef, opts) {
5665
5676
  const thread = this.requireThread(threadRef);
5666
5677
  this.assertNotGlobal(thread, "Diff");
@@ -6030,6 +6041,7 @@ var Orchestrator = class {
6030
6041
  async archive(threadRef) {
6031
6042
  const thread = this.requireThread(threadRef);
6032
6043
  this.stop(thread.id);
6044
+ this.releaseOrchestratorCaffeinate(thread);
6033
6045
  if (isGlobalThread(thread)) {
6034
6046
  const archived2 = setStatus(thread.id, "archived");
6035
6047
  this.emit({ type: "status_changed", threadId: archived2.id, status: "archived" });
@@ -6054,7 +6066,7 @@ var Orchestrator = class {
6054
6066
  this.emit({ type: "status_changed", threadId: archived.id, status: "archived" });
6055
6067
  if (thread.repoPath && !isGlobalRepoPath(thread.repoPath)) {
6056
6068
  try {
6057
- const { ensureWorkspace: ensureWorkspace2 } = await import("../workspaces-L4TXMUNM.js");
6069
+ const { ensureWorkspace: ensureWorkspace2 } = await import("../workspaces-GQ4XKBD3.js");
6058
6070
  await ensureWorkspace2(thread.repoPath);
6059
6071
  } catch {
6060
6072
  }
@@ -6064,6 +6076,7 @@ var Orchestrator = class {
6064
6076
  async purge(threadRef, opts) {
6065
6077
  const thread = this.requireThread(threadRef);
6066
6078
  this.stop(thread.id);
6079
+ this.releaseOrchestratorCaffeinate(thread);
6067
6080
  if (isGlobalThread(thread)) {
6068
6081
  deleteThreadRecord(thread.id);
6069
6082
  return;
@@ -6168,13 +6181,36 @@ function getOrchestrator() {
6168
6181
  return singleton;
6169
6182
  }
6170
6183
 
6184
+ // src/http/fetch.ts
6185
+ var injected = null;
6186
+ function formatFetchError(err, url) {
6187
+ if (!(err instanceof Error)) return `${String(err)} (${url})`;
6188
+ const cause = err.cause;
6189
+ let detail = "";
6190
+ if (cause instanceof Error) {
6191
+ const code = typeof cause.code === "string" ? cause.code : void 0;
6192
+ detail = code ? ` [${code}: ${cause.message}]` : ` [${cause.message}]`;
6193
+ } else if (cause != null) {
6194
+ detail = ` [${String(cause)}]`;
6195
+ }
6196
+ return `${err.message}${detail} (${url})`;
6197
+ }
6198
+ async function httpFetch(input, init) {
6199
+ const url = typeof input === "string" ? input : input.href;
6200
+ const fn = injected ?? globalThis.fetch.bind(globalThis);
6201
+ try {
6202
+ return await fn(url, init);
6203
+ } catch (err) {
6204
+ throw new Error(formatFetchError(err, url));
6205
+ }
6206
+ }
6207
+
6171
6208
  // src/integrations/linear-oauth.ts
6172
6209
  import { createHash, randomBytes } from "crypto";
6173
6210
  import { createServer as createServer2 } from "http";
6174
6211
 
6175
6212
  // src/integrations/linear-app.ts
6176
6213
  var BAKED_LINEAR_CLIENT_ID = "39a15abc7ea5bea17c47f47f85ff5fd0";
6177
- var BAKED_LINEAR_CLIENT_SECRET = "";
6178
6214
 
6179
6215
  // src/integrations/linear-oauth.ts
6180
6216
  var LINEAR_OAUTH_PORT = 19848;
@@ -6184,7 +6220,7 @@ var REFRESH_SKEW_MS = 5 * 6e4;
6184
6220
  function linearOAuthCredentials() {
6185
6221
  const settings = loadAppSettings();
6186
6222
  const clientId = process.env.SIDEBOARD_LINEAR_CLIENT_ID?.trim() || settings.integrations.linearClientId?.trim() || BAKED_LINEAR_CLIENT_ID.trim();
6187
- const clientSecret = process.env.SIDEBOARD_LINEAR_CLIENT_SECRET?.trim() || settings.integrations.linearClientSecret?.trim() || BAKED_LINEAR_CLIENT_SECRET.trim();
6223
+ const clientSecret = process.env.SIDEBOARD_LINEAR_CLIENT_SECRET?.trim() || settings.integrations.linearClientSecret?.trim() || "";
6188
6224
  if (!clientId) {
6189
6225
  throw new Error(
6190
6226
  `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.`
@@ -6200,7 +6236,7 @@ function linearAuthorizationHeader(token) {
6200
6236
  return `Bearer ${t}`;
6201
6237
  }
6202
6238
  async function exchangeLinearToken(body) {
6203
- const res = await fetch(LINEAR_TOKEN, {
6239
+ const res = await httpFetch(LINEAR_TOKEN, {
6204
6240
  method: "POST",
6205
6241
  headers: { "Content-Type": "application/x-www-form-urlencoded" },
6206
6242
  body
@@ -6267,6 +6303,18 @@ async function getLinearAuthToken(settings = loadAppSettings()) {
6267
6303
 
6268
6304
  // src/integrations/linear.ts
6269
6305
  var LINEAR_GRAPHQL = "https://api.linear.app/graphql";
6306
+ var ISSUE_FIELDS = `
6307
+ id
6308
+ identifier
6309
+ title
6310
+ description
6311
+ url
6312
+ priority
6313
+ state { id name type }
6314
+ assignee { id name }
6315
+ team { id key name states { nodes { id name type } } }
6316
+ labels { nodes { name } }
6317
+ `;
6270
6318
  var ASSIGNED_ISSUES_QUERY = `
6271
6319
  query SideboardAssignedIssues($first: Int!) {
6272
6320
  viewer {
@@ -6275,53 +6323,280 @@ query SideboardAssignedIssues($first: Int!) {
6275
6323
  orderBy: updatedAt
6276
6324
  filter: { state: { type: { nin: ["completed", "canceled"] } } }
6277
6325
  ) {
6278
- nodes {
6279
- id
6280
- identifier
6281
- title
6282
- url
6283
- labels { nodes { name } }
6284
- }
6326
+ nodes { ${ISSUE_FIELDS} }
6285
6327
  }
6286
6328
  }
6287
6329
  }
6288
6330
  `;
6289
- async function listLinearIssuesDirect(opts) {
6290
- const apiKey = (opts?.apiKey ?? await getLinearAuthToken())?.trim();
6291
- if (!apiKey) {
6331
+ var TEAMS_QUERY = `
6332
+ query SideboardTeams {
6333
+ viewer { id name }
6334
+ teams(first: 50) {
6335
+ nodes {
6336
+ id
6337
+ key
6338
+ name
6339
+ states { nodes { id name type } }
6340
+ }
6341
+ }
6342
+ }
6343
+ `;
6344
+ var ISSUE_QUERY = `
6345
+ query SideboardIssue($id: String!) {
6346
+ issue(id: $id) { ${ISSUE_FIELDS} }
6347
+ }
6348
+ `;
6349
+ var ISSUE_CREATE = `
6350
+ mutation SideboardIssueCreate($input: IssueCreateInput!) {
6351
+ issueCreate(input: $input) {
6352
+ success
6353
+ issue { ${ISSUE_FIELDS} }
6354
+ }
6355
+ }
6356
+ `;
6357
+ var ISSUE_UPDATE = `
6358
+ mutation SideboardIssueUpdate($id: String!, $input: IssueUpdateInput!) {
6359
+ issueUpdate(id: $id, input: $input) {
6360
+ success
6361
+ issue { ${ISSUE_FIELDS} }
6362
+ }
6363
+ }
6364
+ `;
6365
+ var COMMENT_CREATE = `
6366
+ mutation SideboardCommentCreate($input: CommentCreateInput!) {
6367
+ commentCreate(input: $input) {
6368
+ success
6369
+ comment { id body url }
6370
+ }
6371
+ }
6372
+ `;
6373
+ function rewriteLinearError(message) {
6374
+ if (/scope|permission|not (authorized|allowed)|unauthorized|insufficient/i.test(message)) {
6375
+ return `${message} \u2014 Disconnect and Connect Linear in Account settings to grant write access.`;
6376
+ }
6377
+ return message;
6378
+ }
6379
+ async function requireLinearToken(apiKey) {
6380
+ const token = (apiKey ?? await getLinearAuthToken())?.trim();
6381
+ if (!token) {
6292
6382
  throw new Error("Linear is not connected \u2014 sign in from Account settings");
6293
6383
  }
6294
- const first = Math.max(1, Math.min(100, opts?.limit ?? 50));
6295
- const res = await fetch(LINEAR_GRAPHQL, {
6384
+ return token;
6385
+ }
6386
+ async function linearGraphql(query, variables, opts) {
6387
+ const apiKey = await requireLinearToken(opts?.apiKey);
6388
+ const res = await httpFetch(LINEAR_GRAPHQL, {
6296
6389
  method: "POST",
6297
6390
  headers: {
6298
6391
  "Content-Type": "application/json",
6299
6392
  Authorization: linearAuthorizationHeader(apiKey)
6300
6393
  },
6301
- body: JSON.stringify({
6302
- query: ASSIGNED_ISSUES_QUERY,
6303
- variables: { first }
6304
- })
6394
+ body: JSON.stringify({ query, variables })
6305
6395
  });
6306
6396
  if (!res.ok) {
6307
6397
  const body = await res.text().catch(() => "");
6308
6398
  throw new Error(
6309
- `Linear API error ${res.status}${body ? `: ${body.slice(0, 200)}` : ""}`
6399
+ rewriteLinearError(
6400
+ `Linear API error ${res.status}${body ? `: ${body.slice(0, 200)}` : ""}`
6401
+ )
6310
6402
  );
6311
6403
  }
6312
6404
  const json = await res.json();
6313
6405
  if (json.errors?.length) {
6314
- throw new Error(json.errors.map((e) => e.message ?? "Linear error").join("; "));
6406
+ throw new Error(
6407
+ rewriteLinearError(json.errors.map((e) => e.message ?? "Linear error").join("; "))
6408
+ );
6315
6409
  }
6316
- const nodes = json.data?.viewer?.assignedIssues?.nodes ?? [];
6317
- return nodes.map((node) => ({
6410
+ if (json.data == null) {
6411
+ throw new Error("Linear API returned no data");
6412
+ }
6413
+ return json.data;
6414
+ }
6415
+ function mapState(node) {
6416
+ if (!node?.id) return void 0;
6417
+ return {
6418
+ id: String(node.id),
6419
+ name: String(node.name ?? ""),
6420
+ type: String(node.type ?? "")
6421
+ };
6422
+ }
6423
+ function mapIssue(node) {
6424
+ const team = node.team;
6425
+ return {
6318
6426
  id: String(node.id ?? node.identifier ?? ""),
6319
6427
  identifier: String(node.identifier ?? node.id ?? ""),
6320
6428
  title: String(node.title ?? ""),
6321
6429
  url: String(node.url ?? ""),
6322
- labels: (node.labels?.nodes ?? []).map((l) => l.name).filter((n) => Boolean(n)),
6430
+ description: node.description?.trim() || void 0,
6431
+ priority: typeof node.priority === "number" ? node.priority : void 0,
6432
+ state: mapState(node.state),
6433
+ assignee: node.assignee?.id ? { id: String(node.assignee.id), name: String(node.assignee.name ?? "") } : void 0,
6434
+ team: team?.id ? {
6435
+ id: String(team.id),
6436
+ key: String(team.key ?? ""),
6437
+ name: String(team.name ?? ""),
6438
+ states: (team.states?.nodes ?? []).map((s) => mapState(s)).filter((s) => Boolean(s))
6439
+ } : void 0,
6440
+ labels: (node.labels?.nodes ?? []).map((l) => l.name).filter((n) => Boolean(n))
6441
+ };
6442
+ }
6443
+ function toIssueInfo(issue) {
6444
+ return {
6445
+ id: issue.id,
6446
+ identifier: issue.identifier,
6447
+ title: issue.title,
6448
+ url: issue.url,
6449
+ labels: issue.labels,
6323
6450
  provider: "linear"
6324
- }));
6451
+ };
6452
+ }
6453
+ function mapTeam(node) {
6454
+ return {
6455
+ id: String(node.id ?? ""),
6456
+ key: String(node.key ?? ""),
6457
+ name: String(node.name ?? ""),
6458
+ states: (node.states?.nodes ?? []).map((s) => mapState(s)).filter((s) => Boolean(s))
6459
+ };
6460
+ }
6461
+ function resolveLinearTeam(teams, team) {
6462
+ const t = team.trim();
6463
+ if (!t) throw new Error("Linear team is required (id, key, or name from linear_list_teams)");
6464
+ const lower = t.toLowerCase();
6465
+ const found = teams.find(
6466
+ (x) => x.id === t || x.key.toLowerCase() === lower || x.name.toLowerCase() === lower
6467
+ );
6468
+ if (!found) {
6469
+ const keys = teams.map((x) => x.key).filter(Boolean).join(", ") || "(none)";
6470
+ throw new Error(`Linear team not found: ${team}. Known keys: ${keys}`);
6471
+ }
6472
+ return found;
6473
+ }
6474
+ function resolveLinearState(team, state) {
6475
+ const s = state.trim();
6476
+ if (!s) throw new Error("Linear state is empty");
6477
+ const lower = s.toLowerCase();
6478
+ const found = team.states.find((x) => x.id === s) || team.states.find((x) => x.name.toLowerCase() === lower) || team.states.find((x) => x.type.toLowerCase() === lower);
6479
+ if (!found) {
6480
+ const available = team.states.map((x) => `${x.name} (${x.type})`).join(", ") || "(none)";
6481
+ throw new Error(`Linear state not found on ${team.key}: ${state}. Available: ${available}`);
6482
+ }
6483
+ return found;
6484
+ }
6485
+ function normalizePriority(priority) {
6486
+ if (priority == null) return void 0;
6487
+ if (!Number.isInteger(priority) || priority < 0 || priority > 4) {
6488
+ throw new Error("Linear priority must be 0 (none), 1 (urgent), 2 (high), 3 (medium), or 4 (low)");
6489
+ }
6490
+ return priority;
6491
+ }
6492
+ async function listLinearIssuesDirect(opts) {
6493
+ const first = Math.max(1, Math.min(100, opts?.limit ?? 50));
6494
+ const json = await linearGraphql(ASSIGNED_ISSUES_QUERY, { first }, opts);
6495
+ return (json.viewer?.assignedIssues?.nodes ?? []).map((node) => toIssueInfo(mapIssue(node)));
6496
+ }
6497
+ async function listLinearTeams(opts) {
6498
+ const json = await linearGraphql(TEAMS_QUERY, void 0, opts);
6499
+ return {
6500
+ viewer: {
6501
+ id: String(json.viewer?.id ?? ""),
6502
+ name: String(json.viewer?.name ?? "")
6503
+ },
6504
+ teams: (json.teams?.nodes ?? []).map(mapTeam)
6505
+ };
6506
+ }
6507
+ async function getLinearIssue(id, opts) {
6508
+ const issueId = id.trim();
6509
+ if (!issueId) throw new Error("Linear issue id is required (uuid or ENG-123)");
6510
+ const json = await linearGraphql(
6511
+ ISSUE_QUERY,
6512
+ { id: issueId },
6513
+ opts
6514
+ );
6515
+ if (!json.issue) {
6516
+ throw new Error(`Linear issue not found: ${issueId}`);
6517
+ }
6518
+ return mapIssue(json.issue);
6519
+ }
6520
+ async function createLinearIssue(input, opts) {
6521
+ const title = input.title.trim();
6522
+ if (!title) throw new Error("Linear issue title is required");
6523
+ const { viewer, teams } = await listLinearTeams(opts);
6524
+ const team = resolveLinearTeam(teams, input.team);
6525
+ const mutationInput = {
6526
+ teamId: team.id,
6527
+ title
6528
+ };
6529
+ const description = input.description?.trim();
6530
+ if (description) mutationInput.description = description;
6531
+ if (input.state?.trim()) {
6532
+ mutationInput.stateId = resolveLinearState(team, input.state).id;
6533
+ }
6534
+ const assignee = input.assignee === void 0 ? void 0 : input.assignee?.trim() || null;
6535
+ if (assignee === "me") mutationInput.assigneeId = viewer.id;
6536
+ else if (assignee) mutationInput.assigneeId = assignee;
6537
+ const priority = normalizePriority(input.priority);
6538
+ if (priority != null) mutationInput.priority = priority;
6539
+ const json = await linearGraphql(ISSUE_CREATE, { input: mutationInput }, opts);
6540
+ if (!json.issueCreate?.success || !json.issueCreate.issue) {
6541
+ throw new Error("Linear issueCreate failed");
6542
+ }
6543
+ return mapIssue(json.issueCreate.issue);
6544
+ }
6545
+ async function updateLinearIssue(input, opts) {
6546
+ const issueId = input.id.trim();
6547
+ if (!issueId) throw new Error("Linear issue id is required (uuid or ENG-123)");
6548
+ const mutationInput = {};
6549
+ if (input.title !== void 0) {
6550
+ const title = input.title.trim();
6551
+ if (!title) throw new Error("Linear issue title cannot be empty");
6552
+ mutationInput.title = title;
6553
+ }
6554
+ if (input.description !== void 0) {
6555
+ mutationInput.description = input.description;
6556
+ }
6557
+ if (input.state?.trim()) {
6558
+ const existing = await getLinearIssue(issueId, opts);
6559
+ const team = existing.team;
6560
+ if (!team?.states.length) {
6561
+ throw new Error(`Linear issue ${existing.identifier} has no workflow states to resolve "${input.state}"`);
6562
+ }
6563
+ mutationInput.stateId = resolveLinearState(team, input.state).id;
6564
+ }
6565
+ if (input.assignee !== void 0) {
6566
+ const assignee = input.assignee?.trim() || null;
6567
+ if (assignee === "me") {
6568
+ const { viewer } = await listLinearTeams(opts);
6569
+ mutationInput.assigneeId = viewer.id;
6570
+ } else {
6571
+ mutationInput.assigneeId = assignee;
6572
+ }
6573
+ }
6574
+ if (input.priority !== void 0) {
6575
+ mutationInput.priority = normalizePriority(input.priority);
6576
+ }
6577
+ if (Object.keys(mutationInput).length === 0) {
6578
+ throw new Error("linear_update_issue needs at least one of title, description, state, assignee, priority");
6579
+ }
6580
+ const json = await linearGraphql(ISSUE_UPDATE, { id: issueId, input: mutationInput }, opts);
6581
+ if (!json.issueUpdate?.success || !json.issueUpdate.issue) {
6582
+ throw new Error("Linear issueUpdate failed");
6583
+ }
6584
+ return mapIssue(json.issueUpdate.issue);
6585
+ }
6586
+ async function commentLinearIssue(input, opts) {
6587
+ const issueId = input.id.trim();
6588
+ const body = input.body.trim();
6589
+ if (!issueId) throw new Error("Linear issue id is required (uuid or ENG-123)");
6590
+ if (!body) throw new Error("Linear comment body is required");
6591
+ const json = await linearGraphql(COMMENT_CREATE, { input: { issueId, body } }, opts);
6592
+ if (!json.commentCreate?.success || !json.commentCreate.comment?.id) {
6593
+ throw new Error("Linear commentCreate failed");
6594
+ }
6595
+ return {
6596
+ id: String(json.commentCreate.comment.id),
6597
+ body: String(json.commentCreate.comment.body ?? body),
6598
+ url: json.commentCreate.comment.url?.trim() || void 0
6599
+ };
6325
6600
  }
6326
6601
 
6327
6602
  // src/integrations/issues.ts
@@ -6603,8 +6878,8 @@ function labelGithubUrl(url) {
6603
6878
  }
6604
6879
  return { kind: "other", label: "GitHub", url: raw };
6605
6880
  }
6606
- function appendGithubLink(text2, githubUrl) {
6607
- const body = text2.trimEnd();
6881
+ function appendGithubLink(text3, githubUrl) {
6882
+ const body = text3.trimEnd();
6608
6883
  if (!githubUrl?.trim()) return body;
6609
6884
  const labeled = labelGithubUrl(githubUrl);
6610
6885
  if (!labeled) {
@@ -6887,6 +7162,101 @@ function registerSlackTools(server) {
6887
7162
  );
6888
7163
  }
6889
7164
 
7165
+ // src/mcp/linear-tools.ts
7166
+ import { z as z2 } from "zod";
7167
+ function text2(payload, isError = false) {
7168
+ return {
7169
+ content: [{ type: "text", text: JSON.stringify(payload, null, 2) }],
7170
+ ...isError ? { isError: true } : {}
7171
+ };
7172
+ }
7173
+ function fail2(err) {
7174
+ return text2(
7175
+ { error: err instanceof Error ? err.message : String(err) },
7176
+ true
7177
+ );
7178
+ }
7179
+ var prioritySchema = z2.number().int().min(0).max(4).optional().describe("0 none, 1 urgent, 2 high, 3 medium, 4 low");
7180
+ function registerLinearTools(server) {
7181
+ server.tool(
7182
+ "linear_list_teams",
7183
+ "List Linear teams (id, key, name) and workflow states for the connected Account. Use team key (e.g. ENG) on linear_create_issue; use state name or type (started, completed) on create/update. Viewer id is for assignee=me.",
7184
+ {},
7185
+ async () => {
7186
+ try {
7187
+ return text2(await listLinearTeams());
7188
+ } catch (err) {
7189
+ return fail2(err);
7190
+ }
7191
+ }
7192
+ );
7193
+ server.tool(
7194
+ "linear_get_issue",
7195
+ "Get a Linear issue by uuid or identifier (ENG-123).",
7196
+ { id: z2.string() },
7197
+ async ({ id }) => {
7198
+ try {
7199
+ return text2(await getLinearIssue(id));
7200
+ } catch (err) {
7201
+ return fail2(err);
7202
+ }
7203
+ }
7204
+ );
7205
+ server.tool(
7206
+ "linear_create_issue",
7207
+ 'Create a Linear issue. Call linear_list_teams first. team is id, key, or name. state is name, type (unstarted/started/completed/canceled/backlog), or id. assignee is "me" or a user id.',
7208
+ {
7209
+ team: z2.string(),
7210
+ title: z2.string(),
7211
+ description: z2.string().optional(),
7212
+ state: z2.string().optional(),
7213
+ assignee: z2.string().nullable().optional(),
7214
+ priority: prioritySchema
7215
+ },
7216
+ async (args) => {
7217
+ try {
7218
+ return text2(await createLinearIssue(args));
7219
+ } catch (err) {
7220
+ return fail2(err);
7221
+ }
7222
+ }
7223
+ );
7224
+ server.tool(
7225
+ "linear_update_issue",
7226
+ 'Update a Linear issue (uuid or ENG-123). Pass at least one of title, description, state, assignee, priority. state is name, type, or id. assignee is "me", a user id, or null to unassign.',
7227
+ {
7228
+ id: z2.string(),
7229
+ title: z2.string().optional(),
7230
+ description: z2.string().optional(),
7231
+ state: z2.string().optional(),
7232
+ assignee: z2.string().nullable().optional(),
7233
+ priority: prioritySchema
7234
+ },
7235
+ async (args) => {
7236
+ try {
7237
+ return text2(await updateLinearIssue(args));
7238
+ } catch (err) {
7239
+ return fail2(err);
7240
+ }
7241
+ }
7242
+ );
7243
+ server.tool(
7244
+ "linear_comment",
7245
+ "Add a markdown comment on a Linear issue (uuid or ENG-123).",
7246
+ {
7247
+ id: z2.string(),
7248
+ body: z2.string()
7249
+ },
7250
+ async (args) => {
7251
+ try {
7252
+ return text2(await commentLinearIssue(args));
7253
+ } catch (err) {
7254
+ return fail2(err);
7255
+ }
7256
+ }
7257
+ );
7258
+ }
7259
+
6890
7260
  // src/mcp/server.ts
6891
7261
  var MAX_ORCH_THREADS = 5;
6892
7262
  var CREATE_THREAD_TIMEOUT_MS = 9e4;
@@ -6963,7 +7333,7 @@ async function startMcpServer() {
6963
7333
  server.tool(
6964
7334
  "get_thread",
6965
7335
  "Get a compact thread summary by id/ref",
6966
- { ref: z2.string() },
7336
+ { ref: z3.string() },
6967
7337
  async ({ ref }) => {
6968
7338
  const t = orch.getThread(ref);
6969
7339
  if (!t) {
@@ -6992,14 +7362,14 @@ async function startMcpServer() {
6992
7362
  "present_artifact",
6993
7363
  "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.",
6994
7364
  {
6995
- title: z2.string().describe("Short title shown in the artifact pane header"),
6996
- type: z2.enum(["html", "svg", "markdown", "react"]).describe(
7365
+ title: z3.string().describe("Short title shown in the artifact pane header"),
7366
+ type: z3.enum(["html", "svg", "markdown", "react"]).describe(
6997
7367
  "Artifact kind \u2014 html opens an iframe preview; react transpiles and renders a default-exported component in a sandboxed iframe"
6998
7368
  ),
6999
- content: z2.string().describe(
7369
+ content: z3.string().describe(
7000
7370
  "Full document body (complete HTML page, SVG markup, markdown, or a React component module with a default export)"
7001
7371
  ),
7002
- artifact_id: z2.string().optional().describe("Stable id when updating the same artifact across turns")
7372
+ artifact_id: z3.string().optional().describe("Stable id when updating the same artifact across turns")
7003
7373
  },
7004
7374
  async ({ title, type, artifact_id }) => {
7005
7375
  const id = artifact_id?.trim() || `artifact_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 8)}`;
@@ -7017,15 +7387,15 @@ async function startMcpServer() {
7017
7387
  "ask_user",
7018
7388
  "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.",
7019
7389
  {
7020
- questions: z2.array(
7021
- z2.object({
7022
- question: z2.string().describe("Full question text ending with ?"),
7023
- header: z2.string().max(24).optional().describe("Short label shown above the question"),
7024
- multiSelect: z2.boolean().optional().describe("Allow selecting multiple options"),
7025
- options: z2.array(
7026
- z2.object({
7027
- label: z2.string(),
7028
- description: z2.string().optional().describe("What this option means / when to choose it (strongly preferred)")
7390
+ questions: z3.array(
7391
+ z3.object({
7392
+ question: z3.string().describe("Full question text ending with ?"),
7393
+ header: z3.string().max(24).optional().describe("Short label shown above the question"),
7394
+ multiSelect: z3.boolean().optional().describe("Allow selecting multiple options"),
7395
+ options: z3.array(
7396
+ z3.object({
7397
+ label: z3.string(),
7398
+ description: z3.string().optional().describe("What this option means / when to choose it (strongly preferred)")
7029
7399
  })
7030
7400
  ).min(2).max(6).describe("2\u20136 choices (Sideboard also offers Other)")
7031
7401
  })
@@ -7044,9 +7414,9 @@ async function startMcpServer() {
7044
7414
  "present_plan",
7045
7415
  "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.",
7046
7416
  {
7047
- title: z2.string().optional().describe("Short plan title (defaults to Plan)"),
7048
- content: z2.string().min(1).describe("Full plan markdown (headings, steps, risks, open questions)"),
7049
- thread_id: z2.string().optional().describe("Sideboard thread id when cwd is not the worktree")
7417
+ title: z3.string().optional().describe("Short plan title (defaults to Plan)"),
7418
+ content: z3.string().min(1).describe("Full plan markdown (headings, steps, risks, open questions)"),
7419
+ thread_id: z3.string().optional().describe("Sideboard thread id when cwd is not the worktree")
7050
7420
  },
7051
7421
  async ({ title, content, thread_id }) => {
7052
7422
  const { writePlanFile: writePlanFile2 } = await import("../plan-file-PHVKUAEE.js");
@@ -7069,15 +7439,15 @@ async function startMcpServer() {
7069
7439
  "present_schema",
7070
7440
  "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.",
7071
7441
  {
7072
- title: z2.string().describe("Pane title"),
7073
- mode: z2.enum(["table", "form"]).optional().describe("table = list/filter records; form = edit one record"),
7074
- datasource: z2.enum(["brightsy", "inline"]).optional().describe("brightsy resolves via login; inline uses embedded resource/records"),
7075
- resource_id: z2.string().optional().describe("Brightsy record type UUID (or generic resource id)"),
7076
- record_id: z2.string().optional().describe("Record id when opening form mode"),
7077
- resource: z2.record(z2.unknown()).optional().describe("Inline { id, title, schema, schemaUi?, slug? }"),
7078
- record: z2.record(z2.unknown()).optional().describe("Inline record { id, data, published_at? }"),
7079
- records: z2.array(z2.record(z2.unknown())).optional().describe("Inline records for table mode"),
7080
- pane_id: z2.string().optional().describe("Stable pane id across updates")
7442
+ title: z3.string().describe("Pane title"),
7443
+ mode: z3.enum(["table", "form"]).optional().describe("table = list/filter records; form = edit one record"),
7444
+ datasource: z3.enum(["brightsy", "inline"]).optional().describe("brightsy resolves via login; inline uses embedded resource/records"),
7445
+ resource_id: z3.string().optional().describe("Brightsy record type UUID (or generic resource id)"),
7446
+ record_id: z3.string().optional().describe("Record id when opening form mode"),
7447
+ resource: z3.record(z3.unknown()).optional().describe("Inline { id, title, schema, schemaUi?, slug? }"),
7448
+ record: z3.record(z3.unknown()).optional().describe("Inline record { id, data, published_at? }"),
7449
+ records: z3.array(z3.record(z3.unknown())).optional().describe("Inline records for table mode"),
7450
+ pane_id: z3.string().optional().describe("Stable pane id across updates")
7081
7451
  },
7082
7452
  async (args) => {
7083
7453
  const id = args.pane_id?.trim() || `schema_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 8)}`;
@@ -7098,10 +7468,10 @@ async function startMcpServer() {
7098
7468
  "present_files",
7099
7469
  "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.",
7100
7470
  {
7101
- title: z2.string().optional().describe("Pane title (default: Files)"),
7102
- datasource: z2.enum(["brightsy", "memory"]).optional().describe("brightsy = account storage via login; memory = session demo store"),
7103
- path: z2.string().optional().describe("Initial folder path (e.g. public)"),
7104
- pane_id: z2.string().optional().describe("Stable pane id across updates")
7471
+ title: z3.string().optional().describe("Pane title (default: Files)"),
7472
+ datasource: z3.enum(["brightsy", "memory"]).optional().describe("brightsy = account storage via login; memory = session demo store"),
7473
+ path: z3.string().optional().describe("Initial folder path (e.g. public)"),
7474
+ pane_id: z3.string().optional().describe("Stable pane id across updates")
7105
7475
  },
7106
7476
  async (args) => {
7107
7477
  const id = args.pane_id?.trim() || `files_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 8)}`;
@@ -7117,19 +7487,21 @@ async function startMcpServer() {
7117
7487
  }
7118
7488
  );
7119
7489
  registerSlackTools(server);
7490
+ registerLinearTools(server);
7120
7491
  if (sideboardMcpProfile() !== "worktree") {
7121
- const { getCaffeinateHold, setCaffeinateHold } = await import("../caffeinate-hold-BEJIYPJ7.js");
7492
+ const { getCaffeinateHold, setCaffeinateHold } = await import("../caffeinate-hold-OHAUWOA6.js");
7122
7493
  const { resolveNewThreadOptions: resolveNewThreadOptions2, resolveThreadDefaults } = await import("../app-settings-P42Z3VOB.js");
7123
7494
  const accountDefaults = resolveThreadDefaults();
7124
7495
  const accountDefaultsHint = `Account defaults: agent=${accountDefaults.agent}, model=${accountDefaults.model?.trim() || "Auto"}, effort=${accountDefaults.effort}`;
7125
7496
  server.tool(
7126
7497
  "set_caffeinate",
7127
- "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.",
7498
+ "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. Closing or archiving this orchestration chat also releases the hold. macOS only.",
7128
7499
  {
7129
- enabled: z2.boolean().describe("true = hold caffeinate on; false = release and let the Mac sleep")
7500
+ enabled: z3.boolean().describe("true = hold caffeinate on; false = release and let the Mac sleep")
7130
7501
  },
7131
7502
  async ({ enabled }) => {
7132
- const state = setCaffeinateHold(enabled);
7503
+ const threadId = process.env.SIDEBOARD_ORCHESTRATOR_THREAD_ID?.trim() || null;
7504
+ const state = setCaffeinateHold(enabled, { threadId });
7133
7505
  if (enabled && !state.held) {
7134
7506
  return {
7135
7507
  content: [
@@ -7152,7 +7524,7 @@ async function startMcpServer() {
7152
7524
  text: JSON.stringify({
7153
7525
  ...getCaffeinateHold(),
7154
7526
  ok: true,
7155
- 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)."
7527
+ message: state.held ? "Mac will stay awake until you call set_caffeinate with enabled=false, the user says they are done, or this orchestration chat is closed." : "Caffeinate hold released. The Mac can sleep (unless Settings caffeinate toggles are on)."
7156
7528
  })
7157
7529
  }
7158
7530
  ]
@@ -7163,15 +7535,15 @@ async function startMcpServer() {
7163
7535
  "create_thread",
7164
7536
  `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.`,
7165
7537
  {
7166
- sourceType: z2.enum(["branch", "pr", "ticket"]),
7167
- sourceRef: z2.string(),
7168
- agent: z2.enum(["claude", "codex", "opencode", "brightsy", "cursor"]).optional().describe(`Omit to use Account default agent (${accountDefaults.agent})`),
7169
- model: z2.string().nullable().optional().describe(
7538
+ sourceType: z3.enum(["branch", "pr", "ticket"]),
7539
+ sourceRef: z3.string(),
7540
+ agent: z3.enum(["claude", "codex", "opencode", "brightsy", "cursor"]).optional().describe(`Omit to use Account default agent (${accountDefaults.agent})`),
7541
+ model: z3.string().nullable().optional().describe(
7170
7542
  `Usually omit to use Account default model (${accountDefaults.model?.trim() || "Auto"}). Pass null only to force Auto / agent-default.`
7171
7543
  ),
7172
- repoPath: z2.string(),
7173
- title: z2.string().optional(),
7174
- parentThreadId: z2.string().optional().describe(
7544
+ repoPath: z3.string(),
7545
+ title: z3.string().optional(),
7546
+ parentThreadId: z3.string().optional().describe(
7175
7547
  "Orchestration: omit (preferred) or pass YOUR chat id from the turn reminder / AGENTS.md. Do not invent uuids."
7176
7548
  )
7177
7549
  },
@@ -7278,9 +7650,9 @@ async function startMcpServer() {
7278
7650
  "send_to_thread",
7279
7651
  "Queue a prompt on a worktree thread chat (runs under concurrency cap). Use after create_thread to start or continue a conversation. For commit/push/PR/merge, prefer ask_git (canonical desktop-button phrases). 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.",
7280
7652
  {
7281
- ref: z2.string(),
7282
- prompt: z2.string(),
7283
- force_stop: z2.boolean().optional()
7653
+ ref: z3.string(),
7654
+ prompt: z3.string(),
7655
+ force_stop: z3.boolean().optional()
7284
7656
  },
7285
7657
  async ({ ref, prompt, force_stop }) => {
7286
7658
  if (force_stop) {
@@ -7309,8 +7681,8 @@ async function startMcpServer() {
7309
7681
  "wait_for_turn",
7310
7682
  "Block until the thread finishes its current/queued turn (avoids polling). Use after send_to_thread or ask_git to read the agent reply.",
7311
7683
  {
7312
- ref: z2.string(),
7313
- timeoutMs: z2.number().optional()
7684
+ ref: z3.string(),
7685
+ timeoutMs: z3.number().optional()
7314
7686
  },
7315
7687
  async ({ ref, timeoutMs }) => {
7316
7688
  const thread = await orch.waitForTurn(ref, timeoutMs ?? 6e5);
@@ -7332,7 +7704,7 @@ async function startMcpServer() {
7332
7704
  server.tool(
7333
7705
  "get_turn_result",
7334
7706
  "Final assistant message only (not full transcript)",
7335
- { ref: z2.string() },
7707
+ { ref: z3.string() },
7336
7708
  async ({ ref }) => {
7337
7709
  const result = orch.getTurnResult(ref);
7338
7710
  return { content: [{ type: "text", text: JSON.stringify(result) }] };
@@ -7342,8 +7714,8 @@ async function startMcpServer() {
7342
7714
  "stop_thread",
7343
7715
  "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.",
7344
7716
  {
7345
- ref: z2.string(),
7346
- force: z2.boolean().optional()
7717
+ ref: z3.string(),
7718
+ force: z3.boolean().optional()
7347
7719
  },
7348
7720
  async ({ ref, force }) => {
7349
7721
  const t = orch.getThread(ref);
@@ -7373,7 +7745,7 @@ async function startMcpServer() {
7373
7745
  server.tool(
7374
7746
  "archive_thread",
7375
7747
  "Archive a thread (stops agent/dev, runs archive script, removes worktree when last chat tab). Coordinators commit, push, open PRs, and merge only by asking the worktree agent (ask_git).",
7376
- { ref: z2.string() },
7748
+ { ref: z3.string() },
7377
7749
  async ({ ref }) => {
7378
7750
  const t = orch.getThread(ref);
7379
7751
  if (!t) {
@@ -7406,7 +7778,7 @@ async function startMcpServer() {
7406
7778
  server.tool(
7407
7779
  "restore_thread",
7408
7780
  "Restore an archived thread (recreates worktree from branch when needed)",
7409
- { ref: z2.string() },
7781
+ { ref: z3.string() },
7410
7782
  async ({ ref }) => {
7411
7783
  try {
7412
7784
  const restored = await orch.restore(ref);
@@ -7432,8 +7804,8 @@ async function startMcpServer() {
7432
7804
  "get_diff",
7433
7805
  "Compact diff summary (capped hunks, paginated)",
7434
7806
  {
7435
- ref: z2.string(),
7436
- maxFiles: z2.number().optional()
7807
+ ref: z3.string(),
7808
+ maxFiles: z3.number().optional()
7437
7809
  },
7438
7810
  async ({ ref, maxFiles }) => {
7439
7811
  const summary = await orch.diffSummary(ref);
@@ -7453,7 +7825,7 @@ async function startMcpServer() {
7453
7825
  server.tool(
7454
7826
  "request_review",
7455
7827
  '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.',
7456
- { ref: z2.string().describe("Worktree thread id/ref to review") },
7828
+ { ref: z3.string().describe("Worktree thread id/ref to review") },
7457
7829
  async ({ ref }) => {
7458
7830
  try {
7459
7831
  const tab = await orch.requestReview(ref);
@@ -7482,8 +7854,8 @@ async function startMcpServer() {
7482
7854
  "ask_git",
7483
7855
  "Tell a worktree agent to commit & push, open a draft PR, resolve conflicts, or merge the linked PR \u2014 same short prompts as the desktop git buttons. The worktree agent runs git/gh (`gh pr merge`); this only queues the prompt. Pass a worktree thread ref (not the orchestrator). Then wait_for_turn / get_turn_result. Do not run git or gh from the orchestration cwd.",
7484
7856
  {
7485
- ref: z2.string().describe("Worktree thread id/ref"),
7486
- action: z2.enum(AGENT_GIT_ACTIONS).describe(
7857
+ ref: z3.string().describe("Worktree thread id/ref"),
7858
+ action: z3.enum(AGENT_GIT_ACTIONS).describe(
7487
7859
  "commit-push | create-draft | create-web | resolve-conflicts | merge"
7488
7860
  )
7489
7861
  },
@@ -7510,7 +7882,7 @@ async function startMcpServer() {
7510
7882
  }
7511
7883
  }
7512
7884
  );
7513
- const agentEnum = z2.enum(["claude", "codex", "opencode", "brightsy", "cursor"]);
7885
+ const agentEnum = z3.enum(["claude", "codex", "opencode", "brightsy", "cursor"]);
7514
7886
  server.tool(
7515
7887
  "list_models",
7516
7888
  "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.",
@@ -7533,11 +7905,11 @@ async function startMcpServer() {
7533
7905
  "fork_worktree",
7534
7906
  "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.",
7535
7907
  {
7536
- ref: z2.string().describe("Worktree thread id/ref to fork"),
7537
- through_index: z2.number().optional().describe("Inclusive message index to include in the transcript (default: all)"),
7908
+ ref: z3.string().describe("Worktree thread id/ref to fork"),
7909
+ through_index: z3.number().optional().describe("Inclusive message index to include in the transcript (default: all)"),
7538
7910
  agent: agentEnum.optional().describe("Agent for the forked chat (default: same as source)"),
7539
- model: z2.string().nullable().optional().describe("Usually omit (Auto). Only set from list_models when you need a specific model"),
7540
- title: z2.string().optional()
7911
+ model: z3.string().nullable().optional().describe("Usually omit (Auto). Only set from list_models when you need a specific model"),
7912
+ title: z3.string().optional()
7541
7913
  },
7542
7914
  async ({ ref, through_index, agent, model, title }) => {
7543
7915
  try {
@@ -7578,11 +7950,11 @@ async function startMcpServer() {
7578
7950
  "fork_chat",
7579
7951
  "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.",
7580
7952
  {
7581
- ref: z2.string().describe("Thread id/ref to fork (worktree agent or orchestration chat)"),
7582
- through_index: z2.number().optional().describe("Inclusive message index to include in the transcript (default: all)"),
7953
+ ref: z3.string().describe("Thread id/ref to fork (worktree agent or orchestration chat)"),
7954
+ through_index: z3.number().optional().describe("Inclusive message index to include in the transcript (default: all)"),
7583
7955
  agent: agentEnum.optional().describe("Agent for the forked chat (default: same as source)"),
7584
- model: z2.string().nullable().optional().describe("Usually omit (Auto). Only set from list_models when you need a specific model"),
7585
- title: z2.string().optional()
7956
+ model: z3.string().nullable().optional().describe("Usually omit (Auto). Only set from list_models when you need a specific model"),
7957
+ title: z3.string().optional()
7586
7958
  },
7587
7959
  async ({ ref, through_index, agent, model, title }) => {
7588
7960
  try {
@@ -7628,8 +8000,8 @@ async function startMcpServer() {
7628
8000
  "run_dev_script",
7629
8001
  "Start a .sideboard/.conductor run script for a thread (default script if name omitted); returns port",
7630
8002
  {
7631
- ref: z2.string(),
7632
- name: z2.string().optional()
8003
+ ref: z3.string(),
8004
+ name: z3.string().optional()
7633
8005
  },
7634
8006
  async ({ ref, name }) => {
7635
8007
  const result = await orch.startDev(ref, name);
@@ -7651,7 +8023,7 @@ async function startMcpServer() {
7651
8023
  server.tool(
7652
8024
  "list_run_scripts",
7653
8025
  "List named run scripts available for a thread",
7654
- { ref: z2.string() },
8026
+ { ref: z3.string() },
7655
8027
  async ({ ref }) => {
7656
8028
  const scripts = orch.listThreadRunScripts(ref);
7657
8029
  const active = orch.getActiveRuns(ref);
@@ -7669,8 +8041,8 @@ async function startMcpServer() {
7669
8041
  "stop_dev_script",
7670
8042
  "Stop a running script for a thread (all scripts if name omitted)",
7671
8043
  {
7672
- ref: z2.string(),
7673
- name: z2.string().optional()
8044
+ ref: z3.string(),
8045
+ name: z3.string().optional()
7674
8046
  },
7675
8047
  async ({ ref, name }) => {
7676
8048
  orch.stopDev(ref, name);
@@ -7680,7 +8052,7 @@ async function startMcpServer() {
7680
8052
  server.tool(
7681
8053
  "run_setup",
7682
8054
  "Re-run workspace setup (Sideboard/Conductor settings or .cursor/worktrees.json)",
7683
- { ref: z2.string() },
8055
+ { ref: z3.string() },
7684
8056
  async ({ ref }) => {
7685
8057
  const result = await orch.runSetup(ref);
7686
8058
  return {
@@ -7691,7 +8063,7 @@ async function startMcpServer() {
7691
8063
  server.tool(
7692
8064
  "add_workspace",
7693
8065
  "Register a git repo as a Sideboard workspace",
7694
- { repoPath: z2.string() },
8066
+ { repoPath: z3.string() },
7695
8067
  async ({ repoPath }) => {
7696
8068
  const ws = await orch.addWorkspace(repoPath);
7697
8069
  return { content: [{ type: "text", text: JSON.stringify(ws) }] };
@@ -7700,7 +8072,7 @@ async function startMcpServer() {
7700
8072
  server.tool(
7701
8073
  "remove_workspace",
7702
8074
  "Unregister a Sideboard workspace (does not archive threads)",
7703
- { repoPath: z2.string() },
8075
+ { repoPath: z3.string() },
7704
8076
  async ({ repoPath }) => {
7705
8077
  orch.removeWorkspace(repoPath);
7706
8078
  return { content: [{ type: "text", text: "ok" }] };
@@ -7710,12 +8082,12 @@ async function startMcpServer() {
7710
8082
  "fanout",
7711
8083
  "Best-of-n: create one thread per agent with the same prompt (parallel attempts)",
7712
8084
  {
7713
- prompt: z2.string(),
7714
- agents: z2.array(z2.enum(["claude", "codex", "opencode", "brightsy", "cursor"])),
7715
- repoPath: z2.string(),
7716
- sourceType: z2.enum(["branch", "pr", "ticket"]).optional(),
7717
- sourceRef: z2.string().optional(),
7718
- title: z2.string().optional()
8085
+ prompt: z3.string(),
8086
+ agents: z3.array(z3.enum(["claude", "codex", "opencode", "brightsy", "cursor"])),
8087
+ repoPath: z3.string(),
8088
+ sourceType: z3.enum(["branch", "pr", "ticket"]).optional(),
8089
+ sourceRef: z3.string().optional(),
8090
+ title: z3.string().optional()
7719
8091
  },
7720
8092
  async (args) => {
7721
8093
  const threads = await orch.bestOfN(args);
@@ -7742,8 +8114,8 @@ async function startMcpServer() {
7742
8114
  "list_branches",
7743
8115
  "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).",
7744
8116
  {
7745
- repoPath: z2.string(),
7746
- unmergedOnly: z2.boolean().optional()
8117
+ repoPath: z3.string(),
8118
+ unmergedOnly: z3.boolean().optional()
7747
8119
  },
7748
8120
  async ({ repoPath, unmergedOnly }) => {
7749
8121
  const root = await resolveRepoRoot(repoPath);
@@ -7763,7 +8135,7 @@ async function startMcpServer() {
7763
8135
  server.tool(
7764
8136
  "list_prs",
7765
8137
  "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.",
7766
- { repoPath: z2.string() },
8138
+ { repoPath: z3.string() },
7767
8139
  async ({ repoPath }) => {
7768
8140
  const root = await resolveRepoRoot(repoPath);
7769
8141
  const prs = await listPrs(root);
@@ -7782,7 +8154,7 @@ async function startMcpServer() {
7782
8154
  server.tool(
7783
8155
  "get_pr_stack",
7784
8156
  "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 ask_git merge on stacked PRs.",
7785
- { ref: z2.string() },
8157
+ { ref: z3.string() },
7786
8158
  async ({ ref }) => {
7787
8159
  const stack = await orch.getPrStack(ref);
7788
8160
  return {
@@ -7794,8 +8166,8 @@ async function startMcpServer() {
7794
8166
  "open_pr_stack_layers",
7795
8167
  "Materialize one worktree+thread per stack layer (or a single 1-based layer). Pass a thread ref already on the stack.",
7796
8168
  {
7797
- ref: z2.string(),
7798
- layer: z2.number().int().positive().optional()
8169
+ ref: z3.string(),
8170
+ layer: z3.number().int().positive().optional()
7799
8171
  },
7800
8172
  async ({ ref, layer }) => {
7801
8173
  const result = await orch.openPrStackLayers(ref, { layer });
@@ -7829,9 +8201,9 @@ async function startMcpServer() {
7829
8201
  "add_stack_layer",
7830
8202
  "Add a branch on top of the current stack (`gh stack add`) and open a worktree+thread for it.",
7831
8203
  {
7832
- ref: z2.string(),
7833
- branchName: z2.string(),
7834
- title: z2.string().optional()
8204
+ ref: z3.string(),
8205
+ branchName: z3.string(),
8206
+ title: z3.string().optional()
7835
8207
  },
7836
8208
  async ({ ref, branchName, title }) => {
7837
8209
  const result = await orch.addStackLayer(ref, branchName, { title });
@@ -7860,11 +8232,11 @@ async function startMcpServer() {
7860
8232
  "create_pr_stack",
7861
8233
  "Create a new GitHub PR stack with one Sideboard worktree per layer (bottom\u2192top branch names). Requires `gh extension install github/gh-stack`.",
7862
8234
  {
7863
- repoPath: z2.string(),
7864
- branches: z2.array(z2.string()).min(1),
7865
- agent: z2.enum(["claude", "codex", "opencode", "brightsy", "cursor"]),
7866
- base: z2.string().optional(),
7867
- title: z2.string().optional()
8235
+ repoPath: z3.string(),
8236
+ branches: z3.array(z3.string()).min(1),
8237
+ agent: z3.enum(["claude", "codex", "opencode", "brightsy", "cursor"]),
8238
+ base: z3.string().optional(),
8239
+ title: z3.string().optional()
7868
8240
  },
7869
8241
  async (args) => {
7870
8242
  const result = await orch.createPrStack({
@@ -7902,7 +8274,7 @@ async function startMcpServer() {
7902
8274
  server.tool(
7903
8275
  "list_issues",
7904
8276
  "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.",
7905
- { repoPath: z2.string() },
8277
+ { repoPath: z3.string() },
7906
8278
  async ({ repoPath }) => {
7907
8279
  const root = await resolveRepoRoot(repoPath);
7908
8280
  const result = await listIssues(root);
@@ -7913,8 +8285,8 @@ async function startMcpServer() {
7913
8285
  "list_linear_issues",
7914
8286
  "Deprecated: prefer list_issues. Lists assigned Linear issues via the chosen agent MCP connector",
7915
8287
  {
7916
- agent: z2.enum(["claude", "codex", "opencode"]),
7917
- repoPath: z2.string()
8288
+ agent: z3.enum(["claude", "codex", "opencode"]),
8289
+ repoPath: z3.string()
7918
8290
  },
7919
8291
  async ({ agent, repoPath }) => {
7920
8292
  const issues = await listLinearIssues(agent, repoPath);