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