@sideboard-ai/core 0.1.74 → 0.1.79
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.
- package/dist/{agents-H7M5CQY2.js → agents-EWPHOM6Y.js} +6 -6
- package/dist/{agents-2S2JVLZ6.js → agents-GRAPUXWW.js} +5 -5
- package/dist/{app-settings-3TLA2EJ7.js → app-settings-6IOQYGBK.js} +7 -1
- package/dist/{app-settings-P42Z3VOB.js → app-settings-NILNWNNQ.js} +7 -1
- package/dist/{caffeinate-hold-KLC6SABD.js → caffeinate-hold-EAZNWJBE.js} +5 -1
- package/dist/caffeinate-hold-OHAUWOA6.js +20 -0
- package/dist/{chunk-77W62MTX.js → chunk-5LXWTU3J.js} +6 -6
- package/dist/{chunk-GJ2LZQJI.js → chunk-5VTWBI3I.js} +2 -2
- package/dist/{chunk-2NKSHIRI.js → chunk-6T2SKGCS.js} +3 -3
- package/dist/{chunk-OE7YBB5X.js → chunk-FUUVPN4A.js} +108 -3
- package/dist/{chunk-HCWAIEBU.js → chunk-HCGEFU3J.js} +8 -7
- package/dist/{chunk-XA2FQJTN.js → chunk-HSLQXL6M.js} +6 -5
- package/dist/{chunk-W7YOTDVO.js → chunk-IFPN6TER.js} +47 -2
- package/dist/chunk-MFF2RE3I.js +168 -0
- package/dist/{chunk-KBKPVKYZ.js → chunk-P33ZQ7ZC.js} +3 -3
- package/dist/{chunk-DG3S2UXP.js → chunk-PW5YHHP3.js} +2 -2
- package/dist/chunk-RV6DBEDQ.js +170 -0
- package/dist/{chunk-5KJMNNCB.js → chunk-SHCR6RPU.js} +3 -3
- package/dist/{chunk-PTJJIQK7.js → chunk-XKNBSYA7.js} +47 -2
- package/dist/{chunk-MF2YMEGD.js → chunk-YVVXWWCC.js} +110 -3
- package/dist/{coordinator-prompt-3XXSG7M2.js → coordinator-prompt-2OVON2LQ.js} +4 -4
- package/dist/{coordinator-prompt-RTUCCPCI.js → coordinator-prompt-K2R34A5T.js} +3 -3
- package/dist/{global-workspace-PJU6HISJ.js → global-workspace-RI367AM6.js} +4 -4
- package/dist/{global-workspace-CSIS62Z4.js → global-workspace-VG44RZUH.js} +5 -5
- package/dist/index.cjs +1548 -614
- package/dist/index.d.cts +226 -28
- package/dist/index.d.ts +226 -28
- package/dist/index.js +1277 -575
- package/dist/mcp/run-stdio.cjs +3153 -2509
- package/dist/mcp/run-stdio.js +690 -262
- package/dist/{workspaces-L4TXMUNM.js → workspaces-BQWQEZWE.js} +5 -5
- package/dist/{workspaces-W72KZL4B.js → workspaces-TYPJNUSM.js} +6 -6
- package/dist/{worktree-VOCH3WS3.js → worktree-5VLLFI5W.js} +5 -1
- package/dist/{worktree-KWXHMKRN.js → worktree-UNSOCYZU.js} +5 -1
- package/package.json +1 -1
- package/dist/caffeinate-hold-BEJIYPJ7.js +0 -107
- package/dist/chunk-JAWEDVVA.js +0 -106
package/dist/mcp/run-stdio.js
CHANGED
|
@@ -16,19 +16,22 @@ import {
|
|
|
16
16
|
resolveQuotaFallbackAgent,
|
|
17
17
|
sideboardMcpProfile,
|
|
18
18
|
summarizeTurnStderr
|
|
19
|
-
} from "../chunk-
|
|
19
|
+
} from "../chunk-P33ZQ7ZC.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-
|
|
26
|
+
} from "../chunk-5VTWBI3I.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-
|
|
44
|
+
} from "../chunk-6T2SKGCS.js";
|
|
42
45
|
import {
|
|
43
46
|
SLACK_REPLY_FORMATTING,
|
|
44
47
|
coordinatorSystemPrompt,
|
|
45
48
|
coordinatorTurnReminder,
|
|
46
49
|
enrichWorkspacesWithGithub,
|
|
47
50
|
ensureGlobalCoordinatorCwd
|
|
48
|
-
} from "../chunk-
|
|
51
|
+
} from "../chunk-HSLQXL6M.js";
|
|
49
52
|
import {
|
|
50
53
|
addPrStackLayer,
|
|
51
54
|
allocateTeamName,
|
|
@@ -54,9 +57,11 @@ import {
|
|
|
54
57
|
createExistingBranchWorktree,
|
|
55
58
|
createOrUpdatePr,
|
|
56
59
|
createThreadWorktree,
|
|
60
|
+
currentBranch,
|
|
57
61
|
detectGhStack,
|
|
58
62
|
fetchPrHead,
|
|
59
63
|
formatGhLandError,
|
|
64
|
+
formatGitAuthModeDirective,
|
|
60
65
|
getPr,
|
|
61
66
|
getPrChecks,
|
|
62
67
|
getPrDetails,
|
|
@@ -82,7 +87,7 @@ import {
|
|
|
82
87
|
takenSlugsFromThread,
|
|
83
88
|
threadDisplayLabel,
|
|
84
89
|
worktreeNameFromPath
|
|
85
|
-
} from "../chunk-
|
|
90
|
+
} from "../chunk-FUUVPN4A.js";
|
|
86
91
|
import {
|
|
87
92
|
ATTACHMENTS_DIR,
|
|
88
93
|
LEGACY_ATTACHMENTS_DIR,
|
|
@@ -122,7 +127,7 @@ import {
|
|
|
122
127
|
saveLinearOAuth,
|
|
123
128
|
updateAdvancedSettings,
|
|
124
129
|
writeSecureJson
|
|
125
|
-
} from "../chunk-
|
|
130
|
+
} from "../chunk-XKNBSYA7.js";
|
|
126
131
|
import {
|
|
127
132
|
writePrivateFile
|
|
128
133
|
} from "../chunk-NSTQ6QKD.js";
|
|
@@ -140,7 +145,7 @@ import {
|
|
|
140
145
|
// src/mcp/server.ts
|
|
141
146
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
142
147
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
143
|
-
import { z as
|
|
148
|
+
import { z as z3 } from "zod";
|
|
144
149
|
import { basename as basename4 } from "path";
|
|
145
150
|
|
|
146
151
|
// src/orchestrator/orchestrator.ts
|
|
@@ -341,8 +346,8 @@ ${input.permalink}` : "";
|
|
|
341
346
|
|
|
342
347
|
${body}${link}`;
|
|
343
348
|
}
|
|
344
|
-
function isSlackExternalReplyPrompt(
|
|
345
|
-
return
|
|
349
|
+
function isSlackExternalReplyPrompt(text3) {
|
|
350
|
+
return text3.startsWith("Slack reply from ") && text3.includes("not a command");
|
|
346
351
|
}
|
|
347
352
|
function pendingSlackExternalReplies(messages) {
|
|
348
353
|
let i = messages.length - 1;
|
|
@@ -366,9 +371,9 @@ function formatSlackRepliesForTurn(replies) {
|
|
|
366
371
|
function listSlackOutboundWatches() {
|
|
367
372
|
return pruneWatches(readStore3());
|
|
368
373
|
}
|
|
369
|
-
function formatOwnerSlackFyi(userName,
|
|
374
|
+
function formatOwnerSlackFyi(userName, text3) {
|
|
370
375
|
const who = userName.trim() || "Someone";
|
|
371
|
-
const body =
|
|
376
|
+
const body = text3.trim() || "(no text)";
|
|
372
377
|
return `${who} replied in Slack:
|
|
373
378
|
${body}`;
|
|
374
379
|
}
|
|
@@ -383,7 +388,7 @@ async function relayExternalReply(opts) {
|
|
|
383
388
|
const thread = readThread(threadId);
|
|
384
389
|
if (!thread || thread.status === "archived") return true;
|
|
385
390
|
try {
|
|
386
|
-
const
|
|
391
|
+
const text3 = formatSlackExternalReplyPrompt({
|
|
387
392
|
userName: opts.reply.userName,
|
|
388
393
|
kind: opts.watch.kind,
|
|
389
394
|
toLabel: opts.watch.toLabel,
|
|
@@ -392,7 +397,7 @@ async function relayExternalReply(opts) {
|
|
|
392
397
|
});
|
|
393
398
|
appendMessage(threadId, {
|
|
394
399
|
role: "agent",
|
|
395
|
-
text:
|
|
400
|
+
text: text3,
|
|
396
401
|
ts: (/* @__PURE__ */ new Date()).toISOString()
|
|
397
402
|
});
|
|
398
403
|
} catch {
|
|
@@ -576,7 +581,7 @@ async function refreshSlackReplyBadges(opts) {
|
|
|
576
581
|
const messages = await fetchMessages(token, watch, opts?.fetchImpl);
|
|
577
582
|
const replies = messages.filter((m) => isHumanReply(m, watch)).sort((a, b) => Number(a.ts) - Number(b.ts));
|
|
578
583
|
if (replies.length === 0) continue;
|
|
579
|
-
const
|
|
584
|
+
const injected2 = new Set(watch.injectedReplyTs ?? []);
|
|
580
585
|
const collected = [...watch.replies ?? []];
|
|
581
586
|
let lastSeenTs = watch.lastSeenTs;
|
|
582
587
|
let latestUser;
|
|
@@ -610,7 +615,7 @@ async function refreshSlackReplyBadges(opts) {
|
|
|
610
615
|
latestName = replyUserName;
|
|
611
616
|
latestText = reply.text;
|
|
612
617
|
latestPermalink = permalink;
|
|
613
|
-
if (
|
|
618
|
+
if (injected2.has(ts)) {
|
|
614
619
|
lastSeenTs = ts;
|
|
615
620
|
continue;
|
|
616
621
|
}
|
|
@@ -621,7 +626,7 @@ async function refreshSlackReplyBadges(opts) {
|
|
|
621
626
|
fetchImpl: opts?.fetchImpl
|
|
622
627
|
});
|
|
623
628
|
if (!delivered) break;
|
|
624
|
-
|
|
629
|
+
injected2.add(ts);
|
|
625
630
|
lastSeenTs = ts;
|
|
626
631
|
}
|
|
627
632
|
watches[i] = {
|
|
@@ -633,7 +638,7 @@ async function refreshSlackReplyBadges(opts) {
|
|
|
633
638
|
replyTs: lastSeenTs,
|
|
634
639
|
replyPreview: latestText.slice(0, 140),
|
|
635
640
|
permalink: latestPermalink,
|
|
636
|
-
injectedReplyTs: [...
|
|
641
|
+
injectedReplyTs: [...injected2],
|
|
637
642
|
replies: collected.slice(-MAX_REPLIES_PER_WATCH)
|
|
638
643
|
};
|
|
639
644
|
changed = true;
|
|
@@ -723,9 +728,9 @@ function toolFilePath(input) {
|
|
|
723
728
|
if (!input) return void 0;
|
|
724
729
|
return str(input.file_path) ?? str(input.path) ?? str(input.filePath) ?? str(input.filename);
|
|
725
730
|
}
|
|
726
|
-
function countLines(
|
|
727
|
-
if (!
|
|
728
|
-
return
|
|
731
|
+
function countLines(text3) {
|
|
732
|
+
if (!text3) return 0;
|
|
733
|
+
return text3.split("\n").length;
|
|
729
734
|
}
|
|
730
735
|
function diffFromInput(input) {
|
|
731
736
|
if (!input) return {};
|
|
@@ -852,9 +857,9 @@ function applyAgentEvent(parts, event) {
|
|
|
852
857
|
function partsToAssistantText(parts) {
|
|
853
858
|
return parts.filter((p) => p.type === "text").map((p) => p.text).join("").trim();
|
|
854
859
|
}
|
|
855
|
-
function stripBrightsyNdjsonNoise(
|
|
856
|
-
if (!
|
|
857
|
-
let out =
|
|
860
|
+
function stripBrightsyNdjsonNoise(text3) {
|
|
861
|
+
if (!text3 || !text3.includes('"type"')) return text3;
|
|
862
|
+
let out = text3;
|
|
858
863
|
out = out.replace(
|
|
859
864
|
/\{"type":"(?:tool_use|tool_result|tool|thinking|usage|done|error)"[\s\S]*?\}\s*(?=\{"type":"|$|(?=[A-Za-z*#]))/g,
|
|
860
865
|
""
|
|
@@ -913,9 +918,9 @@ async function spawnAgentTurn(thread, input, onEvent) {
|
|
|
913
918
|
`Cannot spawn ${thread.agent}: thread ${thread.id} has no worktreePath`
|
|
914
919
|
);
|
|
915
920
|
}
|
|
916
|
-
const { isGlobalThread: isGlobalThread2 } = await import("../global-workspace-
|
|
921
|
+
const { isGlobalThread: isGlobalThread2 } = await import("../global-workspace-RI367AM6.js");
|
|
917
922
|
if (isGlobalThread2(thread)) {
|
|
918
|
-
const { ensureGlobalCoordinatorCwd: ensureGlobalCoordinatorCwd2 } = await import("../coordinator-prompt-
|
|
923
|
+
const { ensureGlobalCoordinatorCwd: ensureGlobalCoordinatorCwd2 } = await import("../coordinator-prompt-K2R34A5T.js");
|
|
919
924
|
ensureGlobalCoordinatorCwd2(
|
|
920
925
|
isOrchestratorThread(thread) ? { orchestratorThreadId: thread.id } : void 0
|
|
921
926
|
);
|
|
@@ -932,8 +937,13 @@ async function spawnAgentTurn(thread, input, onEvent) {
|
|
|
932
937
|
}
|
|
933
938
|
const env = childEnvWithAppSettings(cmd.env);
|
|
934
939
|
if (!isOrchestratorThread(thread)) {
|
|
935
|
-
|
|
936
|
-
|
|
940
|
+
try {
|
|
941
|
+
const originEnv = await originGhRepoEnv(thread.worktreePath, { env });
|
|
942
|
+
Object.assign(env, originEnv);
|
|
943
|
+
} catch (err) {
|
|
944
|
+
const detail = err instanceof Error ? err.message : String(err);
|
|
945
|
+
console.warn(`[sideboard] originGhRepoEnv failed (${thread.worktreePath}): ${detail}`);
|
|
946
|
+
}
|
|
937
947
|
}
|
|
938
948
|
const child = execa(cmd.file, cmd.args, {
|
|
939
949
|
cwd: cmd.cwd,
|
|
@@ -1002,8 +1012,8 @@ async function spawnAgentTurn(thread, input, onEvent) {
|
|
|
1002
1012
|
onEvent({ type: "exit", data: exitCode });
|
|
1003
1013
|
const finalized = finalizeParts(parts);
|
|
1004
1014
|
const rawText = assistantText.trim() || partsToAssistantText(finalized);
|
|
1005
|
-
const
|
|
1006
|
-
return { exitCode, sessionId, assistantText:
|
|
1015
|
+
const text3 = thread.agent === "brightsy" ? stripBrightsyNdjsonNoise(rawText) : rawText;
|
|
1016
|
+
return { exitCode, sessionId, assistantText: text3, parts: finalized, usage };
|
|
1007
1017
|
});
|
|
1008
1018
|
return {
|
|
1009
1019
|
pid: child.pid,
|
|
@@ -1017,6 +1027,80 @@ async function spawnAgentTurn(thread, input, onEvent) {
|
|
|
1017
1027
|
};
|
|
1018
1028
|
}
|
|
1019
1029
|
|
|
1030
|
+
// src/land/pr-metadata.ts
|
|
1031
|
+
var GENERIC_TITLE_RE = /^(sideboard:|thread\/|wip\b|tmp\b|update\b|changes?\b|misc\b|stuff\b)/i;
|
|
1032
|
+
function looksGeneric(title) {
|
|
1033
|
+
const t = title.trim();
|
|
1034
|
+
if (!t) return true;
|
|
1035
|
+
if (t.length < 4) return true;
|
|
1036
|
+
if (GENERIC_TITLE_RE.test(t)) return true;
|
|
1037
|
+
if (!/[\s:/\-_]/.test(t) && t.length <= 24) return true;
|
|
1038
|
+
return false;
|
|
1039
|
+
}
|
|
1040
|
+
function truncateTitle(title, max = 72) {
|
|
1041
|
+
const t = title.trim().replace(/\s+/g, " ");
|
|
1042
|
+
if (t.length <= max) return t;
|
|
1043
|
+
return `${t.slice(0, max - 1).trimEnd()}\u2026`;
|
|
1044
|
+
}
|
|
1045
|
+
function titleFromPaths(paths) {
|
|
1046
|
+
if (paths.length === 0) return null;
|
|
1047
|
+
const top = paths.slice(0, 3).map((p) => p.replace(/\/$/, ""));
|
|
1048
|
+
if (paths.length === 1) return truncateTitle(`Update ${top[0]}`);
|
|
1049
|
+
if (paths.length <= 3) return truncateTitle(`Update ${top.join(", ")}`);
|
|
1050
|
+
return truncateTitle(`Update ${top[0]} and ${paths.length - 1} more files`);
|
|
1051
|
+
}
|
|
1052
|
+
async function suggestPrMetadata(worktreePath, opts) {
|
|
1053
|
+
const [{ stdout: logOut }, { stdout: nameStatus }, { stdout: statOut }] = await Promise.all([
|
|
1054
|
+
git(["log", "--format=%s", `${opts.base}..HEAD`], worktreePath, {
|
|
1055
|
+
reject: false
|
|
1056
|
+
}),
|
|
1057
|
+
git(["diff", "--name-only", `${opts.base}...HEAD`], worktreePath, {
|
|
1058
|
+
reject: false
|
|
1059
|
+
}),
|
|
1060
|
+
git(["diff", "--stat", `${opts.base}...HEAD`], worktreePath, {
|
|
1061
|
+
reject: false
|
|
1062
|
+
})
|
|
1063
|
+
]);
|
|
1064
|
+
const { stdout: dirtyNames } = await git(["diff", "--name-only", "HEAD"], worktreePath, {
|
|
1065
|
+
reject: false
|
|
1066
|
+
});
|
|
1067
|
+
const { stdout: untracked } = await git(
|
|
1068
|
+
["ls-files", "--others", "--exclude-standard"],
|
|
1069
|
+
worktreePath,
|
|
1070
|
+
{ reject: false }
|
|
1071
|
+
);
|
|
1072
|
+
const subjects = logOut.split("\n").map((s) => s.trim()).filter(Boolean);
|
|
1073
|
+
const paths = [
|
|
1074
|
+
...new Set(
|
|
1075
|
+
[...nameStatus.split("\n"), ...dirtyNames.split("\n"), ...untracked.split("\n")].map((p) => p.trim()).filter(Boolean)
|
|
1076
|
+
)
|
|
1077
|
+
].sort((a, b) => a.localeCompare(b));
|
|
1078
|
+
const meaningfulSubjects = subjects.filter((s) => !looksGeneric(s));
|
|
1079
|
+
const fallback = opts.fallbackTitle?.trim() || null;
|
|
1080
|
+
const fallbackOk = fallback && !looksGeneric(fallback) ? fallback : null;
|
|
1081
|
+
const title = truncateTitle(
|
|
1082
|
+
meaningfulSubjects[0] ?? fallbackOk ?? titleFromPaths(paths) ?? "Sideboard changes"
|
|
1083
|
+
);
|
|
1084
|
+
const commitMessage = meaningfulSubjects[0] ?? fallbackOk ?? titleFromPaths(paths) ?? `sideboard: ${title}`;
|
|
1085
|
+
const summaryLines = meaningfulSubjects.length > 0 ? meaningfulSubjects.slice(0, 12).map((s) => `- ${s}`) : paths.slice(0, 20).map((p) => `- \`${p}\``);
|
|
1086
|
+
const bodyParts = [
|
|
1087
|
+
"## Summary",
|
|
1088
|
+
summaryLines.length > 0 ? summaryLines.join("\n") : "- Changes landed via Sideboard.",
|
|
1089
|
+
""
|
|
1090
|
+
];
|
|
1091
|
+
if (statOut.trim()) {
|
|
1092
|
+
bodyParts.push("## Diff", "```", statOut.trim(), "```", "");
|
|
1093
|
+
}
|
|
1094
|
+
if (opts.sourceLabel) {
|
|
1095
|
+
bodyParts.push(`_Landed via Sideboard from ${opts.sourceLabel}_`);
|
|
1096
|
+
}
|
|
1097
|
+
return {
|
|
1098
|
+
title,
|
|
1099
|
+
body: bodyParts.join("\n").trim() + "\n",
|
|
1100
|
+
commitMessage: truncateTitle(commitMessage, 72)
|
|
1101
|
+
};
|
|
1102
|
+
}
|
|
1103
|
+
|
|
1020
1104
|
// src/git/agent-git-actions.ts
|
|
1021
1105
|
var AGENT_GIT_ACTIONS = [
|
|
1022
1106
|
"commit-push",
|
|
@@ -1151,8 +1235,20 @@ async function captureLoginEnv() {
|
|
|
1151
1235
|
cachedLoginEnv = { ...process.env };
|
|
1152
1236
|
return { ...cachedLoginEnv };
|
|
1153
1237
|
}
|
|
1238
|
+
var NESTED_ELECTRON_ENV_PREFIXES = ["ELECTRON_", "CHROME_"];
|
|
1239
|
+
function stripNestedElectronEnv(env) {
|
|
1240
|
+
const out = { ...env };
|
|
1241
|
+
for (const key of Object.keys(out)) {
|
|
1242
|
+
if (NESTED_ELECTRON_ENV_PREFIXES.some((prefix) => key.startsWith(prefix))) {
|
|
1243
|
+
delete out[key];
|
|
1244
|
+
}
|
|
1245
|
+
}
|
|
1246
|
+
return out;
|
|
1247
|
+
}
|
|
1154
1248
|
function buildWorkspaceScriptEnv(opts, baseEnv) {
|
|
1155
|
-
const env = {
|
|
1249
|
+
const env = stripNestedElectronEnv({
|
|
1250
|
+
...baseEnv ?? process.env
|
|
1251
|
+
});
|
|
1156
1252
|
const name = opts.workspaceName ?? basename(opts.worktreePath);
|
|
1157
1253
|
const ports = opts.ports ?? [];
|
|
1158
1254
|
const primary = ports[0];
|
|
@@ -1756,7 +1852,7 @@ async function createThread(input, _onSetupLine) {
|
|
|
1756
1852
|
return readThread(thread.id) ?? thread;
|
|
1757
1853
|
}
|
|
1758
1854
|
async function listLinearIssues(agent, repoPath) {
|
|
1759
|
-
const { getAdapter: getAdapter2 } = await import("../agents-
|
|
1855
|
+
const { getAdapter: getAdapter2 } = await import("../agents-GRAPUXWW.js");
|
|
1760
1856
|
await requireAgent(agent, { requireLinear: true });
|
|
1761
1857
|
const adapter = getAdapter2(agent);
|
|
1762
1858
|
if (!adapter.listLinearIssues) {
|
|
@@ -1812,9 +1908,9 @@ async function tryClaudeSummary(transcript, opts) {
|
|
|
1812
1908
|
{ cwd: opts?.cwd, reject: false }
|
|
1813
1909
|
);
|
|
1814
1910
|
if (exitCode !== 0) return null;
|
|
1815
|
-
const
|
|
1816
|
-
if (
|
|
1817
|
-
return
|
|
1911
|
+
const text3 = stdout.trim();
|
|
1912
|
+
if (text3.length < 40) return null;
|
|
1913
|
+
return text3;
|
|
1818
1914
|
} catch {
|
|
1819
1915
|
return null;
|
|
1820
1916
|
}
|
|
@@ -1863,9 +1959,9 @@ function extractiveSummary(transcript) {
|
|
|
1863
1959
|
);
|
|
1864
1960
|
return parts.join("\n");
|
|
1865
1961
|
}
|
|
1866
|
-
function clipSummary(
|
|
1867
|
-
if (
|
|
1868
|
-
return `${
|
|
1962
|
+
function clipSummary(text3) {
|
|
1963
|
+
if (text3.length <= MAX_SUMMARY_CHARS) return text3;
|
|
1964
|
+
return `${text3.slice(0, MAX_SUMMARY_CHARS)}
|
|
1869
1965
|
|
|
1870
1966
|
[\u2026summary truncated\u2026]`;
|
|
1871
1967
|
}
|
|
@@ -2478,8 +2574,8 @@ function buildQuotaHandoffAttachment(from, limitText, fallbackAgent) {
|
|
|
2478
2574
|
);
|
|
2479
2575
|
const recent = from.messages.slice(-8).map((m) => {
|
|
2480
2576
|
const role = m.role === "user" ? "User" : m.role === "agent" ? "Agent" : "Summary";
|
|
2481
|
-
const
|
|
2482
|
-
return
|
|
2577
|
+
const text3 = m.text.trim().replace(/\s+/g, " ").slice(0, 280);
|
|
2578
|
+
return text3 ? `- ${role}: ${text3}` : null;
|
|
2483
2579
|
}).filter(Boolean);
|
|
2484
2580
|
const body = [
|
|
2485
2581
|
`# Orchestration handoff`,
|
|
@@ -2575,13 +2671,13 @@ function resolveConductorCursorAgentId(workspacePath) {
|
|
|
2575
2671
|
for (const hash of hashes) {
|
|
2576
2672
|
const agentsFile = join9(CURSOR_SDK_STORE, hash, "agents.ndjson");
|
|
2577
2673
|
if (!existsSync9(agentsFile)) continue;
|
|
2578
|
-
let
|
|
2674
|
+
let text3;
|
|
2579
2675
|
try {
|
|
2580
|
-
|
|
2676
|
+
text3 = readFileSync6(agentsFile, "utf8");
|
|
2581
2677
|
} catch {
|
|
2582
2678
|
continue;
|
|
2583
2679
|
}
|
|
2584
|
-
for (const line of
|
|
2680
|
+
for (const line of text3.split("\n")) {
|
|
2585
2681
|
const trimmed = line.trim();
|
|
2586
2682
|
if (!trimmed) continue;
|
|
2587
2683
|
try {
|
|
@@ -2620,7 +2716,7 @@ async function adoptThread(input) {
|
|
|
2620
2716
|
messages: input.messages ?? []
|
|
2621
2717
|
});
|
|
2622
2718
|
writeThread(thread);
|
|
2623
|
-
const { ensureWorkspace: ensureWorkspace2 } = await import("../workspaces-
|
|
2719
|
+
const { ensureWorkspace: ensureWorkspace2 } = await import("../workspaces-BQWQEZWE.js");
|
|
2624
2720
|
await ensureWorkspace2(repoPath);
|
|
2625
2721
|
return thread;
|
|
2626
2722
|
}
|
|
@@ -3344,13 +3440,13 @@ function formatStat(files) {
|
|
|
3344
3440
|
return `${n} file${n === 1 ? "" : "s"} changed, ${additions} insertions(+), ${deletions} deletions(-)`;
|
|
3345
3441
|
}
|
|
3346
3442
|
function emptyScopeStats() {
|
|
3347
|
-
const
|
|
3443
|
+
const z4 = emptyStat();
|
|
3348
3444
|
return {
|
|
3349
|
-
commits:
|
|
3350
|
-
uncommitted:
|
|
3351
|
-
staged:
|
|
3352
|
-
unstaged:
|
|
3353
|
-
last_turn:
|
|
3445
|
+
commits: z4,
|
|
3446
|
+
uncommitted: z4,
|
|
3447
|
+
staged: z4,
|
|
3448
|
+
unstaged: z4,
|
|
3449
|
+
last_turn: z4
|
|
3354
3450
|
};
|
|
3355
3451
|
}
|
|
3356
3452
|
function filesFromDiff(nameStatus, numstat, combinedDiff, maxHunk) {
|
|
@@ -3839,80 +3935,6 @@ async function getDiffSummary(worktreePath, repoPath, opts) {
|
|
|
3839
3935
|
};
|
|
3840
3936
|
}
|
|
3841
3937
|
|
|
3842
|
-
// src/land/pr-metadata.ts
|
|
3843
|
-
var GENERIC_TITLE_RE = /^(sideboard:|thread\/|wip\b|tmp\b|update\b|changes?\b|misc\b|stuff\b)/i;
|
|
3844
|
-
function looksGeneric(title) {
|
|
3845
|
-
const t = title.trim();
|
|
3846
|
-
if (!t) return true;
|
|
3847
|
-
if (t.length < 4) return true;
|
|
3848
|
-
if (GENERIC_TITLE_RE.test(t)) return true;
|
|
3849
|
-
if (!/[\s:/\-_]/.test(t) && t.length <= 24) return true;
|
|
3850
|
-
return false;
|
|
3851
|
-
}
|
|
3852
|
-
function truncateTitle(title, max = 72) {
|
|
3853
|
-
const t = title.trim().replace(/\s+/g, " ");
|
|
3854
|
-
if (t.length <= max) return t;
|
|
3855
|
-
return `${t.slice(0, max - 1).trimEnd()}\u2026`;
|
|
3856
|
-
}
|
|
3857
|
-
function titleFromPaths(paths) {
|
|
3858
|
-
if (paths.length === 0) return null;
|
|
3859
|
-
const top = paths.slice(0, 3).map((p) => p.replace(/\/$/, ""));
|
|
3860
|
-
if (paths.length === 1) return truncateTitle(`Update ${top[0]}`);
|
|
3861
|
-
if (paths.length <= 3) return truncateTitle(`Update ${top.join(", ")}`);
|
|
3862
|
-
return truncateTitle(`Update ${top[0]} and ${paths.length - 1} more files`);
|
|
3863
|
-
}
|
|
3864
|
-
async function suggestPrMetadata(worktreePath, opts) {
|
|
3865
|
-
const [{ stdout: logOut }, { stdout: nameStatus }, { stdout: statOut }] = await Promise.all([
|
|
3866
|
-
git(["log", "--format=%s", `${opts.base}..HEAD`], worktreePath, {
|
|
3867
|
-
reject: false
|
|
3868
|
-
}),
|
|
3869
|
-
git(["diff", "--name-only", `${opts.base}...HEAD`], worktreePath, {
|
|
3870
|
-
reject: false
|
|
3871
|
-
}),
|
|
3872
|
-
git(["diff", "--stat", `${opts.base}...HEAD`], worktreePath, {
|
|
3873
|
-
reject: false
|
|
3874
|
-
})
|
|
3875
|
-
]);
|
|
3876
|
-
const { stdout: dirtyNames } = await git(["diff", "--name-only", "HEAD"], worktreePath, {
|
|
3877
|
-
reject: false
|
|
3878
|
-
});
|
|
3879
|
-
const { stdout: untracked } = await git(
|
|
3880
|
-
["ls-files", "--others", "--exclude-standard"],
|
|
3881
|
-
worktreePath,
|
|
3882
|
-
{ reject: false }
|
|
3883
|
-
);
|
|
3884
|
-
const subjects = logOut.split("\n").map((s) => s.trim()).filter(Boolean);
|
|
3885
|
-
const paths = [
|
|
3886
|
-
...new Set(
|
|
3887
|
-
[...nameStatus.split("\n"), ...dirtyNames.split("\n"), ...untracked.split("\n")].map((p) => p.trim()).filter(Boolean)
|
|
3888
|
-
)
|
|
3889
|
-
].sort((a, b) => a.localeCompare(b));
|
|
3890
|
-
const meaningfulSubjects = subjects.filter((s) => !looksGeneric(s));
|
|
3891
|
-
const fallback = opts.fallbackTitle?.trim() || null;
|
|
3892
|
-
const fallbackOk = fallback && !looksGeneric(fallback) ? fallback : null;
|
|
3893
|
-
const title = truncateTitle(
|
|
3894
|
-
meaningfulSubjects[0] ?? fallbackOk ?? titleFromPaths(paths) ?? "Sideboard changes"
|
|
3895
|
-
);
|
|
3896
|
-
const commitMessage = meaningfulSubjects[0] ?? fallbackOk ?? titleFromPaths(paths) ?? `sideboard: ${title}`;
|
|
3897
|
-
const summaryLines = meaningfulSubjects.length > 0 ? meaningfulSubjects.slice(0, 12).map((s) => `- ${s}`) : paths.slice(0, 20).map((p) => `- \`${p}\``);
|
|
3898
|
-
const bodyParts = [
|
|
3899
|
-
"## Summary",
|
|
3900
|
-
summaryLines.length > 0 ? summaryLines.join("\n") : "- Changes landed via Sideboard.",
|
|
3901
|
-
""
|
|
3902
|
-
];
|
|
3903
|
-
if (statOut.trim()) {
|
|
3904
|
-
bodyParts.push("## Diff", "```", statOut.trim(), "```", "");
|
|
3905
|
-
}
|
|
3906
|
-
if (opts.sourceLabel) {
|
|
3907
|
-
bodyParts.push(`_Landed via Sideboard from ${opts.sourceLabel}_`);
|
|
3908
|
-
}
|
|
3909
|
-
return {
|
|
3910
|
-
title,
|
|
3911
|
-
body: bodyParts.join("\n").trim() + "\n",
|
|
3912
|
-
commitMessage: truncateTitle(commitMessage, 72)
|
|
3913
|
-
};
|
|
3914
|
-
}
|
|
3915
|
-
|
|
3916
3938
|
// src/land/land.ts
|
|
3917
3939
|
async function previewLand(thread) {
|
|
3918
3940
|
if (thread.sourceIsFork) {
|
|
@@ -4456,6 +4478,8 @@ function formatWorktreeDirective(thread, opts) {
|
|
|
4456
4478
|
lines.push(
|
|
4457
4479
|
"- Push with `git push -u origin HEAD` (or the current branch name). Do not `git push upstream`."
|
|
4458
4480
|
);
|
|
4481
|
+
lines.push("");
|
|
4482
|
+
lines.push(formatGitAuthModeDirective(opts?.gitAuthMode ?? "auto"));
|
|
4459
4483
|
lines.push(
|
|
4460
4484
|
"- Derive the PR title and body from what the changes actually do and why \u2014 inspect the diff/commits and the user request. Do not use the soccer-team worktree nickname or placeholder branch as the PR title."
|
|
4461
4485
|
);
|
|
@@ -4660,7 +4684,7 @@ var Orchestrator = class {
|
|
|
4660
4684
|
orphans: orphans.map((o) => ({ path: o.path, repoPath: o.repoPath }))
|
|
4661
4685
|
});
|
|
4662
4686
|
}
|
|
4663
|
-
const { autoCleanupOrphansEnabled } = await import("../app-settings-
|
|
4687
|
+
const { autoCleanupOrphansEnabled } = await import("../app-settings-NILNWNNQ.js");
|
|
4664
4688
|
if (autoCleanupOrphansEnabled() && shouldRunWorktreeCleanup() && orphans.length > 0) {
|
|
4665
4689
|
await cleanupOrphanWorktrees({ repoPaths });
|
|
4666
4690
|
}
|
|
@@ -4817,7 +4841,7 @@ var Orchestrator = class {
|
|
|
4817
4841
|
}
|
|
4818
4842
|
async finishCreateThread(threadId, prompt) {
|
|
4819
4843
|
await this.runSetupAfterCreate(threadId);
|
|
4820
|
-
const { autoRunAfterSetupEnabled } = await import("../app-settings-
|
|
4844
|
+
const { autoRunAfterSetupEnabled } = await import("../app-settings-NILNWNNQ.js");
|
|
4821
4845
|
if (autoRunAfterSetupEnabled()) {
|
|
4822
4846
|
try {
|
|
4823
4847
|
await this.startDev(threadId);
|
|
@@ -4897,11 +4921,11 @@ var Orchestrator = class {
|
|
|
4897
4921
|
return results;
|
|
4898
4922
|
}
|
|
4899
4923
|
/** Edit the text of a not-yet-started queued message. */
|
|
4900
|
-
async editQueuedMessage(threadRef, index,
|
|
4924
|
+
async editQueuedMessage(threadRef, index, text3) {
|
|
4901
4925
|
const thread = this.requireThread(threadRef);
|
|
4902
4926
|
return withThreadLock(thread.id, async () => {
|
|
4903
4927
|
const current = this.requireThread(thread.id);
|
|
4904
|
-
const trimmed =
|
|
4928
|
+
const trimmed = text3.trim();
|
|
4905
4929
|
if (!trimmed || index < 0 || index >= current.queue.length) {
|
|
4906
4930
|
return current;
|
|
4907
4931
|
}
|
|
@@ -5095,14 +5119,16 @@ var Orchestrator = class {
|
|
|
5095
5119
|
}
|
|
5096
5120
|
const isBrightsy = fresh.agent === "brightsy";
|
|
5097
5121
|
const isOrchestration = isOrchestratorThread(fresh);
|
|
5122
|
+
const { autoRenameBranchEnabled, getGithubGitAuthMode } = await import("../app-settings-NILNWNNQ.js");
|
|
5123
|
+
const gitAuthMode = getGithubGitAuthMode();
|
|
5098
5124
|
const worktreeDirective = isBrightsy || isOrchestration ? null : formatWorktreeDirective(fresh, {
|
|
5099
5125
|
githubSlug: await resolveGithubRepoSlug(fresh.worktreePath).catch(
|
|
5100
5126
|
() => null
|
|
5101
|
-
)
|
|
5127
|
+
),
|
|
5128
|
+
gitAuthMode
|
|
5102
5129
|
});
|
|
5103
5130
|
const artifactDirective = isBrightsy ? null : formatArtifactDirective();
|
|
5104
5131
|
const settings = loadWorkspaceSettings(fresh.worktreePath, fresh.repoPath);
|
|
5105
|
-
const { autoRenameBranchEnabled } = await import("../app-settings-P42Z3VOB.js");
|
|
5106
5132
|
const renameBranchDirective = !isBrightsy && !isOrchestration && autoRenameBranchEnabled() ? formatRenameBranchDirective(fresh, {
|
|
5107
5133
|
customPrompt: settings?.prompts?.renameBranch
|
|
5108
5134
|
}) : null;
|
|
@@ -5661,6 +5687,14 @@ var Orchestrator = class {
|
|
|
5661
5687
|
throw new Error(`${action} is not available on the global coordinator`);
|
|
5662
5688
|
}
|
|
5663
5689
|
}
|
|
5690
|
+
/** MCP set_caffeinate is a detached hold — closing the chat must not leave the Mac awake. */
|
|
5691
|
+
releaseOrchestratorCaffeinate(thread) {
|
|
5692
|
+
if (!isOrchestratorThread(thread)) return;
|
|
5693
|
+
try {
|
|
5694
|
+
releaseCaffeinateHoldForThread(thread.id);
|
|
5695
|
+
} catch {
|
|
5696
|
+
}
|
|
5697
|
+
}
|
|
5664
5698
|
async diff(threadRef, opts) {
|
|
5665
5699
|
const thread = this.requireThread(threadRef);
|
|
5666
5700
|
this.assertNotGlobal(thread, "Diff");
|
|
@@ -5800,7 +5834,7 @@ var Orchestrator = class {
|
|
|
5800
5834
|
updateThread(thread.id, { title: meta.title });
|
|
5801
5835
|
}
|
|
5802
5836
|
}
|
|
5803
|
-
const { autoArchiveOnMergeEnabled } = await import("../app-settings-
|
|
5837
|
+
const { autoArchiveOnMergeEnabled } = await import("../app-settings-NILNWNNQ.js");
|
|
5804
5838
|
const latest = this.requireThread(thread.id);
|
|
5805
5839
|
if (!shouldAutoArchiveOnPrMerge({
|
|
5806
5840
|
previousPrState: prevState || null,
|
|
@@ -5931,8 +5965,9 @@ var Orchestrator = class {
|
|
|
5931
5965
|
return tab;
|
|
5932
5966
|
}
|
|
5933
5967
|
/**
|
|
5934
|
-
*
|
|
5935
|
-
*
|
|
5968
|
+
* Desktop git buttons + MCP `ask_git`.
|
|
5969
|
+
* When the worktree is clean, push / open the PR here (HTTPS via `gh` if SSH
|
|
5970
|
+
* is missing). When dirty, queue the worktree agent to commit first.
|
|
5936
5971
|
*/
|
|
5937
5972
|
async askGit(threadRef, action) {
|
|
5938
5973
|
if (!AGENT_GIT_ACTIONS.includes(action)) {
|
|
@@ -5950,6 +5985,10 @@ var Orchestrator = class {
|
|
|
5950
5985
|
"No pull request linked. Ask the worktree agent to open a draft PR first (ask_git create-draft)."
|
|
5951
5986
|
);
|
|
5952
5987
|
}
|
|
5988
|
+
if ((action === "commit-push" || action === "create-draft" || action === "create-web") && thread.worktreePath?.trim() && !await isDirty(thread.worktreePath)) {
|
|
5989
|
+
await this.pushAndMaybeOpenPr(thread, action);
|
|
5990
|
+
return this.requireThread(threadRef);
|
|
5991
|
+
}
|
|
5953
5992
|
let prBase;
|
|
5954
5993
|
if (action === "resolve-conflicts") {
|
|
5955
5994
|
try {
|
|
@@ -5960,6 +5999,34 @@ var Orchestrator = class {
|
|
|
5960
5999
|
}
|
|
5961
6000
|
return this.send(threadRef, agentGitPrompt(action, { prBase }));
|
|
5962
6001
|
}
|
|
6002
|
+
/** Push origin (gh HTTPS fallback) and create/update the PR when requested. */
|
|
6003
|
+
async pushAndMaybeOpenPr(thread, action) {
|
|
6004
|
+
const cwd = thread.worktreePath;
|
|
6005
|
+
const branch = await currentBranch(cwd);
|
|
6006
|
+
await pushBranch(cwd, branch);
|
|
6007
|
+
if (action === "commit-push") return;
|
|
6008
|
+
const base = await resolveDefaultBranch(thread.repoPath);
|
|
6009
|
+
const meta = await suggestPrMetadata(cwd, {
|
|
6010
|
+
base,
|
|
6011
|
+
fallbackTitle: thread.prTitle ?? thread.title
|
|
6012
|
+
});
|
|
6013
|
+
const url = await createOrUpdatePr(cwd, {
|
|
6014
|
+
title: meta.title,
|
|
6015
|
+
body: meta.body,
|
|
6016
|
+
base,
|
|
6017
|
+
head: branch,
|
|
6018
|
+
draft: action === "create-draft",
|
|
6019
|
+
web: action === "create-web"
|
|
6020
|
+
});
|
|
6021
|
+
if (!url) return;
|
|
6022
|
+
const patch = { prUrl: url, prTitle: meta.title };
|
|
6023
|
+
try {
|
|
6024
|
+
const fetched = await getPrMeta(cwd, url);
|
|
6025
|
+
if (fetched?.title) patch.prTitle = fetched.title;
|
|
6026
|
+
} catch {
|
|
6027
|
+
}
|
|
6028
|
+
updateThread(thread.id, patch);
|
|
6029
|
+
}
|
|
5963
6030
|
setThreadOptions(threadRef, patch) {
|
|
5964
6031
|
const thread = this.requireThread(threadRef);
|
|
5965
6032
|
const next = {};
|
|
@@ -6030,6 +6097,7 @@ var Orchestrator = class {
|
|
|
6030
6097
|
async archive(threadRef) {
|
|
6031
6098
|
const thread = this.requireThread(threadRef);
|
|
6032
6099
|
this.stop(thread.id);
|
|
6100
|
+
this.releaseOrchestratorCaffeinate(thread);
|
|
6033
6101
|
if (isGlobalThread(thread)) {
|
|
6034
6102
|
const archived2 = setStatus(thread.id, "archived");
|
|
6035
6103
|
this.emit({ type: "status_changed", threadId: archived2.id, status: "archived" });
|
|
@@ -6054,7 +6122,7 @@ var Orchestrator = class {
|
|
|
6054
6122
|
this.emit({ type: "status_changed", threadId: archived.id, status: "archived" });
|
|
6055
6123
|
if (thread.repoPath && !isGlobalRepoPath(thread.repoPath)) {
|
|
6056
6124
|
try {
|
|
6057
|
-
const { ensureWorkspace: ensureWorkspace2 } = await import("../workspaces-
|
|
6125
|
+
const { ensureWorkspace: ensureWorkspace2 } = await import("../workspaces-BQWQEZWE.js");
|
|
6058
6126
|
await ensureWorkspace2(thread.repoPath);
|
|
6059
6127
|
} catch {
|
|
6060
6128
|
}
|
|
@@ -6064,6 +6132,7 @@ var Orchestrator = class {
|
|
|
6064
6132
|
async purge(threadRef, opts) {
|
|
6065
6133
|
const thread = this.requireThread(threadRef);
|
|
6066
6134
|
this.stop(thread.id);
|
|
6135
|
+
this.releaseOrchestratorCaffeinate(thread);
|
|
6067
6136
|
if (isGlobalThread(thread)) {
|
|
6068
6137
|
deleteThreadRecord(thread.id);
|
|
6069
6138
|
return;
|
|
@@ -6075,7 +6144,7 @@ var Orchestrator = class {
|
|
|
6075
6144
|
await runArchiveScript(thread.repoPath, thread.worktreePath);
|
|
6076
6145
|
} catch {
|
|
6077
6146
|
}
|
|
6078
|
-
const { deleteBranchOnPurgeEnabled } = await import("../app-settings-
|
|
6147
|
+
const { deleteBranchOnPurgeEnabled } = await import("../app-settings-NILNWNNQ.js");
|
|
6079
6148
|
const deleteBranch = opts?.deleteBranch ?? deleteBranchOnPurgeEnabled();
|
|
6080
6149
|
await removeWorktree(thread.repoPath, thread.worktreePath, {
|
|
6081
6150
|
deleteBranch: deleteBranch ? thread.branchName : void 0
|
|
@@ -6096,7 +6165,7 @@ var Orchestrator = class {
|
|
|
6096
6165
|
return restored2;
|
|
6097
6166
|
}
|
|
6098
6167
|
if (!existsSync15(thread.worktreePath)) {
|
|
6099
|
-
const { createThreadWorktree: createThreadWorktree2 } = await import("../worktree-
|
|
6168
|
+
const { createThreadWorktree: createThreadWorktree2 } = await import("../worktree-UNSOCYZU.js");
|
|
6100
6169
|
const { execa: execa6 } = await import("execa");
|
|
6101
6170
|
const slug = thread.worktreePath.split("/").pop();
|
|
6102
6171
|
const dest = thread.worktreePath;
|
|
@@ -6168,13 +6237,36 @@ function getOrchestrator() {
|
|
|
6168
6237
|
return singleton;
|
|
6169
6238
|
}
|
|
6170
6239
|
|
|
6240
|
+
// src/http/fetch.ts
|
|
6241
|
+
var injected = null;
|
|
6242
|
+
function formatFetchError(err, url) {
|
|
6243
|
+
if (!(err instanceof Error)) return `${String(err)} (${url})`;
|
|
6244
|
+
const cause = err.cause;
|
|
6245
|
+
let detail = "";
|
|
6246
|
+
if (cause instanceof Error) {
|
|
6247
|
+
const code = typeof cause.code === "string" ? cause.code : void 0;
|
|
6248
|
+
detail = code ? ` [${code}: ${cause.message}]` : ` [${cause.message}]`;
|
|
6249
|
+
} else if (cause != null) {
|
|
6250
|
+
detail = ` [${String(cause)}]`;
|
|
6251
|
+
}
|
|
6252
|
+
return `${err.message}${detail} (${url})`;
|
|
6253
|
+
}
|
|
6254
|
+
async function httpFetch(input, init) {
|
|
6255
|
+
const url = typeof input === "string" ? input : input.href;
|
|
6256
|
+
const fn = injected ?? globalThis.fetch.bind(globalThis);
|
|
6257
|
+
try {
|
|
6258
|
+
return await fn(url, init);
|
|
6259
|
+
} catch (err) {
|
|
6260
|
+
throw new Error(formatFetchError(err, url));
|
|
6261
|
+
}
|
|
6262
|
+
}
|
|
6263
|
+
|
|
6171
6264
|
// src/integrations/linear-oauth.ts
|
|
6172
6265
|
import { createHash, randomBytes } from "crypto";
|
|
6173
6266
|
import { createServer as createServer2 } from "http";
|
|
6174
6267
|
|
|
6175
6268
|
// src/integrations/linear-app.ts
|
|
6176
6269
|
var BAKED_LINEAR_CLIENT_ID = "39a15abc7ea5bea17c47f47f85ff5fd0";
|
|
6177
|
-
var BAKED_LINEAR_CLIENT_SECRET = "";
|
|
6178
6270
|
|
|
6179
6271
|
// src/integrations/linear-oauth.ts
|
|
6180
6272
|
var LINEAR_OAUTH_PORT = 19848;
|
|
@@ -6184,7 +6276,7 @@ var REFRESH_SKEW_MS = 5 * 6e4;
|
|
|
6184
6276
|
function linearOAuthCredentials() {
|
|
6185
6277
|
const settings = loadAppSettings();
|
|
6186
6278
|
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() ||
|
|
6279
|
+
const clientSecret = process.env.SIDEBOARD_LINEAR_CLIENT_SECRET?.trim() || settings.integrations.linearClientSecret?.trim() || "";
|
|
6188
6280
|
if (!clientId) {
|
|
6189
6281
|
throw new Error(
|
|
6190
6282
|
`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 +6292,7 @@ function linearAuthorizationHeader(token) {
|
|
|
6200
6292
|
return `Bearer ${t}`;
|
|
6201
6293
|
}
|
|
6202
6294
|
async function exchangeLinearToken(body) {
|
|
6203
|
-
const res = await
|
|
6295
|
+
const res = await httpFetch(LINEAR_TOKEN, {
|
|
6204
6296
|
method: "POST",
|
|
6205
6297
|
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
6206
6298
|
body
|
|
@@ -6267,6 +6359,18 @@ async function getLinearAuthToken(settings = loadAppSettings()) {
|
|
|
6267
6359
|
|
|
6268
6360
|
// src/integrations/linear.ts
|
|
6269
6361
|
var LINEAR_GRAPHQL = "https://api.linear.app/graphql";
|
|
6362
|
+
var ISSUE_FIELDS = `
|
|
6363
|
+
id
|
|
6364
|
+
identifier
|
|
6365
|
+
title
|
|
6366
|
+
description
|
|
6367
|
+
url
|
|
6368
|
+
priority
|
|
6369
|
+
state { id name type }
|
|
6370
|
+
assignee { id name }
|
|
6371
|
+
team { id key name states { nodes { id name type } } }
|
|
6372
|
+
labels { nodes { name } }
|
|
6373
|
+
`;
|
|
6270
6374
|
var ASSIGNED_ISSUES_QUERY = `
|
|
6271
6375
|
query SideboardAssignedIssues($first: Int!) {
|
|
6272
6376
|
viewer {
|
|
@@ -6275,53 +6379,280 @@ query SideboardAssignedIssues($first: Int!) {
|
|
|
6275
6379
|
orderBy: updatedAt
|
|
6276
6380
|
filter: { state: { type: { nin: ["completed", "canceled"] } } }
|
|
6277
6381
|
) {
|
|
6278
|
-
nodes {
|
|
6279
|
-
id
|
|
6280
|
-
identifier
|
|
6281
|
-
title
|
|
6282
|
-
url
|
|
6283
|
-
labels { nodes { name } }
|
|
6284
|
-
}
|
|
6382
|
+
nodes { ${ISSUE_FIELDS} }
|
|
6285
6383
|
}
|
|
6286
6384
|
}
|
|
6287
6385
|
}
|
|
6288
6386
|
`;
|
|
6289
|
-
|
|
6290
|
-
|
|
6291
|
-
|
|
6387
|
+
var TEAMS_QUERY = `
|
|
6388
|
+
query SideboardTeams {
|
|
6389
|
+
viewer { id name }
|
|
6390
|
+
teams(first: 50) {
|
|
6391
|
+
nodes {
|
|
6392
|
+
id
|
|
6393
|
+
key
|
|
6394
|
+
name
|
|
6395
|
+
states { nodes { id name type } }
|
|
6396
|
+
}
|
|
6397
|
+
}
|
|
6398
|
+
}
|
|
6399
|
+
`;
|
|
6400
|
+
var ISSUE_QUERY = `
|
|
6401
|
+
query SideboardIssue($id: String!) {
|
|
6402
|
+
issue(id: $id) { ${ISSUE_FIELDS} }
|
|
6403
|
+
}
|
|
6404
|
+
`;
|
|
6405
|
+
var ISSUE_CREATE = `
|
|
6406
|
+
mutation SideboardIssueCreate($input: IssueCreateInput!) {
|
|
6407
|
+
issueCreate(input: $input) {
|
|
6408
|
+
success
|
|
6409
|
+
issue { ${ISSUE_FIELDS} }
|
|
6410
|
+
}
|
|
6411
|
+
}
|
|
6412
|
+
`;
|
|
6413
|
+
var ISSUE_UPDATE = `
|
|
6414
|
+
mutation SideboardIssueUpdate($id: String!, $input: IssueUpdateInput!) {
|
|
6415
|
+
issueUpdate(id: $id, input: $input) {
|
|
6416
|
+
success
|
|
6417
|
+
issue { ${ISSUE_FIELDS} }
|
|
6418
|
+
}
|
|
6419
|
+
}
|
|
6420
|
+
`;
|
|
6421
|
+
var COMMENT_CREATE = `
|
|
6422
|
+
mutation SideboardCommentCreate($input: CommentCreateInput!) {
|
|
6423
|
+
commentCreate(input: $input) {
|
|
6424
|
+
success
|
|
6425
|
+
comment { id body url }
|
|
6426
|
+
}
|
|
6427
|
+
}
|
|
6428
|
+
`;
|
|
6429
|
+
function rewriteLinearError(message) {
|
|
6430
|
+
if (/scope|permission|not (authorized|allowed)|unauthorized|insufficient/i.test(message)) {
|
|
6431
|
+
return `${message} \u2014 Disconnect and Connect Linear in Account settings to grant write access.`;
|
|
6432
|
+
}
|
|
6433
|
+
return message;
|
|
6434
|
+
}
|
|
6435
|
+
async function requireLinearToken(apiKey) {
|
|
6436
|
+
const token = (apiKey ?? await getLinearAuthToken())?.trim();
|
|
6437
|
+
if (!token) {
|
|
6292
6438
|
throw new Error("Linear is not connected \u2014 sign in from Account settings");
|
|
6293
6439
|
}
|
|
6294
|
-
|
|
6295
|
-
|
|
6440
|
+
return token;
|
|
6441
|
+
}
|
|
6442
|
+
async function linearGraphql(query, variables, opts) {
|
|
6443
|
+
const apiKey = await requireLinearToken(opts?.apiKey);
|
|
6444
|
+
const res = await httpFetch(LINEAR_GRAPHQL, {
|
|
6296
6445
|
method: "POST",
|
|
6297
6446
|
headers: {
|
|
6298
6447
|
"Content-Type": "application/json",
|
|
6299
6448
|
Authorization: linearAuthorizationHeader(apiKey)
|
|
6300
6449
|
},
|
|
6301
|
-
body: JSON.stringify({
|
|
6302
|
-
query: ASSIGNED_ISSUES_QUERY,
|
|
6303
|
-
variables: { first }
|
|
6304
|
-
})
|
|
6450
|
+
body: JSON.stringify({ query, variables })
|
|
6305
6451
|
});
|
|
6306
6452
|
if (!res.ok) {
|
|
6307
6453
|
const body = await res.text().catch(() => "");
|
|
6308
6454
|
throw new Error(
|
|
6309
|
-
|
|
6455
|
+
rewriteLinearError(
|
|
6456
|
+
`Linear API error ${res.status}${body ? `: ${body.slice(0, 200)}` : ""}`
|
|
6457
|
+
)
|
|
6310
6458
|
);
|
|
6311
6459
|
}
|
|
6312
6460
|
const json = await res.json();
|
|
6313
6461
|
if (json.errors?.length) {
|
|
6314
|
-
throw new Error(
|
|
6462
|
+
throw new Error(
|
|
6463
|
+
rewriteLinearError(json.errors.map((e) => e.message ?? "Linear error").join("; "))
|
|
6464
|
+
);
|
|
6315
6465
|
}
|
|
6316
|
-
|
|
6317
|
-
|
|
6466
|
+
if (json.data == null) {
|
|
6467
|
+
throw new Error("Linear API returned no data");
|
|
6468
|
+
}
|
|
6469
|
+
return json.data;
|
|
6470
|
+
}
|
|
6471
|
+
function mapState(node) {
|
|
6472
|
+
if (!node?.id) return void 0;
|
|
6473
|
+
return {
|
|
6474
|
+
id: String(node.id),
|
|
6475
|
+
name: String(node.name ?? ""),
|
|
6476
|
+
type: String(node.type ?? "")
|
|
6477
|
+
};
|
|
6478
|
+
}
|
|
6479
|
+
function mapIssue(node) {
|
|
6480
|
+
const team = node.team;
|
|
6481
|
+
return {
|
|
6318
6482
|
id: String(node.id ?? node.identifier ?? ""),
|
|
6319
6483
|
identifier: String(node.identifier ?? node.id ?? ""),
|
|
6320
6484
|
title: String(node.title ?? ""),
|
|
6321
6485
|
url: String(node.url ?? ""),
|
|
6322
|
-
|
|
6486
|
+
description: node.description?.trim() || void 0,
|
|
6487
|
+
priority: typeof node.priority === "number" ? node.priority : void 0,
|
|
6488
|
+
state: mapState(node.state),
|
|
6489
|
+
assignee: node.assignee?.id ? { id: String(node.assignee.id), name: String(node.assignee.name ?? "") } : void 0,
|
|
6490
|
+
team: team?.id ? {
|
|
6491
|
+
id: String(team.id),
|
|
6492
|
+
key: String(team.key ?? ""),
|
|
6493
|
+
name: String(team.name ?? ""),
|
|
6494
|
+
states: (team.states?.nodes ?? []).map((s) => mapState(s)).filter((s) => Boolean(s))
|
|
6495
|
+
} : void 0,
|
|
6496
|
+
labels: (node.labels?.nodes ?? []).map((l) => l.name).filter((n) => Boolean(n))
|
|
6497
|
+
};
|
|
6498
|
+
}
|
|
6499
|
+
function toIssueInfo(issue) {
|
|
6500
|
+
return {
|
|
6501
|
+
id: issue.id,
|
|
6502
|
+
identifier: issue.identifier,
|
|
6503
|
+
title: issue.title,
|
|
6504
|
+
url: issue.url,
|
|
6505
|
+
labels: issue.labels,
|
|
6323
6506
|
provider: "linear"
|
|
6324
|
-
}
|
|
6507
|
+
};
|
|
6508
|
+
}
|
|
6509
|
+
function mapTeam(node) {
|
|
6510
|
+
return {
|
|
6511
|
+
id: String(node.id ?? ""),
|
|
6512
|
+
key: String(node.key ?? ""),
|
|
6513
|
+
name: String(node.name ?? ""),
|
|
6514
|
+
states: (node.states?.nodes ?? []).map((s) => mapState(s)).filter((s) => Boolean(s))
|
|
6515
|
+
};
|
|
6516
|
+
}
|
|
6517
|
+
function resolveLinearTeam(teams, team) {
|
|
6518
|
+
const t = team.trim();
|
|
6519
|
+
if (!t) throw new Error("Linear team is required (id, key, or name from linear_list_teams)");
|
|
6520
|
+
const lower = t.toLowerCase();
|
|
6521
|
+
const found = teams.find(
|
|
6522
|
+
(x) => x.id === t || x.key.toLowerCase() === lower || x.name.toLowerCase() === lower
|
|
6523
|
+
);
|
|
6524
|
+
if (!found) {
|
|
6525
|
+
const keys = teams.map((x) => x.key).filter(Boolean).join(", ") || "(none)";
|
|
6526
|
+
throw new Error(`Linear team not found: ${team}. Known keys: ${keys}`);
|
|
6527
|
+
}
|
|
6528
|
+
return found;
|
|
6529
|
+
}
|
|
6530
|
+
function resolveLinearState(team, state) {
|
|
6531
|
+
const s = state.trim();
|
|
6532
|
+
if (!s) throw new Error("Linear state is empty");
|
|
6533
|
+
const lower = s.toLowerCase();
|
|
6534
|
+
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);
|
|
6535
|
+
if (!found) {
|
|
6536
|
+
const available = team.states.map((x) => `${x.name} (${x.type})`).join(", ") || "(none)";
|
|
6537
|
+
throw new Error(`Linear state not found on ${team.key}: ${state}. Available: ${available}`);
|
|
6538
|
+
}
|
|
6539
|
+
return found;
|
|
6540
|
+
}
|
|
6541
|
+
function normalizePriority(priority) {
|
|
6542
|
+
if (priority == null) return void 0;
|
|
6543
|
+
if (!Number.isInteger(priority) || priority < 0 || priority > 4) {
|
|
6544
|
+
throw new Error("Linear priority must be 0 (none), 1 (urgent), 2 (high), 3 (medium), or 4 (low)");
|
|
6545
|
+
}
|
|
6546
|
+
return priority;
|
|
6547
|
+
}
|
|
6548
|
+
async function listLinearIssuesDirect(opts) {
|
|
6549
|
+
const first = Math.max(1, Math.min(100, opts?.limit ?? 50));
|
|
6550
|
+
const json = await linearGraphql(ASSIGNED_ISSUES_QUERY, { first }, opts);
|
|
6551
|
+
return (json.viewer?.assignedIssues?.nodes ?? []).map((node) => toIssueInfo(mapIssue(node)));
|
|
6552
|
+
}
|
|
6553
|
+
async function listLinearTeams(opts) {
|
|
6554
|
+
const json = await linearGraphql(TEAMS_QUERY, void 0, opts);
|
|
6555
|
+
return {
|
|
6556
|
+
viewer: {
|
|
6557
|
+
id: String(json.viewer?.id ?? ""),
|
|
6558
|
+
name: String(json.viewer?.name ?? "")
|
|
6559
|
+
},
|
|
6560
|
+
teams: (json.teams?.nodes ?? []).map(mapTeam)
|
|
6561
|
+
};
|
|
6562
|
+
}
|
|
6563
|
+
async function getLinearIssue(id, opts) {
|
|
6564
|
+
const issueId = id.trim();
|
|
6565
|
+
if (!issueId) throw new Error("Linear issue id is required (uuid or ENG-123)");
|
|
6566
|
+
const json = await linearGraphql(
|
|
6567
|
+
ISSUE_QUERY,
|
|
6568
|
+
{ id: issueId },
|
|
6569
|
+
opts
|
|
6570
|
+
);
|
|
6571
|
+
if (!json.issue) {
|
|
6572
|
+
throw new Error(`Linear issue not found: ${issueId}`);
|
|
6573
|
+
}
|
|
6574
|
+
return mapIssue(json.issue);
|
|
6575
|
+
}
|
|
6576
|
+
async function createLinearIssue(input, opts) {
|
|
6577
|
+
const title = input.title.trim();
|
|
6578
|
+
if (!title) throw new Error("Linear issue title is required");
|
|
6579
|
+
const { viewer, teams } = await listLinearTeams(opts);
|
|
6580
|
+
const team = resolveLinearTeam(teams, input.team);
|
|
6581
|
+
const mutationInput = {
|
|
6582
|
+
teamId: team.id,
|
|
6583
|
+
title
|
|
6584
|
+
};
|
|
6585
|
+
const description = input.description?.trim();
|
|
6586
|
+
if (description) mutationInput.description = description;
|
|
6587
|
+
if (input.state?.trim()) {
|
|
6588
|
+
mutationInput.stateId = resolveLinearState(team, input.state).id;
|
|
6589
|
+
}
|
|
6590
|
+
const assignee = input.assignee === void 0 ? void 0 : input.assignee?.trim() || null;
|
|
6591
|
+
if (assignee === "me") mutationInput.assigneeId = viewer.id;
|
|
6592
|
+
else if (assignee) mutationInput.assigneeId = assignee;
|
|
6593
|
+
const priority = normalizePriority(input.priority);
|
|
6594
|
+
if (priority != null) mutationInput.priority = priority;
|
|
6595
|
+
const json = await linearGraphql(ISSUE_CREATE, { input: mutationInput }, opts);
|
|
6596
|
+
if (!json.issueCreate?.success || !json.issueCreate.issue) {
|
|
6597
|
+
throw new Error("Linear issueCreate failed");
|
|
6598
|
+
}
|
|
6599
|
+
return mapIssue(json.issueCreate.issue);
|
|
6600
|
+
}
|
|
6601
|
+
async function updateLinearIssue(input, opts) {
|
|
6602
|
+
const issueId = input.id.trim();
|
|
6603
|
+
if (!issueId) throw new Error("Linear issue id is required (uuid or ENG-123)");
|
|
6604
|
+
const mutationInput = {};
|
|
6605
|
+
if (input.title !== void 0) {
|
|
6606
|
+
const title = input.title.trim();
|
|
6607
|
+
if (!title) throw new Error("Linear issue title cannot be empty");
|
|
6608
|
+
mutationInput.title = title;
|
|
6609
|
+
}
|
|
6610
|
+
if (input.description !== void 0) {
|
|
6611
|
+
mutationInput.description = input.description;
|
|
6612
|
+
}
|
|
6613
|
+
if (input.state?.trim()) {
|
|
6614
|
+
const existing = await getLinearIssue(issueId, opts);
|
|
6615
|
+
const team = existing.team;
|
|
6616
|
+
if (!team?.states.length) {
|
|
6617
|
+
throw new Error(`Linear issue ${existing.identifier} has no workflow states to resolve "${input.state}"`);
|
|
6618
|
+
}
|
|
6619
|
+
mutationInput.stateId = resolveLinearState(team, input.state).id;
|
|
6620
|
+
}
|
|
6621
|
+
if (input.assignee !== void 0) {
|
|
6622
|
+
const assignee = input.assignee?.trim() || null;
|
|
6623
|
+
if (assignee === "me") {
|
|
6624
|
+
const { viewer } = await listLinearTeams(opts);
|
|
6625
|
+
mutationInput.assigneeId = viewer.id;
|
|
6626
|
+
} else {
|
|
6627
|
+
mutationInput.assigneeId = assignee;
|
|
6628
|
+
}
|
|
6629
|
+
}
|
|
6630
|
+
if (input.priority !== void 0) {
|
|
6631
|
+
mutationInput.priority = normalizePriority(input.priority);
|
|
6632
|
+
}
|
|
6633
|
+
if (Object.keys(mutationInput).length === 0) {
|
|
6634
|
+
throw new Error("linear_update_issue needs at least one of title, description, state, assignee, priority");
|
|
6635
|
+
}
|
|
6636
|
+
const json = await linearGraphql(ISSUE_UPDATE, { id: issueId, input: mutationInput }, opts);
|
|
6637
|
+
if (!json.issueUpdate?.success || !json.issueUpdate.issue) {
|
|
6638
|
+
throw new Error("Linear issueUpdate failed");
|
|
6639
|
+
}
|
|
6640
|
+
return mapIssue(json.issueUpdate.issue);
|
|
6641
|
+
}
|
|
6642
|
+
async function commentLinearIssue(input, opts) {
|
|
6643
|
+
const issueId = input.id.trim();
|
|
6644
|
+
const body = input.body.trim();
|
|
6645
|
+
if (!issueId) throw new Error("Linear issue id is required (uuid or ENG-123)");
|
|
6646
|
+
if (!body) throw new Error("Linear comment body is required");
|
|
6647
|
+
const json = await linearGraphql(COMMENT_CREATE, { input: { issueId, body } }, opts);
|
|
6648
|
+
if (!json.commentCreate?.success || !json.commentCreate.comment?.id) {
|
|
6649
|
+
throw new Error("Linear commentCreate failed");
|
|
6650
|
+
}
|
|
6651
|
+
return {
|
|
6652
|
+
id: String(json.commentCreate.comment.id),
|
|
6653
|
+
body: String(json.commentCreate.comment.body ?? body),
|
|
6654
|
+
url: json.commentCreate.comment.url?.trim() || void 0
|
|
6655
|
+
};
|
|
6325
6656
|
}
|
|
6326
6657
|
|
|
6327
6658
|
// src/integrations/issues.ts
|
|
@@ -6603,8 +6934,8 @@ function labelGithubUrl(url) {
|
|
|
6603
6934
|
}
|
|
6604
6935
|
return { kind: "other", label: "GitHub", url: raw };
|
|
6605
6936
|
}
|
|
6606
|
-
function appendGithubLink(
|
|
6607
|
-
const body =
|
|
6937
|
+
function appendGithubLink(text3, githubUrl) {
|
|
6938
|
+
const body = text3.trimEnd();
|
|
6608
6939
|
if (!githubUrl?.trim()) return body;
|
|
6609
6940
|
const labeled = labelGithubUrl(githubUrl);
|
|
6610
6941
|
if (!labeled) {
|
|
@@ -6887,6 +7218,101 @@ function registerSlackTools(server) {
|
|
|
6887
7218
|
);
|
|
6888
7219
|
}
|
|
6889
7220
|
|
|
7221
|
+
// src/mcp/linear-tools.ts
|
|
7222
|
+
import { z as z2 } from "zod";
|
|
7223
|
+
function text2(payload, isError = false) {
|
|
7224
|
+
return {
|
|
7225
|
+
content: [{ type: "text", text: JSON.stringify(payload, null, 2) }],
|
|
7226
|
+
...isError ? { isError: true } : {}
|
|
7227
|
+
};
|
|
7228
|
+
}
|
|
7229
|
+
function fail2(err) {
|
|
7230
|
+
return text2(
|
|
7231
|
+
{ error: err instanceof Error ? err.message : String(err) },
|
|
7232
|
+
true
|
|
7233
|
+
);
|
|
7234
|
+
}
|
|
7235
|
+
var prioritySchema = z2.number().int().min(0).max(4).optional().describe("0 none, 1 urgent, 2 high, 3 medium, 4 low");
|
|
7236
|
+
function registerLinearTools(server) {
|
|
7237
|
+
server.tool(
|
|
7238
|
+
"linear_list_teams",
|
|
7239
|
+
"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.",
|
|
7240
|
+
{},
|
|
7241
|
+
async () => {
|
|
7242
|
+
try {
|
|
7243
|
+
return text2(await listLinearTeams());
|
|
7244
|
+
} catch (err) {
|
|
7245
|
+
return fail2(err);
|
|
7246
|
+
}
|
|
7247
|
+
}
|
|
7248
|
+
);
|
|
7249
|
+
server.tool(
|
|
7250
|
+
"linear_get_issue",
|
|
7251
|
+
"Get a Linear issue by uuid or identifier (ENG-123).",
|
|
7252
|
+
{ id: z2.string() },
|
|
7253
|
+
async ({ id }) => {
|
|
7254
|
+
try {
|
|
7255
|
+
return text2(await getLinearIssue(id));
|
|
7256
|
+
} catch (err) {
|
|
7257
|
+
return fail2(err);
|
|
7258
|
+
}
|
|
7259
|
+
}
|
|
7260
|
+
);
|
|
7261
|
+
server.tool(
|
|
7262
|
+
"linear_create_issue",
|
|
7263
|
+
'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.',
|
|
7264
|
+
{
|
|
7265
|
+
team: z2.string(),
|
|
7266
|
+
title: z2.string(),
|
|
7267
|
+
description: z2.string().optional(),
|
|
7268
|
+
state: z2.string().optional(),
|
|
7269
|
+
assignee: z2.string().nullable().optional(),
|
|
7270
|
+
priority: prioritySchema
|
|
7271
|
+
},
|
|
7272
|
+
async (args) => {
|
|
7273
|
+
try {
|
|
7274
|
+
return text2(await createLinearIssue(args));
|
|
7275
|
+
} catch (err) {
|
|
7276
|
+
return fail2(err);
|
|
7277
|
+
}
|
|
7278
|
+
}
|
|
7279
|
+
);
|
|
7280
|
+
server.tool(
|
|
7281
|
+
"linear_update_issue",
|
|
7282
|
+
'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.',
|
|
7283
|
+
{
|
|
7284
|
+
id: z2.string(),
|
|
7285
|
+
title: z2.string().optional(),
|
|
7286
|
+
description: z2.string().optional(),
|
|
7287
|
+
state: z2.string().optional(),
|
|
7288
|
+
assignee: z2.string().nullable().optional(),
|
|
7289
|
+
priority: prioritySchema
|
|
7290
|
+
},
|
|
7291
|
+
async (args) => {
|
|
7292
|
+
try {
|
|
7293
|
+
return text2(await updateLinearIssue(args));
|
|
7294
|
+
} catch (err) {
|
|
7295
|
+
return fail2(err);
|
|
7296
|
+
}
|
|
7297
|
+
}
|
|
7298
|
+
);
|
|
7299
|
+
server.tool(
|
|
7300
|
+
"linear_comment",
|
|
7301
|
+
"Add a markdown comment on a Linear issue (uuid or ENG-123).",
|
|
7302
|
+
{
|
|
7303
|
+
id: z2.string(),
|
|
7304
|
+
body: z2.string()
|
|
7305
|
+
},
|
|
7306
|
+
async (args) => {
|
|
7307
|
+
try {
|
|
7308
|
+
return text2(await commentLinearIssue(args));
|
|
7309
|
+
} catch (err) {
|
|
7310
|
+
return fail2(err);
|
|
7311
|
+
}
|
|
7312
|
+
}
|
|
7313
|
+
);
|
|
7314
|
+
}
|
|
7315
|
+
|
|
6890
7316
|
// src/mcp/server.ts
|
|
6891
7317
|
var MAX_ORCH_THREADS = 5;
|
|
6892
7318
|
var CREATE_THREAD_TIMEOUT_MS = 9e4;
|
|
@@ -6910,7 +7336,7 @@ function withTimeout(promise, ms, label) {
|
|
|
6910
7336
|
async function startMcpServer() {
|
|
6911
7337
|
const orch = getOrchestrator();
|
|
6912
7338
|
try {
|
|
6913
|
-
const { maxConcurrentAgents } = await import("../app-settings-
|
|
7339
|
+
const { maxConcurrentAgents } = await import("../app-settings-NILNWNNQ.js");
|
|
6914
7340
|
orch.setMaxConcurrent(maxConcurrentAgents());
|
|
6915
7341
|
} catch {
|
|
6916
7342
|
}
|
|
@@ -6963,7 +7389,7 @@ async function startMcpServer() {
|
|
|
6963
7389
|
server.tool(
|
|
6964
7390
|
"get_thread",
|
|
6965
7391
|
"Get a compact thread summary by id/ref",
|
|
6966
|
-
{ ref:
|
|
7392
|
+
{ ref: z3.string() },
|
|
6967
7393
|
async ({ ref }) => {
|
|
6968
7394
|
const t = orch.getThread(ref);
|
|
6969
7395
|
if (!t) {
|
|
@@ -6992,14 +7418,14 @@ async function startMcpServer() {
|
|
|
6992
7418
|
"present_artifact",
|
|
6993
7419
|
"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
7420
|
{
|
|
6995
|
-
title:
|
|
6996
|
-
type:
|
|
7421
|
+
title: z3.string().describe("Short title shown in the artifact pane header"),
|
|
7422
|
+
type: z3.enum(["html", "svg", "markdown", "react"]).describe(
|
|
6997
7423
|
"Artifact kind \u2014 html opens an iframe preview; react transpiles and renders a default-exported component in a sandboxed iframe"
|
|
6998
7424
|
),
|
|
6999
|
-
content:
|
|
7425
|
+
content: z3.string().describe(
|
|
7000
7426
|
"Full document body (complete HTML page, SVG markup, markdown, or a React component module with a default export)"
|
|
7001
7427
|
),
|
|
7002
|
-
artifact_id:
|
|
7428
|
+
artifact_id: z3.string().optional().describe("Stable id when updating the same artifact across turns")
|
|
7003
7429
|
},
|
|
7004
7430
|
async ({ title, type, artifact_id }) => {
|
|
7005
7431
|
const id = artifact_id?.trim() || `artifact_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 8)}`;
|
|
@@ -7017,15 +7443,15 @@ async function startMcpServer() {
|
|
|
7017
7443
|
"ask_user",
|
|
7018
7444
|
"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
7445
|
{
|
|
7020
|
-
questions:
|
|
7021
|
-
|
|
7022
|
-
question:
|
|
7023
|
-
header:
|
|
7024
|
-
multiSelect:
|
|
7025
|
-
options:
|
|
7026
|
-
|
|
7027
|
-
label:
|
|
7028
|
-
description:
|
|
7446
|
+
questions: z3.array(
|
|
7447
|
+
z3.object({
|
|
7448
|
+
question: z3.string().describe("Full question text ending with ?"),
|
|
7449
|
+
header: z3.string().max(24).optional().describe("Short label shown above the question"),
|
|
7450
|
+
multiSelect: z3.boolean().optional().describe("Allow selecting multiple options"),
|
|
7451
|
+
options: z3.array(
|
|
7452
|
+
z3.object({
|
|
7453
|
+
label: z3.string(),
|
|
7454
|
+
description: z3.string().optional().describe("What this option means / when to choose it (strongly preferred)")
|
|
7029
7455
|
})
|
|
7030
7456
|
).min(2).max(6).describe("2\u20136 choices (Sideboard also offers Other)")
|
|
7031
7457
|
})
|
|
@@ -7044,9 +7470,9 @@ async function startMcpServer() {
|
|
|
7044
7470
|
"present_plan",
|
|
7045
7471
|
"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
7472
|
{
|
|
7047
|
-
title:
|
|
7048
|
-
content:
|
|
7049
|
-
thread_id:
|
|
7473
|
+
title: z3.string().optional().describe("Short plan title (defaults to Plan)"),
|
|
7474
|
+
content: z3.string().min(1).describe("Full plan markdown (headings, steps, risks, open questions)"),
|
|
7475
|
+
thread_id: z3.string().optional().describe("Sideboard thread id when cwd is not the worktree")
|
|
7050
7476
|
},
|
|
7051
7477
|
async ({ title, content, thread_id }) => {
|
|
7052
7478
|
const { writePlanFile: writePlanFile2 } = await import("../plan-file-PHVKUAEE.js");
|
|
@@ -7069,15 +7495,15 @@ async function startMcpServer() {
|
|
|
7069
7495
|
"present_schema",
|
|
7070
7496
|
"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
7497
|
{
|
|
7072
|
-
title:
|
|
7073
|
-
mode:
|
|
7074
|
-
datasource:
|
|
7075
|
-
resource_id:
|
|
7076
|
-
record_id:
|
|
7077
|
-
resource:
|
|
7078
|
-
record:
|
|
7079
|
-
records:
|
|
7080
|
-
pane_id:
|
|
7498
|
+
title: z3.string().describe("Pane title"),
|
|
7499
|
+
mode: z3.enum(["table", "form"]).optional().describe("table = list/filter records; form = edit one record"),
|
|
7500
|
+
datasource: z3.enum(["brightsy", "inline"]).optional().describe("brightsy resolves via login; inline uses embedded resource/records"),
|
|
7501
|
+
resource_id: z3.string().optional().describe("Brightsy record type UUID (or generic resource id)"),
|
|
7502
|
+
record_id: z3.string().optional().describe("Record id when opening form mode"),
|
|
7503
|
+
resource: z3.record(z3.unknown()).optional().describe("Inline { id, title, schema, schemaUi?, slug? }"),
|
|
7504
|
+
record: z3.record(z3.unknown()).optional().describe("Inline record { id, data, published_at? }"),
|
|
7505
|
+
records: z3.array(z3.record(z3.unknown())).optional().describe("Inline records for table mode"),
|
|
7506
|
+
pane_id: z3.string().optional().describe("Stable pane id across updates")
|
|
7081
7507
|
},
|
|
7082
7508
|
async (args) => {
|
|
7083
7509
|
const id = args.pane_id?.trim() || `schema_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 8)}`;
|
|
@@ -7098,10 +7524,10 @@ async function startMcpServer() {
|
|
|
7098
7524
|
"present_files",
|
|
7099
7525
|
"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
7526
|
{
|
|
7101
|
-
title:
|
|
7102
|
-
datasource:
|
|
7103
|
-
path:
|
|
7104
|
-
pane_id:
|
|
7527
|
+
title: z3.string().optional().describe("Pane title (default: Files)"),
|
|
7528
|
+
datasource: z3.enum(["brightsy", "memory"]).optional().describe("brightsy = account storage via login; memory = session demo store"),
|
|
7529
|
+
path: z3.string().optional().describe("Initial folder path (e.g. public)"),
|
|
7530
|
+
pane_id: z3.string().optional().describe("Stable pane id across updates")
|
|
7105
7531
|
},
|
|
7106
7532
|
async (args) => {
|
|
7107
7533
|
const id = args.pane_id?.trim() || `files_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 8)}`;
|
|
@@ -7117,19 +7543,21 @@ async function startMcpServer() {
|
|
|
7117
7543
|
}
|
|
7118
7544
|
);
|
|
7119
7545
|
registerSlackTools(server);
|
|
7546
|
+
registerLinearTools(server);
|
|
7120
7547
|
if (sideboardMcpProfile() !== "worktree") {
|
|
7121
|
-
const { getCaffeinateHold, setCaffeinateHold } = await import("../caffeinate-hold-
|
|
7122
|
-
const { resolveNewThreadOptions: resolveNewThreadOptions2, resolveThreadDefaults } = await import("../app-settings-
|
|
7548
|
+
const { getCaffeinateHold, setCaffeinateHold } = await import("../caffeinate-hold-OHAUWOA6.js");
|
|
7549
|
+
const { resolveNewThreadOptions: resolveNewThreadOptions2, resolveThreadDefaults } = await import("../app-settings-NILNWNNQ.js");
|
|
7123
7550
|
const accountDefaults = resolveThreadDefaults();
|
|
7124
7551
|
const accountDefaultsHint = `Account defaults: agent=${accountDefaults.agent}, model=${accountDefaults.model?.trim() || "Auto"}, effort=${accountDefaults.effort}`;
|
|
7125
7552
|
server.tool(
|
|
7126
7553
|
"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.",
|
|
7554
|
+
"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
7555
|
{
|
|
7129
|
-
enabled:
|
|
7556
|
+
enabled: z3.boolean().describe("true = hold caffeinate on; false = release and let the Mac sleep")
|
|
7130
7557
|
},
|
|
7131
7558
|
async ({ enabled }) => {
|
|
7132
|
-
const
|
|
7559
|
+
const threadId = process.env.SIDEBOARD_ORCHESTRATOR_THREAD_ID?.trim() || null;
|
|
7560
|
+
const state = setCaffeinateHold(enabled, { threadId });
|
|
7133
7561
|
if (enabled && !state.held) {
|
|
7134
7562
|
return {
|
|
7135
7563
|
content: [
|
|
@@ -7152,7 +7580,7 @@ async function startMcpServer() {
|
|
|
7152
7580
|
text: JSON.stringify({
|
|
7153
7581
|
...getCaffeinateHold(),
|
|
7154
7582
|
ok: true,
|
|
7155
|
-
message: state.held ? "Mac will stay awake until you call set_caffeinate with enabled=false
|
|
7583
|
+
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
7584
|
})
|
|
7157
7585
|
}
|
|
7158
7586
|
]
|
|
@@ -7163,15 +7591,15 @@ async function startMcpServer() {
|
|
|
7163
7591
|
"create_thread",
|
|
7164
7592
|
`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
7593
|
{
|
|
7166
|
-
sourceType:
|
|
7167
|
-
sourceRef:
|
|
7168
|
-
agent:
|
|
7169
|
-
model:
|
|
7594
|
+
sourceType: z3.enum(["branch", "pr", "ticket"]),
|
|
7595
|
+
sourceRef: z3.string(),
|
|
7596
|
+
agent: z3.enum(["claude", "codex", "opencode", "brightsy", "cursor"]).optional().describe(`Omit to use Account default agent (${accountDefaults.agent})`),
|
|
7597
|
+
model: z3.string().nullable().optional().describe(
|
|
7170
7598
|
`Usually omit to use Account default model (${accountDefaults.model?.trim() || "Auto"}). Pass null only to force Auto / agent-default.`
|
|
7171
7599
|
),
|
|
7172
|
-
repoPath:
|
|
7173
|
-
title:
|
|
7174
|
-
parentThreadId:
|
|
7600
|
+
repoPath: z3.string(),
|
|
7601
|
+
title: z3.string().optional(),
|
|
7602
|
+
parentThreadId: z3.string().optional().describe(
|
|
7175
7603
|
"Orchestration: omit (preferred) or pass YOUR chat id from the turn reminder / AGENTS.md. Do not invent uuids."
|
|
7176
7604
|
)
|
|
7177
7605
|
},
|
|
@@ -7278,9 +7706,9 @@ async function startMcpServer() {
|
|
|
7278
7706
|
"send_to_thread",
|
|
7279
7707
|
"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
7708
|
{
|
|
7281
|
-
ref:
|
|
7282
|
-
prompt:
|
|
7283
|
-
force_stop:
|
|
7709
|
+
ref: z3.string(),
|
|
7710
|
+
prompt: z3.string(),
|
|
7711
|
+
force_stop: z3.boolean().optional()
|
|
7284
7712
|
},
|
|
7285
7713
|
async ({ ref, prompt, force_stop }) => {
|
|
7286
7714
|
if (force_stop) {
|
|
@@ -7309,8 +7737,8 @@ async function startMcpServer() {
|
|
|
7309
7737
|
"wait_for_turn",
|
|
7310
7738
|
"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
7739
|
{
|
|
7312
|
-
ref:
|
|
7313
|
-
timeoutMs:
|
|
7740
|
+
ref: z3.string(),
|
|
7741
|
+
timeoutMs: z3.number().optional()
|
|
7314
7742
|
},
|
|
7315
7743
|
async ({ ref, timeoutMs }) => {
|
|
7316
7744
|
const thread = await orch.waitForTurn(ref, timeoutMs ?? 6e5);
|
|
@@ -7332,7 +7760,7 @@ async function startMcpServer() {
|
|
|
7332
7760
|
server.tool(
|
|
7333
7761
|
"get_turn_result",
|
|
7334
7762
|
"Final assistant message only (not full transcript)",
|
|
7335
|
-
{ ref:
|
|
7763
|
+
{ ref: z3.string() },
|
|
7336
7764
|
async ({ ref }) => {
|
|
7337
7765
|
const result = orch.getTurnResult(ref);
|
|
7338
7766
|
return { content: [{ type: "text", text: JSON.stringify(result) }] };
|
|
@@ -7342,8 +7770,8 @@ async function startMcpServer() {
|
|
|
7342
7770
|
"stop_thread",
|
|
7343
7771
|
"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
7772
|
{
|
|
7345
|
-
ref:
|
|
7346
|
-
force:
|
|
7773
|
+
ref: z3.string(),
|
|
7774
|
+
force: z3.boolean().optional()
|
|
7347
7775
|
},
|
|
7348
7776
|
async ({ ref, force }) => {
|
|
7349
7777
|
const t = orch.getThread(ref);
|
|
@@ -7373,7 +7801,7 @@ async function startMcpServer() {
|
|
|
7373
7801
|
server.tool(
|
|
7374
7802
|
"archive_thread",
|
|
7375
7803
|
"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:
|
|
7804
|
+
{ ref: z3.string() },
|
|
7377
7805
|
async ({ ref }) => {
|
|
7378
7806
|
const t = orch.getThread(ref);
|
|
7379
7807
|
if (!t) {
|
|
@@ -7406,7 +7834,7 @@ async function startMcpServer() {
|
|
|
7406
7834
|
server.tool(
|
|
7407
7835
|
"restore_thread",
|
|
7408
7836
|
"Restore an archived thread (recreates worktree from branch when needed)",
|
|
7409
|
-
{ ref:
|
|
7837
|
+
{ ref: z3.string() },
|
|
7410
7838
|
async ({ ref }) => {
|
|
7411
7839
|
try {
|
|
7412
7840
|
const restored = await orch.restore(ref);
|
|
@@ -7432,8 +7860,8 @@ async function startMcpServer() {
|
|
|
7432
7860
|
"get_diff",
|
|
7433
7861
|
"Compact diff summary (capped hunks, paginated)",
|
|
7434
7862
|
{
|
|
7435
|
-
ref:
|
|
7436
|
-
maxFiles:
|
|
7863
|
+
ref: z3.string(),
|
|
7864
|
+
maxFiles: z3.number().optional()
|
|
7437
7865
|
},
|
|
7438
7866
|
async ({ ref, maxFiles }) => {
|
|
7439
7867
|
const summary = await orch.diffSummary(ref);
|
|
@@ -7453,7 +7881,7 @@ async function startMcpServer() {
|
|
|
7453
7881
|
server.tool(
|
|
7454
7882
|
"request_review",
|
|
7455
7883
|
'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:
|
|
7884
|
+
{ ref: z3.string().describe("Worktree thread id/ref to review") },
|
|
7457
7885
|
async ({ ref }) => {
|
|
7458
7886
|
try {
|
|
7459
7887
|
const tab = await orch.requestReview(ref);
|
|
@@ -7480,10 +7908,10 @@ async function startMcpServer() {
|
|
|
7480
7908
|
);
|
|
7481
7909
|
server.tool(
|
|
7482
7910
|
"ask_git",
|
|
7483
|
-
"
|
|
7911
|
+
"Commit & push, open a draft PR, resolve conflicts, or merge \u2014 same actions as the desktop git buttons. When the worktree is clean, Sideboard pushes / opens the PR itself (HTTPS via `gh` if SSH is missing). When dirty, queues the worktree agent to commit; then wait_for_turn. Pass a worktree thread ref (not the orchestrator). Do not run git or gh from the orchestration cwd.",
|
|
7484
7912
|
{
|
|
7485
|
-
ref:
|
|
7486
|
-
action:
|
|
7913
|
+
ref: z3.string().describe("Worktree thread id/ref"),
|
|
7914
|
+
action: z3.enum(AGENT_GIT_ACTIONS).describe(
|
|
7487
7915
|
"commit-push | create-draft | create-web | resolve-conflicts | merge"
|
|
7488
7916
|
)
|
|
7489
7917
|
},
|
|
@@ -7510,7 +7938,7 @@ async function startMcpServer() {
|
|
|
7510
7938
|
}
|
|
7511
7939
|
}
|
|
7512
7940
|
);
|
|
7513
|
-
const agentEnum =
|
|
7941
|
+
const agentEnum = z3.enum(["claude", "codex", "opencode", "brightsy", "cursor"]);
|
|
7514
7942
|
server.tool(
|
|
7515
7943
|
"list_models",
|
|
7516
7944
|
"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 +7961,11 @@ async function startMcpServer() {
|
|
|
7533
7961
|
"fork_worktree",
|
|
7534
7962
|
"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
7963
|
{
|
|
7536
|
-
ref:
|
|
7537
|
-
through_index:
|
|
7964
|
+
ref: z3.string().describe("Worktree thread id/ref to fork"),
|
|
7965
|
+
through_index: z3.number().optional().describe("Inclusive message index to include in the transcript (default: all)"),
|
|
7538
7966
|
agent: agentEnum.optional().describe("Agent for the forked chat (default: same as source)"),
|
|
7539
|
-
model:
|
|
7540
|
-
title:
|
|
7967
|
+
model: z3.string().nullable().optional().describe("Usually omit (Auto). Only set from list_models when you need a specific model"),
|
|
7968
|
+
title: z3.string().optional()
|
|
7541
7969
|
},
|
|
7542
7970
|
async ({ ref, through_index, agent, model, title }) => {
|
|
7543
7971
|
try {
|
|
@@ -7578,11 +8006,11 @@ async function startMcpServer() {
|
|
|
7578
8006
|
"fork_chat",
|
|
7579
8007
|
"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
8008
|
{
|
|
7581
|
-
ref:
|
|
7582
|
-
through_index:
|
|
8009
|
+
ref: z3.string().describe("Thread id/ref to fork (worktree agent or orchestration chat)"),
|
|
8010
|
+
through_index: z3.number().optional().describe("Inclusive message index to include in the transcript (default: all)"),
|
|
7583
8011
|
agent: agentEnum.optional().describe("Agent for the forked chat (default: same as source)"),
|
|
7584
|
-
model:
|
|
7585
|
-
title:
|
|
8012
|
+
model: z3.string().nullable().optional().describe("Usually omit (Auto). Only set from list_models when you need a specific model"),
|
|
8013
|
+
title: z3.string().optional()
|
|
7586
8014
|
},
|
|
7587
8015
|
async ({ ref, through_index, agent, model, title }) => {
|
|
7588
8016
|
try {
|
|
@@ -7628,8 +8056,8 @@ async function startMcpServer() {
|
|
|
7628
8056
|
"run_dev_script",
|
|
7629
8057
|
"Start a .sideboard/.conductor run script for a thread (default script if name omitted); returns port",
|
|
7630
8058
|
{
|
|
7631
|
-
ref:
|
|
7632
|
-
name:
|
|
8059
|
+
ref: z3.string(),
|
|
8060
|
+
name: z3.string().optional()
|
|
7633
8061
|
},
|
|
7634
8062
|
async ({ ref, name }) => {
|
|
7635
8063
|
const result = await orch.startDev(ref, name);
|
|
@@ -7651,7 +8079,7 @@ async function startMcpServer() {
|
|
|
7651
8079
|
server.tool(
|
|
7652
8080
|
"list_run_scripts",
|
|
7653
8081
|
"List named run scripts available for a thread",
|
|
7654
|
-
{ ref:
|
|
8082
|
+
{ ref: z3.string() },
|
|
7655
8083
|
async ({ ref }) => {
|
|
7656
8084
|
const scripts = orch.listThreadRunScripts(ref);
|
|
7657
8085
|
const active = orch.getActiveRuns(ref);
|
|
@@ -7669,8 +8097,8 @@ async function startMcpServer() {
|
|
|
7669
8097
|
"stop_dev_script",
|
|
7670
8098
|
"Stop a running script for a thread (all scripts if name omitted)",
|
|
7671
8099
|
{
|
|
7672
|
-
ref:
|
|
7673
|
-
name:
|
|
8100
|
+
ref: z3.string(),
|
|
8101
|
+
name: z3.string().optional()
|
|
7674
8102
|
},
|
|
7675
8103
|
async ({ ref, name }) => {
|
|
7676
8104
|
orch.stopDev(ref, name);
|
|
@@ -7680,7 +8108,7 @@ async function startMcpServer() {
|
|
|
7680
8108
|
server.tool(
|
|
7681
8109
|
"run_setup",
|
|
7682
8110
|
"Re-run workspace setup (Sideboard/Conductor settings or .cursor/worktrees.json)",
|
|
7683
|
-
{ ref:
|
|
8111
|
+
{ ref: z3.string() },
|
|
7684
8112
|
async ({ ref }) => {
|
|
7685
8113
|
const result = await orch.runSetup(ref);
|
|
7686
8114
|
return {
|
|
@@ -7691,7 +8119,7 @@ async function startMcpServer() {
|
|
|
7691
8119
|
server.tool(
|
|
7692
8120
|
"add_workspace",
|
|
7693
8121
|
"Register a git repo as a Sideboard workspace",
|
|
7694
|
-
{ repoPath:
|
|
8122
|
+
{ repoPath: z3.string() },
|
|
7695
8123
|
async ({ repoPath }) => {
|
|
7696
8124
|
const ws = await orch.addWorkspace(repoPath);
|
|
7697
8125
|
return { content: [{ type: "text", text: JSON.stringify(ws) }] };
|
|
@@ -7700,7 +8128,7 @@ async function startMcpServer() {
|
|
|
7700
8128
|
server.tool(
|
|
7701
8129
|
"remove_workspace",
|
|
7702
8130
|
"Unregister a Sideboard workspace (does not archive threads)",
|
|
7703
|
-
{ repoPath:
|
|
8131
|
+
{ repoPath: z3.string() },
|
|
7704
8132
|
async ({ repoPath }) => {
|
|
7705
8133
|
orch.removeWorkspace(repoPath);
|
|
7706
8134
|
return { content: [{ type: "text", text: "ok" }] };
|
|
@@ -7710,12 +8138,12 @@ async function startMcpServer() {
|
|
|
7710
8138
|
"fanout",
|
|
7711
8139
|
"Best-of-n: create one thread per agent with the same prompt (parallel attempts)",
|
|
7712
8140
|
{
|
|
7713
|
-
prompt:
|
|
7714
|
-
agents:
|
|
7715
|
-
repoPath:
|
|
7716
|
-
sourceType:
|
|
7717
|
-
sourceRef:
|
|
7718
|
-
title:
|
|
8141
|
+
prompt: z3.string(),
|
|
8142
|
+
agents: z3.array(z3.enum(["claude", "codex", "opencode", "brightsy", "cursor"])),
|
|
8143
|
+
repoPath: z3.string(),
|
|
8144
|
+
sourceType: z3.enum(["branch", "pr", "ticket"]).optional(),
|
|
8145
|
+
sourceRef: z3.string().optional(),
|
|
8146
|
+
title: z3.string().optional()
|
|
7719
8147
|
},
|
|
7720
8148
|
async (args) => {
|
|
7721
8149
|
const threads = await orch.bestOfN(args);
|
|
@@ -7742,8 +8170,8 @@ async function startMcpServer() {
|
|
|
7742
8170
|
"list_branches",
|
|
7743
8171
|
"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
8172
|
{
|
|
7745
|
-
repoPath:
|
|
7746
|
-
unmergedOnly:
|
|
8173
|
+
repoPath: z3.string(),
|
|
8174
|
+
unmergedOnly: z3.boolean().optional()
|
|
7747
8175
|
},
|
|
7748
8176
|
async ({ repoPath, unmergedOnly }) => {
|
|
7749
8177
|
const root = await resolveRepoRoot(repoPath);
|
|
@@ -7763,7 +8191,7 @@ async function startMcpServer() {
|
|
|
7763
8191
|
server.tool(
|
|
7764
8192
|
"list_prs",
|
|
7765
8193
|
"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:
|
|
8194
|
+
{ repoPath: z3.string() },
|
|
7767
8195
|
async ({ repoPath }) => {
|
|
7768
8196
|
const root = await resolveRepoRoot(repoPath);
|
|
7769
8197
|
const prs = await listPrs(root);
|
|
@@ -7782,7 +8210,7 @@ async function startMcpServer() {
|
|
|
7782
8210
|
server.tool(
|
|
7783
8211
|
"get_pr_stack",
|
|
7784
8212
|
"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:
|
|
8213
|
+
{ ref: z3.string() },
|
|
7786
8214
|
async ({ ref }) => {
|
|
7787
8215
|
const stack = await orch.getPrStack(ref);
|
|
7788
8216
|
return {
|
|
@@ -7794,8 +8222,8 @@ async function startMcpServer() {
|
|
|
7794
8222
|
"open_pr_stack_layers",
|
|
7795
8223
|
"Materialize one worktree+thread per stack layer (or a single 1-based layer). Pass a thread ref already on the stack.",
|
|
7796
8224
|
{
|
|
7797
|
-
ref:
|
|
7798
|
-
layer:
|
|
8225
|
+
ref: z3.string(),
|
|
8226
|
+
layer: z3.number().int().positive().optional()
|
|
7799
8227
|
},
|
|
7800
8228
|
async ({ ref, layer }) => {
|
|
7801
8229
|
const result = await orch.openPrStackLayers(ref, { layer });
|
|
@@ -7829,9 +8257,9 @@ async function startMcpServer() {
|
|
|
7829
8257
|
"add_stack_layer",
|
|
7830
8258
|
"Add a branch on top of the current stack (`gh stack add`) and open a worktree+thread for it.",
|
|
7831
8259
|
{
|
|
7832
|
-
ref:
|
|
7833
|
-
branchName:
|
|
7834
|
-
title:
|
|
8260
|
+
ref: z3.string(),
|
|
8261
|
+
branchName: z3.string(),
|
|
8262
|
+
title: z3.string().optional()
|
|
7835
8263
|
},
|
|
7836
8264
|
async ({ ref, branchName, title }) => {
|
|
7837
8265
|
const result = await orch.addStackLayer(ref, branchName, { title });
|
|
@@ -7860,11 +8288,11 @@ async function startMcpServer() {
|
|
|
7860
8288
|
"create_pr_stack",
|
|
7861
8289
|
"Create a new GitHub PR stack with one Sideboard worktree per layer (bottom\u2192top branch names). Requires `gh extension install github/gh-stack`.",
|
|
7862
8290
|
{
|
|
7863
|
-
repoPath:
|
|
7864
|
-
branches:
|
|
7865
|
-
agent:
|
|
7866
|
-
base:
|
|
7867
|
-
title:
|
|
8291
|
+
repoPath: z3.string(),
|
|
8292
|
+
branches: z3.array(z3.string()).min(1),
|
|
8293
|
+
agent: z3.enum(["claude", "codex", "opencode", "brightsy", "cursor"]),
|
|
8294
|
+
base: z3.string().optional(),
|
|
8295
|
+
title: z3.string().optional()
|
|
7868
8296
|
},
|
|
7869
8297
|
async (args) => {
|
|
7870
8298
|
const result = await orch.createPrStack({
|
|
@@ -7902,7 +8330,7 @@ async function startMcpServer() {
|
|
|
7902
8330
|
server.tool(
|
|
7903
8331
|
"list_issues",
|
|
7904
8332
|
"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:
|
|
8333
|
+
{ repoPath: z3.string() },
|
|
7906
8334
|
async ({ repoPath }) => {
|
|
7907
8335
|
const root = await resolveRepoRoot(repoPath);
|
|
7908
8336
|
const result = await listIssues(root);
|
|
@@ -7913,8 +8341,8 @@ async function startMcpServer() {
|
|
|
7913
8341
|
"list_linear_issues",
|
|
7914
8342
|
"Deprecated: prefer list_issues. Lists assigned Linear issues via the chosen agent MCP connector",
|
|
7915
8343
|
{
|
|
7916
|
-
agent:
|
|
7917
|
-
repoPath:
|
|
8344
|
+
agent: z3.enum(["claude", "codex", "opencode"]),
|
|
8345
|
+
repoPath: z3.string()
|
|
7918
8346
|
},
|
|
7919
8347
|
async ({ agent, repoPath }) => {
|
|
7920
8348
|
const issues = await listLinearIssues(agent, repoPath);
|