@sideboard-ai/core 0.1.67 → 0.1.71
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{agents-OTBZVTZO.js → agents-GB7XL2LI.js} +8 -7
- package/dist/{agents-TS3EI2YI.js → agents-TCAK3MXN.js} +8 -7
- 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-WAFIF22N.js → chunk-3CY6WJ7O.js} +2 -2
- package/dist/{chunk-7ZOQY4A7.js → chunk-57ROTAFD.js} +62 -3
- package/dist/{chunk-57WWNMFP.js → chunk-7NCIHRAU.js} +22 -4
- package/dist/{chunk-FGDJOJ2L.js → chunk-AE5VBOFE.js} +1 -1
- package/dist/{chunk-DB3I75FI.js → chunk-BTL7EMGT.js} +4 -4
- package/dist/{chunk-4UIYN56C.js → chunk-D4DPQ552.js} +1 -1
- package/dist/{chunk-JWZNK3CQ.js → chunk-DGPUAZA4.js} +62 -3
- package/dist/{chunk-ZRFAZVGL.js → chunk-H2IXNFQ2.js} +22 -4
- package/dist/chunk-JAWEDVVA.js +106 -0
- package/dist/chunk-JT7R45JB.js +20 -0
- package/dist/{chunk-4BUCGNZS.js → chunk-MF2YMEGD.js} +83 -29
- package/dist/{chunk-K7LM4CLD.js → chunk-NG7S3OPX.js} +12 -6
- package/dist/chunk-NSTQ6QKD.js +23 -0
- package/dist/{chunk-LW5CMQGZ.js → chunk-OE7YBB5X.js} +83 -29
- package/dist/{chunk-BPOTVI5J.js → chunk-PTJJIQK7.js} +472 -18
- package/dist/{chunk-B4PRRKMA.js → chunk-S25IHL5H.js} +12 -6
- package/dist/{chunk-5HTTCYWA.js → chunk-UH57WVFP.js} +2 -2
- package/dist/{chunk-M5V4QIWF.js → chunk-VROPG6QF.js} +1 -1
- package/dist/{chunk-MHR4OGZ4.js → chunk-W7YOTDVO.js} +493 -18
- package/dist/{connected-teams-RUNMOCG5.js → connected-teams-35EIRJCP.js} +2 -2
- package/dist/{connected-teams-OMDHTL6A.js → connected-teams-ZRS53OAT.js} +2 -2
- package/dist/{coordinator-prompt-6HMTOX4G.js → coordinator-prompt-2J4PIMUF.js} +7 -4
- package/dist/{coordinator-prompt-3BSMKZL4.js → coordinator-prompt-QYYG2IBB.js} +7 -4
- package/dist/{global-workspace-YZ2NJVLL.js → global-workspace-KHIFQUUM.js} +12 -5
- package/dist/{global-workspace-QS7A23CB.js → global-workspace-SVRYNJZA.js} +12 -5
- package/dist/index.cjs +4805 -1262
- package/dist/index.d.cts +570 -22
- package/dist/index.d.ts +570 -22
- package/dist/index.js +3084 -340
- package/dist/mcp/run-stdio.cjs +2339 -617
- package/dist/mcp/run-stdio.js +1289 -290
- package/dist/{run-Y2MI3EVV.js → run-A4RHY7HH.js} +1 -1
- package/dist/{run-JLD2Y52J.js → run-V2WZUMJA.js} +1 -1
- package/dist/{workspaces-EBFW7I7S.js → workspaces-AHFTHVBT.js} +7 -6
- package/dist/{workspaces-AIUYUXB7.js → workspaces-SVQIEVIZ.js} +7 -6
- package/dist/{worktree-WMDJW2BL.js → worktree-KWXHMKRN.js} +2 -2
- package/dist/{worktree-7NBPIVFZ.js → worktree-VOCH3WS3.js} +2 -2
- 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-
|
|
20
|
-
import "../chunk-
|
|
19
|
+
} from "../chunk-NG7S3OPX.js";
|
|
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,
|
|
@@ -105,20 +106,29 @@ import {
|
|
|
105
106
|
gh,
|
|
106
107
|
git,
|
|
107
108
|
run
|
|
108
|
-
} from "../chunk-
|
|
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 {
|
|
@@ -2058,7 +2068,7 @@ async function adoptThread(input) {
|
|
|
2058
2068
|
throw new Error(`Worktree not found: ${input.worktreePath}`);
|
|
2059
2069
|
}
|
|
2060
2070
|
const repoPath = await resolveRepoRoot(input.worktreePath);
|
|
2061
|
-
const { stdout: branch } = await import("../run-
|
|
2071
|
+
const { stdout: branch } = await import("../run-A4RHY7HH.js").then(
|
|
2062
2072
|
({ git: git2 }) => git2(["rev-parse", "--abbrev-ref", "HEAD"], input.worktreePath)
|
|
2063
2073
|
);
|
|
2064
2074
|
const thread = createEmptyThread({
|
|
@@ -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
|
}
|
|
@@ -2630,24 +2640,25 @@ function capPatch(patch, maxHunk) {
|
|
|
2630
2640
|
|
|
2631
2641
|
\u2026 truncated (${patch.length - maxHunk} more chars)`;
|
|
2632
2642
|
}
|
|
2643
|
+
var mergeBaseCache = /* @__PURE__ */ new Map();
|
|
2644
|
+
var MERGE_BASE_TTL_MS = 45e3;
|
|
2633
2645
|
async function resolveMergeBase(worktreePath, base, repoPath) {
|
|
2634
2646
|
const baseRef = await resolveDiffBaseRef(worktreePath, base, repoPath);
|
|
2647
|
+
const cacheKey = `${worktreePath}\0${baseRef}`;
|
|
2648
|
+
const hit = mergeBaseCache.get(cacheKey);
|
|
2649
|
+
if (hit && Date.now() - hit.at < MERGE_BASE_TTL_MS) return hit.value;
|
|
2635
2650
|
const { stdout } = await git(["merge-base", baseRef, "HEAD"], worktreePath, {
|
|
2636
2651
|
reject: false
|
|
2637
2652
|
});
|
|
2638
|
-
const mb = stdout.trim();
|
|
2639
|
-
|
|
2653
|
+
const mb = stdout.trim() || baseRef;
|
|
2654
|
+
mergeBaseCache.set(cacheKey, { at: Date.now(), value: mb });
|
|
2655
|
+
return mb;
|
|
2640
2656
|
}
|
|
2641
2657
|
async function countUnpushedCommits(worktreePath) {
|
|
2642
2658
|
const head = await git(["rev-parse", "--abbrev-ref", "HEAD"], worktreePath, {
|
|
2643
2659
|
reject: false
|
|
2644
2660
|
});
|
|
2645
2661
|
const branch = head.stdout.trim();
|
|
2646
|
-
if (branch && branch !== "HEAD") {
|
|
2647
|
-
await git(["fetch", "origin", branch, "--quiet"], worktreePath, {
|
|
2648
|
-
reject: false
|
|
2649
|
-
});
|
|
2650
|
-
}
|
|
2651
2662
|
const upstream = await git(
|
|
2652
2663
|
["rev-list", "--count", "@{upstream}..HEAD"],
|
|
2653
2664
|
worktreePath,
|
|
@@ -2680,11 +2691,37 @@ function splitCombinedDiff(stdout) {
|
|
|
2680
2691
|
}
|
|
2681
2692
|
return map;
|
|
2682
2693
|
}
|
|
2694
|
+
function syntheticAddPatch(path, content, maxHunk) {
|
|
2695
|
+
const lines = content.split("\n");
|
|
2696
|
+
const body = lines.map((l) => `+${l}`).join("\n");
|
|
2697
|
+
const header = `diff --git a/${path} b/${path}
|
|
2698
|
+
new file mode 100644
|
|
2699
|
+
--- /dev/null
|
|
2700
|
+
+++ b/${path}
|
|
2701
|
+
@@ -0,0 +1,${lines.length} @@
|
|
2702
|
+
`;
|
|
2703
|
+
return {
|
|
2704
|
+
path,
|
|
2705
|
+
status: "A",
|
|
2706
|
+
patch: capPatch(`${header}${body}`, maxHunk),
|
|
2707
|
+
additions: lines.length,
|
|
2708
|
+
deletions: 0
|
|
2709
|
+
};
|
|
2710
|
+
}
|
|
2683
2711
|
async function untrackedPatch(worktreePath, path, maxHunk) {
|
|
2712
|
+
const abs = join7(worktreePath, path);
|
|
2713
|
+
try {
|
|
2714
|
+
const st = statSync2(abs);
|
|
2715
|
+
if (st.isFile() && st.size > maxHunk) {
|
|
2716
|
+
const buf = readFileSync5(abs).subarray(0, maxHunk);
|
|
2717
|
+
return syntheticAddPatch(path, buf.toString("utf8"), maxHunk);
|
|
2718
|
+
}
|
|
2719
|
+
} catch {
|
|
2720
|
+
}
|
|
2684
2721
|
const { stdout: patch } = await git(
|
|
2685
|
-
["diff", "--no-index", "--", "/dev/null", path],
|
|
2722
|
+
["diff", "--no-ext-diff", "--no-color", "--no-index", "--", "/dev/null", path],
|
|
2686
2723
|
worktreePath,
|
|
2687
|
-
{ reject: false }
|
|
2724
|
+
{ reject: false, timeoutMs: 4e3 }
|
|
2688
2725
|
);
|
|
2689
2726
|
const normalized = patch.replace(/^diff --git a\/.+? b\/.+?$/m, `diff --git a/${path} b/${path}`).replace(/^--- .*$/m, "--- /dev/null").replace(/^\+\+\+ .*$/m, `+++ b/${path}`);
|
|
2690
2727
|
const addLines = normalized.split("\n").filter((l) => l.startsWith("+") && !l.startsWith("+++")).length;
|
|
@@ -2696,22 +2733,97 @@ async function untrackedPatch(worktreePath, path, maxHunk) {
|
|
|
2696
2733
|
deletions: 0
|
|
2697
2734
|
};
|
|
2698
2735
|
}
|
|
2699
|
-
async function listUntrackedPaths(worktreePath) {
|
|
2700
|
-
const
|
|
2701
|
-
|
|
2702
|
-
|
|
2703
|
-
{ reject: false }
|
|
2704
|
-
);
|
|
2736
|
+
async function listUntrackedPaths(worktreePath, path) {
|
|
2737
|
+
const args = ["ls-files", "-z", "--others", "--exclude-standard"];
|
|
2738
|
+
if (path) args.push("--", path);
|
|
2739
|
+
const { stdout } = await git(args, worktreePath, { reject: false });
|
|
2705
2740
|
return stdout.split("\0").filter(Boolean).filter((p) => !isSideboardScratchPath(p));
|
|
2706
2741
|
}
|
|
2707
|
-
async function listUntrackedFiles(worktreePath, maxHunk) {
|
|
2708
|
-
const
|
|
2709
|
-
if (
|
|
2710
|
-
return Promise.all(
|
|
2742
|
+
async function listUntrackedFiles(worktreePath, maxHunk, paths) {
|
|
2743
|
+
const list = paths ?? await listUntrackedPaths(worktreePath);
|
|
2744
|
+
if (list.length === 0) return [];
|
|
2745
|
+
return Promise.all(list.map((path) => untrackedPatch(worktreePath, path, maxHunk)));
|
|
2746
|
+
}
|
|
2747
|
+
function untrackedStubs(paths) {
|
|
2748
|
+
return paths.map((path) => ({
|
|
2749
|
+
path,
|
|
2750
|
+
status: "A",
|
|
2751
|
+
patch: "",
|
|
2752
|
+
additions: 0,
|
|
2753
|
+
deletions: 0
|
|
2754
|
+
}));
|
|
2755
|
+
}
|
|
2756
|
+
function pathspec(path) {
|
|
2757
|
+
return path ? ["--", path] : [];
|
|
2758
|
+
}
|
|
2759
|
+
function gitDiff(args, cwd, opts) {
|
|
2760
|
+
return git(["diff", "--no-ext-diff", "--no-color", ...args], cwd, {
|
|
2761
|
+
reject: false,
|
|
2762
|
+
timeoutMs: opts?.timeoutMs
|
|
2763
|
+
});
|
|
2764
|
+
}
|
|
2765
|
+
function fileFromPatch(path, patch, maxHunk) {
|
|
2766
|
+
let status = "M";
|
|
2767
|
+
if (/^new file mode /m.test(patch) || /^--- \/dev\/null/m.test(patch)) {
|
|
2768
|
+
status = "A";
|
|
2769
|
+
} else if (/^deleted file mode /m.test(patch) || /^\+\+\+ \/dev\/null/m.test(patch)) {
|
|
2770
|
+
status = "D";
|
|
2771
|
+
} else if (/^rename from /m.test(patch)) {
|
|
2772
|
+
status = "R";
|
|
2773
|
+
}
|
|
2774
|
+
const additions = patch.split("\n").filter((l) => l.startsWith("+") && !l.startsWith("+++")).length;
|
|
2775
|
+
const deletions = patch.split("\n").filter((l) => l.startsWith("-") && !l.startsWith("---")).length;
|
|
2776
|
+
return {
|
|
2777
|
+
path,
|
|
2778
|
+
status,
|
|
2779
|
+
patch: capPatch(patch, maxHunk),
|
|
2780
|
+
additions,
|
|
2781
|
+
deletions
|
|
2782
|
+
};
|
|
2783
|
+
}
|
|
2784
|
+
async function gitDiffParts(worktreePath, diffArgs, includePatches, path) {
|
|
2785
|
+
const extra = pathspec(path);
|
|
2786
|
+
if (includePatches) {
|
|
2787
|
+
const diff = await gitDiff([...diffArgs, ...extra], worktreePath, {
|
|
2788
|
+
timeoutMs: 4e3
|
|
2789
|
+
});
|
|
2790
|
+
return { nameStatus: "", numstat: "", combinedDiff: diff.stdout };
|
|
2791
|
+
}
|
|
2792
|
+
const [ns, num] = await Promise.all([
|
|
2793
|
+
gitDiff(["--name-status", ...diffArgs, ...extra], worktreePath),
|
|
2794
|
+
gitDiff(["--numstat", ...diffArgs, ...extra], worktreePath)
|
|
2795
|
+
]);
|
|
2796
|
+
return {
|
|
2797
|
+
nameStatus: ns.stdout,
|
|
2798
|
+
numstat: num.stdout,
|
|
2799
|
+
combinedDiff: ""
|
|
2800
|
+
};
|
|
2801
|
+
}
|
|
2802
|
+
function formatStat(files) {
|
|
2803
|
+
if (files.length === 0) return "(no changes)";
|
|
2804
|
+
const additions = files.reduce((n2, f) => n2 + (f.additions ?? 0), 0);
|
|
2805
|
+
const deletions = files.reduce((n2, f) => n2 + (f.deletions ?? 0), 0);
|
|
2806
|
+
const n = files.length;
|
|
2807
|
+
return `${n} file${n === 1 ? "" : "s"} changed, ${additions} insertions(+), ${deletions} deletions(-)`;
|
|
2808
|
+
}
|
|
2809
|
+
function emptyScopeStats() {
|
|
2810
|
+
const z3 = emptyStat();
|
|
2811
|
+
return {
|
|
2812
|
+
commits: z3,
|
|
2813
|
+
uncommitted: z3,
|
|
2814
|
+
staged: z3,
|
|
2815
|
+
unstaged: z3,
|
|
2816
|
+
last_turn: z3
|
|
2817
|
+
};
|
|
2711
2818
|
}
|
|
2712
2819
|
function filesFromDiff(nameStatus, numstat, combinedDiff, maxHunk) {
|
|
2713
|
-
const counts = parseNumstat(numstat);
|
|
2714
2820
|
const patches = splitCombinedDiff(combinedDiff);
|
|
2821
|
+
if (!nameStatus.trim() && patches.size > 0) {
|
|
2822
|
+
return [...patches.entries()].map(
|
|
2823
|
+
([path, patch]) => fileFromPatch(path, patch, maxHunk)
|
|
2824
|
+
);
|
|
2825
|
+
}
|
|
2826
|
+
const counts = parseNumstat(numstat);
|
|
2715
2827
|
const files = [];
|
|
2716
2828
|
for (const { status, path } of parseNameStatus(nameStatus)) {
|
|
2717
2829
|
const n = counts.get(path);
|
|
@@ -2733,11 +2845,11 @@ async function computeScopeStats(worktreePath, mergeBase, lastTurnBase, untracke
|
|
|
2733
2845
|
{ stdout: unstagedNum },
|
|
2734
2846
|
lastTurnNum
|
|
2735
2847
|
] = await Promise.all([
|
|
2736
|
-
mergeBase ?
|
|
2737
|
-
|
|
2738
|
-
|
|
2739
|
-
|
|
2740
|
-
lastTurnBase ?
|
|
2848
|
+
mergeBase ? gitDiff(["--numstat", mergeBase], worktreePath) : Promise.resolve({ stdout: "" }),
|
|
2849
|
+
gitDiff(["--numstat", "HEAD"], worktreePath),
|
|
2850
|
+
gitDiff(["--numstat", "--cached"], worktreePath),
|
|
2851
|
+
gitDiff(["--numstat"], worktreePath),
|
|
2852
|
+
lastTurnBase ? gitDiff(["--numstat", lastTurnBase], worktreePath) : Promise.resolve({ stdout: "" })
|
|
2741
2853
|
]);
|
|
2742
2854
|
return {
|
|
2743
2855
|
commits: mergeBase ? statFromNumstat(commitsNum, untrackedCount) : emptyStat(),
|
|
@@ -2756,7 +2868,7 @@ async function listBranchCommits(worktreePath, repoPath, opts) {
|
|
|
2756
2868
|
if (head.exitCode !== 0) return [];
|
|
2757
2869
|
let base = opts?.base;
|
|
2758
2870
|
try {
|
|
2759
|
-
base = base ?? await resolveDefaultBranch(repoPath);
|
|
2871
|
+
base = base ?? await resolveDefaultBranch(repoPath, { network: false });
|
|
2760
2872
|
} catch {
|
|
2761
2873
|
base = "HEAD";
|
|
2762
2874
|
}
|
|
@@ -2786,7 +2898,121 @@ async function listBranchCommits(worktreePath, repoPath, opts) {
|
|
|
2786
2898
|
}
|
|
2787
2899
|
return commits;
|
|
2788
2900
|
}
|
|
2901
|
+
var SHA_RE = /^[0-9a-f]{7,40}$/i;
|
|
2902
|
+
function emptyPathDiff(scope, base, hasLastTurnBase, commitSha = null) {
|
|
2903
|
+
return {
|
|
2904
|
+
scope,
|
|
2905
|
+
commitSha: scope === "commits" ? commitSha : null,
|
|
2906
|
+
base,
|
|
2907
|
+
files: [],
|
|
2908
|
+
stat: formatStat([]),
|
|
2909
|
+
dirty: false,
|
|
2910
|
+
unpushed: 0,
|
|
2911
|
+
hasLastTurnBase,
|
|
2912
|
+
commits: [],
|
|
2913
|
+
scopeStats: emptyScopeStats()
|
|
2914
|
+
};
|
|
2915
|
+
}
|
|
2916
|
+
async function getPathDiff(worktreePath, repoPath, opts) {
|
|
2917
|
+
const { path, scope, commitSha, lastTurnBase, hasLastTurnBase, includePatches, maxHunk } = opts;
|
|
2918
|
+
if (scope === "last_turn" && !lastTurnBase) {
|
|
2919
|
+
return emptyPathDiff(scope, "last turn", false);
|
|
2920
|
+
}
|
|
2921
|
+
let diffArgs;
|
|
2922
|
+
let labelBase;
|
|
2923
|
+
if (scope === "staged") {
|
|
2924
|
+
diffArgs = ["--cached"];
|
|
2925
|
+
labelBase = "staged";
|
|
2926
|
+
} else if (scope === "unstaged") {
|
|
2927
|
+
diffArgs = [];
|
|
2928
|
+
labelBase = "unstaged";
|
|
2929
|
+
} else if (scope === "uncommitted") {
|
|
2930
|
+
diffArgs = ["HEAD"];
|
|
2931
|
+
labelBase = "HEAD";
|
|
2932
|
+
} else if (scope === "last_turn") {
|
|
2933
|
+
diffArgs = [lastTurnBase];
|
|
2934
|
+
labelBase = "last turn";
|
|
2935
|
+
} else if (scope === "commits" && commitSha) {
|
|
2936
|
+
diffArgs = [`${commitSha}^!`];
|
|
2937
|
+
labelBase = commitSha.slice(0, 7);
|
|
2938
|
+
} else if (scope === "commits" && opts.base && SHA_RE.test(opts.base)) {
|
|
2939
|
+
diffArgs = [opts.base];
|
|
2940
|
+
labelBase = opts.base.slice(0, 7);
|
|
2941
|
+
} else if (scope === "commits") {
|
|
2942
|
+
let base = opts.base;
|
|
2943
|
+
try {
|
|
2944
|
+
base = base ?? await resolveDefaultBranch(repoPath, { network: false });
|
|
2945
|
+
} catch {
|
|
2946
|
+
base = "HEAD";
|
|
2947
|
+
}
|
|
2948
|
+
const mergeBase = await resolveMergeBase(worktreePath, base, repoPath);
|
|
2949
|
+
diffArgs = [mergeBase];
|
|
2950
|
+
labelBase = mergeBase;
|
|
2951
|
+
} else {
|
|
2952
|
+
diffArgs = ["HEAD"];
|
|
2953
|
+
labelBase = "HEAD";
|
|
2954
|
+
}
|
|
2955
|
+
const diff = await gitDiff(
|
|
2956
|
+
includePatches ? [...diffArgs, "--", path] : ["--name-status", ...diffArgs, "--", path],
|
|
2957
|
+
worktreePath,
|
|
2958
|
+
{ timeoutMs: 4e3 }
|
|
2959
|
+
);
|
|
2960
|
+
const filesMap = /* @__PURE__ */ new Map();
|
|
2961
|
+
if (includePatches) {
|
|
2962
|
+
const parsed = filesFromDiff("", "", diff.stdout, maxHunk);
|
|
2963
|
+
for (const file of parsed) filesMap.set(file.path, file);
|
|
2964
|
+
if (diff.stdout.trim() && !filesMap.has(path)) {
|
|
2965
|
+
filesMap.set(path, fileFromPatch(path, diff.stdout, maxHunk));
|
|
2966
|
+
}
|
|
2967
|
+
} else {
|
|
2968
|
+
for (const file of filesFromDiff(diff.stdout, "", "", maxHunk)) {
|
|
2969
|
+
filesMap.set(file.path, file);
|
|
2970
|
+
}
|
|
2971
|
+
}
|
|
2972
|
+
if (filesMap.size === 0) {
|
|
2973
|
+
const maybe = await listUntrackedPaths(worktreePath, path);
|
|
2974
|
+
if (maybe.length > 0) {
|
|
2975
|
+
const extra = includePatches ? await listUntrackedFiles(worktreePath, maxHunk, maybe) : untrackedStubs(maybe);
|
|
2976
|
+
for (const file of extra) filesMap.set(file.path, file);
|
|
2977
|
+
}
|
|
2978
|
+
}
|
|
2979
|
+
const files = [...filesMap.values()].sort((a, b) => a.path.localeCompare(b.path));
|
|
2980
|
+
return {
|
|
2981
|
+
scope,
|
|
2982
|
+
commitSha: scope === "commits" ? commitSha : null,
|
|
2983
|
+
base: labelBase,
|
|
2984
|
+
files,
|
|
2985
|
+
stat: formatStat(files),
|
|
2986
|
+
dirty: files.length > 0,
|
|
2987
|
+
unpushed: 0,
|
|
2988
|
+
hasLastTurnBase,
|
|
2989
|
+
commits: [],
|
|
2990
|
+
scopeStats: emptyScopeStats(),
|
|
2991
|
+
mergeBase: SHA_RE.test(diffArgs[0] ?? "") ? diffArgs[0] : null
|
|
2992
|
+
};
|
|
2993
|
+
}
|
|
2789
2994
|
async function getDiff(worktreePath, repoPath, opts) {
|
|
2995
|
+
const maxHunk = opts?.maxHunkChars ?? 8e3;
|
|
2996
|
+
const scope = opts?.scope ?? "commits";
|
|
2997
|
+
const lastTurnBase = opts?.lastTurnBase?.trim() || null;
|
|
2998
|
+
const hasLastTurnBase = Boolean(lastTurnBase);
|
|
2999
|
+
const commitSha = opts?.commitSha?.trim() || null;
|
|
3000
|
+
const includePatches = opts?.includePatches ?? true;
|
|
3001
|
+
const pathFilter = opts?.path?.trim() || null;
|
|
3002
|
+
const fileOnly = Boolean(pathFilter);
|
|
3003
|
+
const includeMeta = (opts?.includeMeta ?? true) && !fileOnly;
|
|
3004
|
+
if (fileOnly && pathFilter && !includeMeta) {
|
|
3005
|
+
return getPathDiff(worktreePath, repoPath, {
|
|
3006
|
+
path: pathFilter,
|
|
3007
|
+
scope,
|
|
3008
|
+
commitSha,
|
|
3009
|
+
lastTurnBase,
|
|
3010
|
+
hasLastTurnBase,
|
|
3011
|
+
base: opts?.base,
|
|
3012
|
+
includePatches,
|
|
3013
|
+
maxHunk
|
|
3014
|
+
});
|
|
3015
|
+
}
|
|
2790
3016
|
const status = await inspectGitWorktree(worktreePath);
|
|
2791
3017
|
if (status === "missing_worktree") {
|
|
2792
3018
|
throw new Error("Worktree not found");
|
|
@@ -2794,18 +3020,19 @@ async function getDiff(worktreePath, repoPath, opts) {
|
|
|
2794
3020
|
if (status === "not_git") {
|
|
2795
3021
|
throw new Error("Not a Git repository");
|
|
2796
3022
|
}
|
|
2797
|
-
const
|
|
2798
|
-
const
|
|
2799
|
-
const
|
|
2800
|
-
const hasLastTurnBase = Boolean(lastTurnBase);
|
|
2801
|
-
const commitSha = opts?.commitSha?.trim() || null;
|
|
2802
|
-
const head = await git(["rev-parse", "--verify", "HEAD"], worktreePath, {
|
|
3023
|
+
const wantUntracked = opts?.includeUntracked !== void 0 ? opts.includeUntracked : !fileOnly && (scope === "commits" && !commitSha || scope === "uncommitted" || scope === "unstaged" || scope === "last_turn");
|
|
3024
|
+
const needsMergeBase = scope === "commits" && !commitSha || includeMeta;
|
|
3025
|
+
const headP = git(["rev-parse", "--verify", "HEAD"], worktreePath, {
|
|
2803
3026
|
reject: false
|
|
2804
3027
|
});
|
|
3028
|
+
const untrackedP = wantUntracked ? listUntrackedPaths(worktreePath, pathFilter) : Promise.resolve([]);
|
|
3029
|
+
const head = await headP;
|
|
2805
3030
|
const hasHead = head.exitCode === 0;
|
|
2806
|
-
const untrackedPaths = await listUntrackedPaths(worktreePath);
|
|
2807
3031
|
if (!hasHead) {
|
|
2808
|
-
const
|
|
3032
|
+
const untrackedPaths2 = await untrackedP;
|
|
3033
|
+
const files2 = scope === "staged" ? [] : includePatches ? (await listUntrackedFiles(worktreePath, maxHunk, untrackedPaths2)).sort(
|
|
3034
|
+
(a, b) => a.path.localeCompare(b.path)
|
|
3035
|
+
) : untrackedStubs(untrackedPaths2).sort(
|
|
2809
3036
|
(a, b) => a.path.localeCompare(b.path)
|
|
2810
3037
|
);
|
|
2811
3038
|
const untrackedStat = {
|
|
@@ -2819,7 +3046,7 @@ async function getDiff(worktreePath, repoPath, opts) {
|
|
|
2819
3046
|
commitSha: null,
|
|
2820
3047
|
base: "(no commits)",
|
|
2821
3048
|
files: files2,
|
|
2822
|
-
stat: files2
|
|
3049
|
+
stat: formatStat(files2),
|
|
2823
3050
|
dirty: files2.length > 0,
|
|
2824
3051
|
unpushed: 0,
|
|
2825
3052
|
hasLastTurnBase,
|
|
@@ -2833,137 +3060,122 @@ async function getDiff(worktreePath, repoPath, opts) {
|
|
|
2833
3060
|
}
|
|
2834
3061
|
};
|
|
2835
3062
|
}
|
|
2836
|
-
let
|
|
2837
|
-
|
|
2838
|
-
|
|
2839
|
-
|
|
2840
|
-
|
|
2841
|
-
|
|
2842
|
-
|
|
2843
|
-
|
|
2844
|
-
|
|
2845
|
-
await
|
|
2846
|
-
|
|
2847
|
-
|
|
2848
|
-
|
|
3063
|
+
let mergeBase = "HEAD";
|
|
3064
|
+
let baseLabel = "HEAD";
|
|
3065
|
+
if (needsMergeBase) {
|
|
3066
|
+
let base = opts?.base;
|
|
3067
|
+
try {
|
|
3068
|
+
base = base ?? await resolveDefaultBranch(repoPath, { network: false });
|
|
3069
|
+
} catch {
|
|
3070
|
+
base = "HEAD";
|
|
3071
|
+
}
|
|
3072
|
+
mergeBase = await resolveMergeBase(worktreePath, base, repoPath);
|
|
3073
|
+
baseLabel = await resolveDiffBaseRef(worktreePath, base, repoPath);
|
|
3074
|
+
}
|
|
3075
|
+
const includeUntracked = wantUntracked;
|
|
3076
|
+
const metaPromise = includeMeta ? Promise.all([
|
|
3077
|
+
computeScopeStats(
|
|
3078
|
+
worktreePath,
|
|
3079
|
+
mergeBase,
|
|
3080
|
+
lastTurnBase,
|
|
3081
|
+
// Cheap: don't wait on the untracked walk just to pad the badge.
|
|
3082
|
+
0
|
|
3083
|
+
),
|
|
3084
|
+
listBranchCommits(worktreePath, repoPath, { base: baseLabel }),
|
|
3085
|
+
isDirty(worktreePath),
|
|
3086
|
+
countUnpushedCommits(worktreePath)
|
|
3087
|
+
]).then(([scopeStats, commits, dirty, unpushed]) => ({
|
|
3088
|
+
scopeStats,
|
|
3089
|
+
commits,
|
|
3090
|
+
dirty,
|
|
3091
|
+
unpushed
|
|
3092
|
+
})) : Promise.resolve({
|
|
3093
|
+
scopeStats: emptyScopeStats(),
|
|
3094
|
+
commits: [],
|
|
3095
|
+
dirty: false,
|
|
3096
|
+
unpushed: 0
|
|
3097
|
+
});
|
|
3098
|
+
if (scope === "last_turn" && !lastTurnBase) {
|
|
3099
|
+
const meta2 = await metaPromise;
|
|
3100
|
+
return {
|
|
3101
|
+
scope,
|
|
3102
|
+
commitSha: null,
|
|
3103
|
+
base: "last turn",
|
|
3104
|
+
files: [],
|
|
3105
|
+
stat: "(no last agent turn)",
|
|
3106
|
+
dirty: meta2.dirty,
|
|
3107
|
+
unpushed: meta2.unpushed,
|
|
3108
|
+
hasLastTurnBase: false,
|
|
3109
|
+
commits: meta2.commits,
|
|
3110
|
+
scopeStats: meta2.scopeStats
|
|
3111
|
+
};
|
|
2849
3112
|
}
|
|
2850
|
-
|
|
2851
|
-
|
|
2852
|
-
worktreePath,
|
|
2853
|
-
mergeBase,
|
|
2854
|
-
lastTurnBase,
|
|
2855
|
-
untrackedPaths.length
|
|
2856
|
-
);
|
|
2857
|
-
const includeUntracked = scope === "commits" && !commitSha || scope === "uncommitted" || scope === "unstaged" || scope === "last_turn";
|
|
2858
|
-
let nameStatus = "";
|
|
2859
|
-
let numstat = "";
|
|
2860
|
-
let combinedDiff = "";
|
|
2861
|
-
let labelBase = base;
|
|
3113
|
+
let diffArgs;
|
|
3114
|
+
let labelBase = baseLabel;
|
|
2862
3115
|
if (scope === "staged") {
|
|
2863
|
-
|
|
2864
|
-
git(["diff", "--name-status", "--cached"], worktreePath, { reject: false }),
|
|
2865
|
-
git(["diff", "--numstat", "--cached"], worktreePath, { reject: false }),
|
|
2866
|
-
git(["diff", "--cached"], worktreePath, { reject: false })
|
|
2867
|
-
]);
|
|
2868
|
-
nameStatus = ns.stdout;
|
|
2869
|
-
numstat = num.stdout;
|
|
2870
|
-
combinedDiff = diff.stdout;
|
|
3116
|
+
diffArgs = ["--cached"];
|
|
2871
3117
|
labelBase = "staged";
|
|
2872
3118
|
} else if (scope === "unstaged") {
|
|
2873
|
-
|
|
2874
|
-
git(["diff", "--name-status"], worktreePath, { reject: false }),
|
|
2875
|
-
git(["diff", "--numstat"], worktreePath, { reject: false }),
|
|
2876
|
-
git(["diff"], worktreePath, { reject: false })
|
|
2877
|
-
]);
|
|
2878
|
-
nameStatus = ns.stdout;
|
|
2879
|
-
numstat = num.stdout;
|
|
2880
|
-
combinedDiff = diff.stdout;
|
|
3119
|
+
diffArgs = [];
|
|
2881
3120
|
labelBase = "unstaged";
|
|
2882
3121
|
} else if (scope === "uncommitted") {
|
|
2883
|
-
|
|
2884
|
-
git(["diff", "--name-status", "HEAD"], worktreePath, { reject: false }),
|
|
2885
|
-
git(["diff", "--numstat", "HEAD"], worktreePath, { reject: false }),
|
|
2886
|
-
git(["diff", "HEAD"], worktreePath, { reject: false })
|
|
2887
|
-
]);
|
|
2888
|
-
nameStatus = ns.stdout;
|
|
2889
|
-
numstat = num.stdout;
|
|
2890
|
-
combinedDiff = diff.stdout;
|
|
3122
|
+
diffArgs = ["HEAD"];
|
|
2891
3123
|
labelBase = "HEAD";
|
|
2892
3124
|
} else if (scope === "last_turn") {
|
|
2893
|
-
|
|
2894
|
-
const commits2 = await listBranchCommits(worktreePath, repoPath, { base });
|
|
2895
|
-
return {
|
|
2896
|
-
scope,
|
|
2897
|
-
commitSha: null,
|
|
2898
|
-
base: "last turn",
|
|
2899
|
-
files: [],
|
|
2900
|
-
stat: "(no last agent turn)",
|
|
2901
|
-
dirty: await isDirty(worktreePath),
|
|
2902
|
-
unpushed: await countUnpushedCommits(worktreePath),
|
|
2903
|
-
hasLastTurnBase: false,
|
|
2904
|
-
commits: commits2,
|
|
2905
|
-
scopeStats
|
|
2906
|
-
};
|
|
2907
|
-
}
|
|
2908
|
-
const [ns, num, diff] = await Promise.all([
|
|
2909
|
-
git(["diff", "--name-status", lastTurnBase], worktreePath, { reject: false }),
|
|
2910
|
-
git(["diff", "--numstat", lastTurnBase], worktreePath, { reject: false }),
|
|
2911
|
-
git(["diff", lastTurnBase], worktreePath, { reject: false })
|
|
2912
|
-
]);
|
|
2913
|
-
nameStatus = ns.stdout;
|
|
2914
|
-
numstat = num.stdout;
|
|
2915
|
-
combinedDiff = diff.stdout;
|
|
3125
|
+
diffArgs = [lastTurnBase];
|
|
2916
3126
|
labelBase = "last turn";
|
|
2917
3127
|
} else if (scope === "commits" && commitSha) {
|
|
2918
|
-
|
|
2919
|
-
const [ns, num, diff] = await Promise.all([
|
|
2920
|
-
git(["diff", "--name-status", range], worktreePath, { reject: false }),
|
|
2921
|
-
git(["diff", "--numstat", range], worktreePath, { reject: false }),
|
|
2922
|
-
git(["diff", range], worktreePath, { reject: false })
|
|
2923
|
-
]);
|
|
2924
|
-
nameStatus = ns.stdout;
|
|
2925
|
-
numstat = num.stdout;
|
|
2926
|
-
combinedDiff = diff.stdout;
|
|
3128
|
+
diffArgs = [`${commitSha}^!`];
|
|
2927
3129
|
labelBase = commitSha.slice(0, 7);
|
|
2928
3130
|
} else {
|
|
2929
|
-
|
|
2930
|
-
|
|
2931
|
-
|
|
2932
|
-
|
|
2933
|
-
|
|
2934
|
-
|
|
2935
|
-
|
|
2936
|
-
|
|
2937
|
-
|
|
2938
|
-
|
|
2939
|
-
|
|
3131
|
+
diffArgs = [mergeBase];
|
|
3132
|
+
labelBase = baseLabel;
|
|
3133
|
+
}
|
|
3134
|
+
const partsPromise = gitDiffParts(
|
|
3135
|
+
worktreePath,
|
|
3136
|
+
diffArgs,
|
|
3137
|
+
includePatches,
|
|
3138
|
+
pathFilter
|
|
3139
|
+
);
|
|
3140
|
+
const [parts, meta, untrackedPaths] = await Promise.all([
|
|
3141
|
+
partsPromise,
|
|
3142
|
+
metaPromise,
|
|
3143
|
+
untrackedP
|
|
3144
|
+
]);
|
|
2940
3145
|
const filesMap = /* @__PURE__ */ new Map();
|
|
2941
|
-
for (const file of filesFromDiff(
|
|
3146
|
+
for (const file of filesFromDiff(
|
|
3147
|
+
parts.nameStatus,
|
|
3148
|
+
parts.numstat,
|
|
3149
|
+
parts.combinedDiff,
|
|
3150
|
+
maxHunk
|
|
3151
|
+
)) {
|
|
2942
3152
|
filesMap.set(file.path, file);
|
|
2943
3153
|
}
|
|
2944
3154
|
if (includeUntracked) {
|
|
2945
|
-
const
|
|
2946
|
-
for (const file of
|
|
3155
|
+
const extra = includePatches ? await listUntrackedFiles(worktreePath, maxHunk, untrackedPaths) : untrackedStubs(untrackedPaths);
|
|
3156
|
+
for (const file of extra) {
|
|
2947
3157
|
if (!filesMap.has(file.path)) filesMap.set(file.path, file);
|
|
2948
3158
|
}
|
|
3159
|
+
} else if (fileOnly && pathFilter && !filesMap.has(pathFilter)) {
|
|
3160
|
+
const maybe = await listUntrackedPaths(worktreePath, pathFilter);
|
|
3161
|
+
if (maybe.length > 0) {
|
|
3162
|
+
const extra = includePatches ? await listUntrackedFiles(worktreePath, maxHunk, maybe) : untrackedStubs(maybe);
|
|
3163
|
+
for (const file of extra) filesMap.set(file.path, file);
|
|
3164
|
+
}
|
|
2949
3165
|
}
|
|
2950
3166
|
const files = [...filesMap.values()].sort((a, b) => a.path.localeCompare(b.path));
|
|
2951
|
-
const { stdout: statOut } = await git(
|
|
2952
|
-
scope === "staged" ? ["diff", "--stat", "--cached"] : scope === "unstaged" ? ["diff", "--stat"] : scope === "uncommitted" ? ["diff", "--stat", "HEAD"] : scope === "last_turn" && lastTurnBase ? ["diff", "--stat", lastTurnBase] : scope === "commits" && commitSha ? ["diff", "--stat", `${commitSha}^!`] : ["diff", "--stat", mergeBase],
|
|
2953
|
-
worktreePath,
|
|
2954
|
-
{ reject: false }
|
|
2955
|
-
);
|
|
2956
3167
|
return {
|
|
2957
3168
|
scope,
|
|
2958
3169
|
commitSha: scope === "commits" ? commitSha : null,
|
|
2959
3170
|
base: labelBase,
|
|
2960
3171
|
files,
|
|
2961
|
-
stat:
|
|
2962
|
-
dirty:
|
|
2963
|
-
unpushed:
|
|
3172
|
+
stat: formatStat(files),
|
|
3173
|
+
dirty: fileOnly || !includeMeta ? files.length > 0 : meta.dirty,
|
|
3174
|
+
unpushed: meta.unpushed,
|
|
2964
3175
|
hasLastTurnBase,
|
|
2965
|
-
commits,
|
|
2966
|
-
scopeStats
|
|
3176
|
+
commits: meta.commits,
|
|
3177
|
+
scopeStats: meta.scopeStats,
|
|
3178
|
+
mergeBase: needsMergeBase ? mergeBase : null
|
|
2967
3179
|
};
|
|
2968
3180
|
}
|
|
2969
3181
|
async function listWorktreeFiles(worktreePath, opts) {
|
|
@@ -3179,7 +3391,7 @@ async function previewLand(thread) {
|
|
|
3179
3391
|
const target = await resolveDefaultBranch(thread.repoPath);
|
|
3180
3392
|
if (thread.branchName === target || thread.branchName === "main" || thread.branchName === "master") {
|
|
3181
3393
|
}
|
|
3182
|
-
const { stdout: head } = await import("../run-
|
|
3394
|
+
const { stdout: head } = await import("../run-A4RHY7HH.js").then(
|
|
3183
3395
|
({ git: git2 }) => git2(["rev-parse", "--abbrev-ref", "HEAD"], thread.worktreePath)
|
|
3184
3396
|
);
|
|
3185
3397
|
const current = head.trim();
|
|
@@ -3218,7 +3430,7 @@ async function confirmLand(thread, opts) {
|
|
|
3218
3430
|
if (preview.dirty) {
|
|
3219
3431
|
committed = await commitAll(thread.worktreePath, meta.commitMessage);
|
|
3220
3432
|
}
|
|
3221
|
-
const { git: git2 } = await import("../run-
|
|
3433
|
+
const { git: git2 } = await import("../run-A4RHY7HH.js");
|
|
3222
3434
|
const { stdout: headOut } = await git2(
|
|
3223
3435
|
["rev-parse", "--abbrev-ref", "HEAD"],
|
|
3224
3436
|
thread.worktreePath,
|
|
@@ -3910,7 +4122,7 @@ var Orchestrator = class {
|
|
|
3910
4122
|
orphans: orphans.map((o) => ({ path: o.path, repoPath: o.repoPath }))
|
|
3911
4123
|
});
|
|
3912
4124
|
}
|
|
3913
|
-
const { autoCleanupOrphansEnabled } = await import("../app-settings-
|
|
4125
|
+
const { autoCleanupOrphansEnabled } = await import("../app-settings-P42Z3VOB.js");
|
|
3914
4126
|
if (autoCleanupOrphansEnabled() && shouldRunWorktreeCleanup() && orphans.length > 0) {
|
|
3915
4127
|
await cleanupOrphanWorktrees({ repoPaths });
|
|
3916
4128
|
}
|
|
@@ -4067,7 +4279,7 @@ var Orchestrator = class {
|
|
|
4067
4279
|
}
|
|
4068
4280
|
async finishCreateThread(threadId, prompt) {
|
|
4069
4281
|
await this.runSetupAfterCreate(threadId);
|
|
4070
|
-
const { autoRunAfterSetupEnabled } = await import("../app-settings-
|
|
4282
|
+
const { autoRunAfterSetupEnabled } = await import("../app-settings-P42Z3VOB.js");
|
|
4071
4283
|
if (autoRunAfterSetupEnabled()) {
|
|
4072
4284
|
try {
|
|
4073
4285
|
await this.startDev(threadId);
|
|
@@ -4147,11 +4359,11 @@ var Orchestrator = class {
|
|
|
4147
4359
|
return results;
|
|
4148
4360
|
}
|
|
4149
4361
|
/** Edit the text of a not-yet-started queued message. */
|
|
4150
|
-
async editQueuedMessage(threadRef, index,
|
|
4362
|
+
async editQueuedMessage(threadRef, index, text2) {
|
|
4151
4363
|
const thread = this.requireThread(threadRef);
|
|
4152
4364
|
return withThreadLock(thread.id, async () => {
|
|
4153
4365
|
const current = this.requireThread(thread.id);
|
|
4154
|
-
const trimmed =
|
|
4366
|
+
const trimmed = text2.trim();
|
|
4155
4367
|
if (!trimmed || index < 0 || index >= current.queue.length) {
|
|
4156
4368
|
return current;
|
|
4157
4369
|
}
|
|
@@ -4296,10 +4508,19 @@ var Orchestrator = class {
|
|
|
4296
4508
|
attachments: sentAttachments ?? thread.attachments
|
|
4297
4509
|
}
|
|
4298
4510
|
);
|
|
4299
|
-
const
|
|
4300
|
-
|
|
4301
|
-
|
|
4302
|
-
|
|
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;
|
|
4303
4524
|
const artifactReminder = thread.agent !== "brightsy" ? [
|
|
4304
4525
|
"Sideboard side column (important):",
|
|
4305
4526
|
"There is no claude.ai Artifact tool here \u2014 that is normal.",
|
|
@@ -4339,7 +4560,7 @@ var Orchestrator = class {
|
|
|
4339
4560
|
});
|
|
4340
4561
|
const artifactDirective = isBrightsy ? null : formatArtifactDirective();
|
|
4341
4562
|
const settings = loadWorkspaceSettings(fresh.worktreePath, fresh.repoPath);
|
|
4342
|
-
const { autoRenameBranchEnabled } = await import("../app-settings-
|
|
4563
|
+
const { autoRenameBranchEnabled } = await import("../app-settings-P42Z3VOB.js");
|
|
4343
4564
|
const renameBranchDirective = !isBrightsy && !isOrchestration && autoRenameBranchEnabled() ? formatRenameBranchDirective(fresh, {
|
|
4344
4565
|
customPrompt: settings?.prompts?.renameBranch
|
|
4345
4566
|
}) : null;
|
|
@@ -4359,7 +4580,9 @@ var Orchestrator = class {
|
|
|
4359
4580
|
goal: fresh.sourceRef || fresh.title || "Orchestration",
|
|
4360
4581
|
parentId: fresh.id,
|
|
4361
4582
|
workspaces: inventory,
|
|
4362
|
-
audience:
|
|
4583
|
+
audience: /^(Slack DM|Slack @mention)(?:\n|$)/.test(
|
|
4584
|
+
expandedPrompt.trim()
|
|
4585
|
+
) ? "slack" : "desktop"
|
|
4363
4586
|
});
|
|
4364
4587
|
}
|
|
4365
4588
|
}
|
|
@@ -4902,6 +5125,11 @@ var Orchestrator = class {
|
|
|
4902
5125
|
return getDiff(thread.worktreePath, thread.repoPath, {
|
|
4903
5126
|
scope: opts?.scope,
|
|
4904
5127
|
commitSha: opts?.commitSha,
|
|
5128
|
+
base: opts?.base,
|
|
5129
|
+
includePatches: opts?.includePatches,
|
|
5130
|
+
includeMeta: opts?.includeMeta,
|
|
5131
|
+
includeUntracked: opts?.includeUntracked,
|
|
5132
|
+
path: opts?.path,
|
|
4905
5133
|
lastTurnBase: this.turnBaselines.get(thread.id) ?? null
|
|
4906
5134
|
});
|
|
4907
5135
|
}
|
|
@@ -4979,7 +5207,8 @@ var Orchestrator = class {
|
|
|
4979
5207
|
isDraft: false,
|
|
4980
5208
|
reviewDecision: null,
|
|
4981
5209
|
baseRefName: "",
|
|
4982
|
-
headRefName: ""
|
|
5210
|
+
headRefName: "",
|
|
5211
|
+
isInMergeQueue: false
|
|
4983
5212
|
};
|
|
4984
5213
|
await this.persistPrMetaAndMaybeArchive(thread, metaLike);
|
|
4985
5214
|
return { url: metaLike.url, state };
|
|
@@ -5029,7 +5258,7 @@ var Orchestrator = class {
|
|
|
5029
5258
|
updateThread(thread.id, { title: meta.title });
|
|
5030
5259
|
}
|
|
5031
5260
|
}
|
|
5032
|
-
const { autoArchiveOnMergeEnabled } = await import("../app-settings-
|
|
5261
|
+
const { autoArchiveOnMergeEnabled } = await import("../app-settings-P42Z3VOB.js");
|
|
5033
5262
|
const latest = this.requireThread(thread.id);
|
|
5034
5263
|
if (!shouldAutoArchiveOnPrMerge({
|
|
5035
5264
|
previousPrState: prevState || null,
|
|
@@ -5253,7 +5482,7 @@ var Orchestrator = class {
|
|
|
5253
5482
|
this.emit({ type: "status_changed", threadId: archived.id, status: "archived" });
|
|
5254
5483
|
if (thread.repoPath && !isGlobalRepoPath(thread.repoPath)) {
|
|
5255
5484
|
try {
|
|
5256
|
-
const { ensureWorkspace: ensureWorkspace2 } = await import("../workspaces-
|
|
5485
|
+
const { ensureWorkspace: ensureWorkspace2 } = await import("../workspaces-SVQIEVIZ.js");
|
|
5257
5486
|
await ensureWorkspace2(thread.repoPath);
|
|
5258
5487
|
} catch {
|
|
5259
5488
|
}
|
|
@@ -5274,7 +5503,7 @@ var Orchestrator = class {
|
|
|
5274
5503
|
await runArchiveScript(thread.repoPath, thread.worktreePath);
|
|
5275
5504
|
} catch {
|
|
5276
5505
|
}
|
|
5277
|
-
const { deleteBranchOnPurgeEnabled } = await import("../app-settings-
|
|
5506
|
+
const { deleteBranchOnPurgeEnabled } = await import("../app-settings-P42Z3VOB.js");
|
|
5278
5507
|
const deleteBranch = opts?.deleteBranch ?? deleteBranchOnPurgeEnabled();
|
|
5279
5508
|
await removeWorktree(thread.repoPath, thread.worktreePath, {
|
|
5280
5509
|
deleteBranch: deleteBranch ? thread.branchName : void 0
|
|
@@ -5295,7 +5524,7 @@ var Orchestrator = class {
|
|
|
5295
5524
|
return restored2;
|
|
5296
5525
|
}
|
|
5297
5526
|
if (!existsSync13(thread.worktreePath)) {
|
|
5298
|
-
const { createThreadWorktree: createThreadWorktree2 } = await import("../worktree-
|
|
5527
|
+
const { createThreadWorktree: createThreadWorktree2 } = await import("../worktree-KWXHMKRN.js");
|
|
5299
5528
|
const { execa: execa6 } = await import("execa");
|
|
5300
5529
|
const slug = thread.worktreePath.split("/").pop();
|
|
5301
5530
|
const dest = thread.worktreePath;
|
|
@@ -5367,6 +5596,103 @@ function getOrchestrator() {
|
|
|
5367
5596
|
return singleton;
|
|
5368
5597
|
}
|
|
5369
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
|
+
|
|
5370
5696
|
// src/integrations/linear.ts
|
|
5371
5697
|
var LINEAR_GRAPHQL = "https://api.linear.app/graphql";
|
|
5372
5698
|
var ASSIGNED_ISSUES_QUERY = `
|
|
@@ -5389,16 +5715,16 @@ query SideboardAssignedIssues($first: Int!) {
|
|
|
5389
5715
|
}
|
|
5390
5716
|
`;
|
|
5391
5717
|
async function listLinearIssuesDirect(opts) {
|
|
5392
|
-
const apiKey = (opts?.apiKey ??
|
|
5718
|
+
const apiKey = (opts?.apiKey ?? await getLinearAuthToken())?.trim();
|
|
5393
5719
|
if (!apiKey) {
|
|
5394
|
-
throw new Error("Linear is not connected \u2014
|
|
5720
|
+
throw new Error("Linear is not connected \u2014 sign in from Account settings");
|
|
5395
5721
|
}
|
|
5396
5722
|
const first = Math.max(1, Math.min(100, opts?.limit ?? 50));
|
|
5397
5723
|
const res = await fetch(LINEAR_GRAPHQL, {
|
|
5398
5724
|
method: "POST",
|
|
5399
5725
|
headers: {
|
|
5400
5726
|
"Content-Type": "application/json",
|
|
5401
|
-
Authorization: apiKey
|
|
5727
|
+
Authorization: linearAuthorizationHeader(apiKey)
|
|
5402
5728
|
},
|
|
5403
5729
|
body: JSON.stringify({
|
|
5404
5730
|
query: ASSIGNED_ISSUES_QUERY,
|
|
@@ -5488,6 +5814,640 @@ function mcpArchiveBlockedReason(thread) {
|
|
|
5488
5814
|
return null;
|
|
5489
5815
|
}
|
|
5490
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
|
+
|
|
5491
6451
|
// src/mcp/server.ts
|
|
5492
6452
|
var MAX_ORCH_THREADS = 5;
|
|
5493
6453
|
var CREATE_THREAD_TIMEOUT_MS = 9e4;
|
|
@@ -5511,7 +6471,7 @@ function withTimeout(promise, ms, label) {
|
|
|
5511
6471
|
async function startMcpServer() {
|
|
5512
6472
|
const orch = getOrchestrator();
|
|
5513
6473
|
try {
|
|
5514
|
-
const { maxConcurrentAgents } = await import("../app-settings-
|
|
6474
|
+
const { maxConcurrentAgents } = await import("../app-settings-P42Z3VOB.js");
|
|
5515
6475
|
orch.setMaxConcurrent(maxConcurrentAgents());
|
|
5516
6476
|
} catch {
|
|
5517
6477
|
}
|
|
@@ -5564,7 +6524,7 @@ async function startMcpServer() {
|
|
|
5564
6524
|
server.tool(
|
|
5565
6525
|
"get_thread",
|
|
5566
6526
|
"Get a compact thread summary by id/ref",
|
|
5567
|
-
{ ref:
|
|
6527
|
+
{ ref: z2.string() },
|
|
5568
6528
|
async ({ ref }) => {
|
|
5569
6529
|
const t = orch.getThread(ref);
|
|
5570
6530
|
if (!t) {
|
|
@@ -5593,14 +6553,14 @@ async function startMcpServer() {
|
|
|
5593
6553
|
"present_artifact",
|
|
5594
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.",
|
|
5595
6555
|
{
|
|
5596
|
-
title:
|
|
5597
|
-
type:
|
|
6556
|
+
title: z2.string().describe("Short title shown in the artifact pane header"),
|
|
6557
|
+
type: z2.enum(["html", "svg", "markdown", "react"]).describe(
|
|
5598
6558
|
"Artifact kind \u2014 html opens an iframe preview; react transpiles and renders a default-exported component in a sandboxed iframe"
|
|
5599
6559
|
),
|
|
5600
|
-
content:
|
|
6560
|
+
content: z2.string().describe(
|
|
5601
6561
|
"Full document body (complete HTML page, SVG markup, markdown, or a React component module with a default export)"
|
|
5602
6562
|
),
|
|
5603
|
-
artifact_id:
|
|
6563
|
+
artifact_id: z2.string().optional().describe("Stable id when updating the same artifact across turns")
|
|
5604
6564
|
},
|
|
5605
6565
|
async ({ title, type, artifact_id }) => {
|
|
5606
6566
|
const id = artifact_id?.trim() || `artifact_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 8)}`;
|
|
@@ -5618,15 +6578,15 @@ async function startMcpServer() {
|
|
|
5618
6578
|
"ask_user",
|
|
5619
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.",
|
|
5620
6580
|
{
|
|
5621
|
-
questions:
|
|
5622
|
-
|
|
5623
|
-
question:
|
|
5624
|
-
header:
|
|
5625
|
-
multiSelect:
|
|
5626
|
-
options:
|
|
5627
|
-
|
|
5628
|
-
label:
|
|
5629
|
-
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)")
|
|
5630
6590
|
})
|
|
5631
6591
|
).min(2).max(6).describe("2\u20136 choices (Sideboard also offers Other)")
|
|
5632
6592
|
})
|
|
@@ -5645,9 +6605,9 @@ async function startMcpServer() {
|
|
|
5645
6605
|
"present_plan",
|
|
5646
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.",
|
|
5647
6607
|
{
|
|
5648
|
-
title:
|
|
5649
|
-
content:
|
|
5650
|
-
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")
|
|
5651
6611
|
},
|
|
5652
6612
|
async ({ title, content, thread_id }) => {
|
|
5653
6613
|
const { writePlanFile: writePlanFile2 } = await import("../plan-file-PHVKUAEE.js");
|
|
@@ -5668,17 +6628,17 @@ async function startMcpServer() {
|
|
|
5668
6628
|
);
|
|
5669
6629
|
server.tool(
|
|
5670
6630
|
"present_schema",
|
|
5671
|
-
"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.",
|
|
5672
6632
|
{
|
|
5673
|
-
title:
|
|
5674
|
-
mode:
|
|
5675
|
-
datasource:
|
|
5676
|
-
resource_id:
|
|
5677
|
-
record_id:
|
|
5678
|
-
resource:
|
|
5679
|
-
record:
|
|
5680
|
-
records:
|
|
5681
|
-
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")
|
|
5682
6642
|
},
|
|
5683
6643
|
async (args) => {
|
|
5684
6644
|
const id = args.pane_id?.trim() || `schema_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 8)}`;
|
|
@@ -5699,10 +6659,10 @@ async function startMcpServer() {
|
|
|
5699
6659
|
"present_files",
|
|
5700
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.",
|
|
5701
6661
|
{
|
|
5702
|
-
title:
|
|
5703
|
-
datasource:
|
|
5704
|
-
path:
|
|
5705
|
-
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")
|
|
5706
6666
|
},
|
|
5707
6667
|
async (args) => {
|
|
5708
6668
|
const id = args.pane_id?.trim() || `files_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 8)}`;
|
|
@@ -5717,23 +6677,62 @@ async function startMcpServer() {
|
|
|
5717
6677
|
return { content: [{ type: "text", text: JSON.stringify(payload) }] };
|
|
5718
6678
|
}
|
|
5719
6679
|
);
|
|
6680
|
+
registerSlackTools(server);
|
|
5720
6681
|
if (sideboardMcpProfile() !== "worktree") {
|
|
5721
|
-
const {
|
|
6682
|
+
const { getCaffeinateHold, setCaffeinateHold } = await import("../caffeinate-hold-BEJIYPJ7.js");
|
|
6683
|
+
const { resolveNewThreadOptions: resolveNewThreadOptions2, resolveThreadDefaults } = await import("../app-settings-P42Z3VOB.js");
|
|
5722
6684
|
const accountDefaults = resolveThreadDefaults();
|
|
5723
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
|
+
);
|
|
5724
6723
|
server.tool(
|
|
5725
6724
|
"create_thread",
|
|
5726
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.`,
|
|
5727
6726
|
{
|
|
5728
|
-
sourceType:
|
|
5729
|
-
sourceRef:
|
|
5730
|
-
agent:
|
|
5731
|
-
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(
|
|
5732
6731
|
`Usually omit to use Account default model (${accountDefaults.model?.trim() || "Auto"}). Pass null only to force Auto / agent-default.`
|
|
5733
6732
|
),
|
|
5734
|
-
repoPath:
|
|
5735
|
-
title:
|
|
5736
|
-
parentThreadId:
|
|
6733
|
+
repoPath: z2.string(),
|
|
6734
|
+
title: z2.string().optional(),
|
|
6735
|
+
parentThreadId: z2.string().optional().describe(
|
|
5737
6736
|
"Orchestration: omit (preferred) or pass YOUR chat id from the turn reminder / AGENTS.md. Do not invent uuids."
|
|
5738
6737
|
)
|
|
5739
6738
|
},
|
|
@@ -5840,9 +6839,9 @@ async function startMcpServer() {
|
|
|
5840
6839
|
"send_to_thread",
|
|
5841
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.",
|
|
5842
6841
|
{
|
|
5843
|
-
ref:
|
|
5844
|
-
prompt:
|
|
5845
|
-
force_stop:
|
|
6842
|
+
ref: z2.string(),
|
|
6843
|
+
prompt: z2.string(),
|
|
6844
|
+
force_stop: z2.boolean().optional()
|
|
5846
6845
|
},
|
|
5847
6846
|
async ({ ref, prompt, force_stop }) => {
|
|
5848
6847
|
if (force_stop) {
|
|
@@ -5871,8 +6870,8 @@ async function startMcpServer() {
|
|
|
5871
6870
|
"wait_for_turn",
|
|
5872
6871
|
"Block until the thread finishes its current/queued turn (avoids polling). Use after send_to_thread to read the agent reply.",
|
|
5873
6872
|
{
|
|
5874
|
-
ref:
|
|
5875
|
-
timeoutMs:
|
|
6873
|
+
ref: z2.string(),
|
|
6874
|
+
timeoutMs: z2.number().optional()
|
|
5876
6875
|
},
|
|
5877
6876
|
async ({ ref, timeoutMs }) => {
|
|
5878
6877
|
const thread = await orch.waitForTurn(ref, timeoutMs ?? 6e5);
|
|
@@ -5894,7 +6893,7 @@ async function startMcpServer() {
|
|
|
5894
6893
|
server.tool(
|
|
5895
6894
|
"get_turn_result",
|
|
5896
6895
|
"Final assistant message only (not full transcript)",
|
|
5897
|
-
{ ref:
|
|
6896
|
+
{ ref: z2.string() },
|
|
5898
6897
|
async ({ ref }) => {
|
|
5899
6898
|
const result = orch.getTurnResult(ref);
|
|
5900
6899
|
return { content: [{ type: "text", text: JSON.stringify(result) }] };
|
|
@@ -5904,8 +6903,8 @@ async function startMcpServer() {
|
|
|
5904
6903
|
"stop_thread",
|
|
5905
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.",
|
|
5906
6905
|
{
|
|
5907
|
-
ref:
|
|
5908
|
-
force:
|
|
6906
|
+
ref: z2.string(),
|
|
6907
|
+
force: z2.boolean().optional()
|
|
5909
6908
|
},
|
|
5910
6909
|
async ({ ref, force }) => {
|
|
5911
6910
|
const t = orch.getThread(ref);
|
|
@@ -5934,8 +6933,8 @@ async function startMcpServer() {
|
|
|
5934
6933
|
);
|
|
5935
6934
|
server.tool(
|
|
5936
6935
|
"archive_thread",
|
|
5937
|
-
"Archive a thread (stops agent/dev, runs archive script, removes worktree when last chat tab).
|
|
5938
|
-
{ 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() },
|
|
5939
6938
|
async ({ ref }) => {
|
|
5940
6939
|
const t = orch.getThread(ref);
|
|
5941
6940
|
if (!t) {
|
|
@@ -5968,7 +6967,7 @@ async function startMcpServer() {
|
|
|
5968
6967
|
server.tool(
|
|
5969
6968
|
"restore_thread",
|
|
5970
6969
|
"Restore an archived thread (recreates worktree from branch when needed)",
|
|
5971
|
-
{ ref:
|
|
6970
|
+
{ ref: z2.string() },
|
|
5972
6971
|
async ({ ref }) => {
|
|
5973
6972
|
try {
|
|
5974
6973
|
const restored = await orch.restore(ref);
|
|
@@ -5994,8 +6993,8 @@ async function startMcpServer() {
|
|
|
5994
6993
|
"get_diff",
|
|
5995
6994
|
"Compact diff summary (capped hunks, paginated)",
|
|
5996
6995
|
{
|
|
5997
|
-
ref:
|
|
5998
|
-
maxFiles:
|
|
6996
|
+
ref: z2.string(),
|
|
6997
|
+
maxFiles: z2.number().optional()
|
|
5999
6998
|
},
|
|
6000
6999
|
async ({ ref, maxFiles }) => {
|
|
6001
7000
|
const summary = await orch.diffSummary(ref);
|
|
@@ -6015,7 +7014,7 @@ async function startMcpServer() {
|
|
|
6015
7014
|
server.tool(
|
|
6016
7015
|
"request_review",
|
|
6017
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.',
|
|
6018
|
-
{ ref:
|
|
7017
|
+
{ ref: z2.string().describe("Worktree thread id/ref to review") },
|
|
6019
7018
|
async ({ ref }) => {
|
|
6020
7019
|
try {
|
|
6021
7020
|
const tab = await orch.requestReview(ref);
|
|
@@ -6040,7 +7039,7 @@ async function startMcpServer() {
|
|
|
6040
7039
|
}
|
|
6041
7040
|
}
|
|
6042
7041
|
);
|
|
6043
|
-
const agentEnum =
|
|
7042
|
+
const agentEnum = z2.enum(["claude", "codex", "opencode", "brightsy", "cursor"]);
|
|
6044
7043
|
server.tool(
|
|
6045
7044
|
"list_models",
|
|
6046
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.",
|
|
@@ -6063,11 +7062,11 @@ async function startMcpServer() {
|
|
|
6063
7062
|
"fork_worktree",
|
|
6064
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.",
|
|
6065
7064
|
{
|
|
6066
|
-
ref:
|
|
6067
|
-
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)"),
|
|
6068
7067
|
agent: agentEnum.optional().describe("Agent for the forked chat (default: same as source)"),
|
|
6069
|
-
model:
|
|
6070
|
-
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()
|
|
6071
7070
|
},
|
|
6072
7071
|
async ({ ref, through_index, agent, model, title }) => {
|
|
6073
7072
|
try {
|
|
@@ -6106,13 +7105,13 @@ async function startMcpServer() {
|
|
|
6106
7105
|
);
|
|
6107
7106
|
server.tool(
|
|
6108
7107
|
"fork_chat",
|
|
6109
|
-
"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.",
|
|
6110
7109
|
{
|
|
6111
|
-
ref:
|
|
6112
|
-
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)"),
|
|
6113
7112
|
agent: agentEnum.optional().describe("Agent for the forked chat (default: same as source)"),
|
|
6114
|
-
model:
|
|
6115
|
-
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()
|
|
6116
7115
|
},
|
|
6117
7116
|
async ({ ref, through_index, agent, model, title }) => {
|
|
6118
7117
|
try {
|
|
@@ -6158,8 +7157,8 @@ async function startMcpServer() {
|
|
|
6158
7157
|
"run_dev_script",
|
|
6159
7158
|
"Start a .sideboard/.conductor run script for a thread (default script if name omitted); returns port",
|
|
6160
7159
|
{
|
|
6161
|
-
ref:
|
|
6162
|
-
name:
|
|
7160
|
+
ref: z2.string(),
|
|
7161
|
+
name: z2.string().optional()
|
|
6163
7162
|
},
|
|
6164
7163
|
async ({ ref, name }) => {
|
|
6165
7164
|
const result = await orch.startDev(ref, name);
|
|
@@ -6181,7 +7180,7 @@ async function startMcpServer() {
|
|
|
6181
7180
|
server.tool(
|
|
6182
7181
|
"list_run_scripts",
|
|
6183
7182
|
"List named run scripts available for a thread",
|
|
6184
|
-
{ ref:
|
|
7183
|
+
{ ref: z2.string() },
|
|
6185
7184
|
async ({ ref }) => {
|
|
6186
7185
|
const scripts = orch.listThreadRunScripts(ref);
|
|
6187
7186
|
const active = orch.getActiveRuns(ref);
|
|
@@ -6199,8 +7198,8 @@ async function startMcpServer() {
|
|
|
6199
7198
|
"stop_dev_script",
|
|
6200
7199
|
"Stop a running script for a thread (all scripts if name omitted)",
|
|
6201
7200
|
{
|
|
6202
|
-
ref:
|
|
6203
|
-
name:
|
|
7201
|
+
ref: z2.string(),
|
|
7202
|
+
name: z2.string().optional()
|
|
6204
7203
|
},
|
|
6205
7204
|
async ({ ref, name }) => {
|
|
6206
7205
|
orch.stopDev(ref, name);
|
|
@@ -6210,7 +7209,7 @@ async function startMcpServer() {
|
|
|
6210
7209
|
server.tool(
|
|
6211
7210
|
"run_setup",
|
|
6212
7211
|
"Re-run workspace setup (Sideboard/Conductor settings or .cursor/worktrees.json)",
|
|
6213
|
-
{ ref:
|
|
7212
|
+
{ ref: z2.string() },
|
|
6214
7213
|
async ({ ref }) => {
|
|
6215
7214
|
const result = await orch.runSetup(ref);
|
|
6216
7215
|
return {
|
|
@@ -6221,7 +7220,7 @@ async function startMcpServer() {
|
|
|
6221
7220
|
server.tool(
|
|
6222
7221
|
"add_workspace",
|
|
6223
7222
|
"Register a git repo as a Sideboard workspace",
|
|
6224
|
-
{ repoPath:
|
|
7223
|
+
{ repoPath: z2.string() },
|
|
6225
7224
|
async ({ repoPath }) => {
|
|
6226
7225
|
const ws = await orch.addWorkspace(repoPath);
|
|
6227
7226
|
return { content: [{ type: "text", text: JSON.stringify(ws) }] };
|
|
@@ -6230,7 +7229,7 @@ async function startMcpServer() {
|
|
|
6230
7229
|
server.tool(
|
|
6231
7230
|
"remove_workspace",
|
|
6232
7231
|
"Unregister a Sideboard workspace (does not archive threads)",
|
|
6233
|
-
{ repoPath:
|
|
7232
|
+
{ repoPath: z2.string() },
|
|
6234
7233
|
async ({ repoPath }) => {
|
|
6235
7234
|
orch.removeWorkspace(repoPath);
|
|
6236
7235
|
return { content: [{ type: "text", text: "ok" }] };
|
|
@@ -6240,12 +7239,12 @@ async function startMcpServer() {
|
|
|
6240
7239
|
"fanout",
|
|
6241
7240
|
"Best-of-n: create one thread per agent with the same prompt (parallel attempts)",
|
|
6242
7241
|
{
|
|
6243
|
-
prompt:
|
|
6244
|
-
agents:
|
|
6245
|
-
repoPath:
|
|
6246
|
-
sourceType:
|
|
6247
|
-
sourceRef:
|
|
6248
|
-
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()
|
|
6249
7248
|
},
|
|
6250
7249
|
async (args) => {
|
|
6251
7250
|
const threads = await orch.bestOfN(args);
|
|
@@ -6272,8 +7271,8 @@ async function startMcpServer() {
|
|
|
6272
7271
|
"list_branches",
|
|
6273
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).",
|
|
6274
7273
|
{
|
|
6275
|
-
repoPath:
|
|
6276
|
-
unmergedOnly:
|
|
7274
|
+
repoPath: z2.string(),
|
|
7275
|
+
unmergedOnly: z2.boolean().optional()
|
|
6277
7276
|
},
|
|
6278
7277
|
async ({ repoPath, unmergedOnly }) => {
|
|
6279
7278
|
const root = await resolveRepoRoot(repoPath);
|
|
@@ -6293,7 +7292,7 @@ async function startMcpServer() {
|
|
|
6293
7292
|
server.tool(
|
|
6294
7293
|
"list_prs",
|
|
6295
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.",
|
|
6296
|
-
{ repoPath:
|
|
7295
|
+
{ repoPath: z2.string() },
|
|
6297
7296
|
async ({ repoPath }) => {
|
|
6298
7297
|
const root = await resolveRepoRoot(repoPath);
|
|
6299
7298
|
const prs = await listPrs(root);
|
|
@@ -6312,7 +7311,7 @@ async function startMcpServer() {
|
|
|
6312
7311
|
server.tool(
|
|
6313
7312
|
"get_pr_stack",
|
|
6314
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.",
|
|
6315
|
-
{ ref:
|
|
7314
|
+
{ ref: z2.string() },
|
|
6316
7315
|
async ({ ref }) => {
|
|
6317
7316
|
const stack = await orch.getPrStack(ref);
|
|
6318
7317
|
return {
|
|
@@ -6324,8 +7323,8 @@ async function startMcpServer() {
|
|
|
6324
7323
|
"open_pr_stack_layers",
|
|
6325
7324
|
"Materialize one worktree+thread per stack layer (or a single 1-based layer). Pass a thread ref already on the stack.",
|
|
6326
7325
|
{
|
|
6327
|
-
ref:
|
|
6328
|
-
layer:
|
|
7326
|
+
ref: z2.string(),
|
|
7327
|
+
layer: z2.number().int().positive().optional()
|
|
6329
7328
|
},
|
|
6330
7329
|
async ({ ref, layer }) => {
|
|
6331
7330
|
const result = await orch.openPrStackLayers(ref, { layer });
|
|
@@ -6359,9 +7358,9 @@ async function startMcpServer() {
|
|
|
6359
7358
|
"add_stack_layer",
|
|
6360
7359
|
"Add a branch on top of the current stack (`gh stack add`) and open a worktree+thread for it.",
|
|
6361
7360
|
{
|
|
6362
|
-
ref:
|
|
6363
|
-
branchName:
|
|
6364
|
-
title:
|
|
7361
|
+
ref: z2.string(),
|
|
7362
|
+
branchName: z2.string(),
|
|
7363
|
+
title: z2.string().optional()
|
|
6365
7364
|
},
|
|
6366
7365
|
async ({ ref, branchName, title }) => {
|
|
6367
7366
|
const result = await orch.addStackLayer(ref, branchName, { title });
|
|
@@ -6390,11 +7389,11 @@ async function startMcpServer() {
|
|
|
6390
7389
|
"create_pr_stack",
|
|
6391
7390
|
"Create a new GitHub PR stack with one Sideboard worktree per layer (bottom\u2192top branch names). Requires `gh extension install github/gh-stack`.",
|
|
6392
7391
|
{
|
|
6393
|
-
repoPath:
|
|
6394
|
-
branches:
|
|
6395
|
-
agent:
|
|
6396
|
-
base:
|
|
6397
|
-
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()
|
|
6398
7397
|
},
|
|
6399
7398
|
async (args) => {
|
|
6400
7399
|
const result = await orch.createPrStack({
|
|
@@ -6432,7 +7431,7 @@ async function startMcpServer() {
|
|
|
6432
7431
|
server.tool(
|
|
6433
7432
|
"list_issues",
|
|
6434
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.",
|
|
6435
|
-
{ repoPath:
|
|
7434
|
+
{ repoPath: z2.string() },
|
|
6436
7435
|
async ({ repoPath }) => {
|
|
6437
7436
|
const root = await resolveRepoRoot(repoPath);
|
|
6438
7437
|
const result = await listIssues(root);
|
|
@@ -6443,8 +7442,8 @@ async function startMcpServer() {
|
|
|
6443
7442
|
"list_linear_issues",
|
|
6444
7443
|
"Deprecated: prefer list_issues. Lists assigned Linear issues via the chosen agent MCP connector",
|
|
6445
7444
|
{
|
|
6446
|
-
agent:
|
|
6447
|
-
repoPath:
|
|
7445
|
+
agent: z2.enum(["claude", "codex", "opencode"]),
|
|
7446
|
+
repoPath: z2.string()
|
|
6448
7447
|
},
|
|
6449
7448
|
async ({ agent, repoPath }) => {
|
|
6450
7449
|
const issues = await listLinearIssues(agent, repoPath);
|