@sideboard-ai/core 0.1.279 → 0.1.281
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/{abletime-VCVPSLSU.js → abletime-WLDBJJUI.js} +3 -3
- package/dist/{abletime-VRATUMDO.js → abletime-X64E3GUJ.js} +3 -3
- package/dist/{abletime-oauth-4K7MICAD.js → abletime-oauth-AXR676W5.js} +2 -2
- package/dist/{abletime-oauth-4C4G2J3R.js → abletime-oauth-UWOCEICP.js} +2 -2
- package/dist/{abletime-rest-PIBCNHF3.js → abletime-rest-LPUDJUEP.js} +2 -2
- package/dist/{abletime-rest-6N63ZVQ3.js → abletime-rest-S4XWEXV2.js} +2 -2
- package/dist/{agents-DYCFLDNL.js → agents-2IE4HKHA.js} +5 -5
- package/dist/{agents-RXP5NZBC.js → agents-XA2E64AW.js} +5 -5
- package/dist/{chunk-V5DMHB6Y.js → chunk-2KNO2GJQ.js} +781 -159
- package/dist/{chunk-TZ5JHIYK.js → chunk-2SJXQ4UX.js} +68 -21
- package/dist/{chunk-K7MWJLR7.js → chunk-5MGLNVNK.js} +6 -906
- package/dist/{chunk-RK5PIWCW.js → chunk-ADXVTKLC.js} +12 -4
- package/dist/{chunk-Y7JPL4WA.js → chunk-BDAUPNHL.js} +3 -4
- package/dist/{chunk-R7HLXD4I.js → chunk-BS3QLVPL.js} +93 -57
- package/dist/{chunk-CPGJ5ICZ.js → chunk-BYZ7F2BQ.js} +766 -148
- package/dist/{chunk-2J34G3WJ.js → chunk-DGPROCE5.js} +93 -57
- package/dist/{chunk-HQXBBUQX.js → chunk-DQZPF6WL.js} +12 -11
- package/dist/{chunk-YDXNCBA2.js → chunk-EIEAR62M.js} +12 -11
- package/dist/{chunk-XKZAVGTT.js → chunk-I6HSGKMZ.js} +1 -1
- package/dist/chunk-JPH7PYSK.js +1241 -0
- package/dist/{chunk-IPPY3FG5.js → chunk-K5XSTHHP.js} +6 -906
- package/dist/chunk-N5UGX3JL.js +1239 -0
- package/dist/{chunk-HNNS25SG.js → chunk-OCUIZD4Y.js} +14 -4
- package/dist/{chunk-IJDQKC5A.js → chunk-PY4UCMRI.js} +4 -4
- package/dist/{chunk-NQ7JMVSU.js → chunk-U7HAVAGJ.js} +1 -1
- package/dist/{chunk-PZURG3XN.js → chunk-VA55JG2I.js} +4 -4
- package/dist/{chunk-CPA43FBK.js → chunk-W7HIK2A6.js} +3 -4
- package/dist/{chunk-SKSOCIJT.js → chunk-WY2IAB7J.js} +68 -21
- package/dist/{chunk-QINJKNLR.js → chunk-XTPQEZZA.js} +2 -2
- package/dist/{conductor-J6WDOXWM.js → conductor-CNSCOC54.js} +5 -5
- package/dist/{conductor-OW6NAK3N.js → conductor-U776JSXQ.js} +5 -5
- package/dist/{coordinator-prompt-Z2D32HWK.js → coordinator-prompt-4DJIHSE6.js} +3 -3
- package/dist/{coordinator-prompt-ERTHREGJ.js → coordinator-prompt-SVVGU6AZ.js} +3 -3
- package/dist/{global-workspace-5PJXO45Z.js → global-workspace-UPUDWIS6.js} +3 -3
- package/dist/{global-workspace-AK5PSPQ2.js → global-workspace-XON4YZXE.js} +3 -3
- package/dist/index.cjs +3819 -2995
- package/dist/index.d.cts +103 -30
- package/dist/index.d.ts +103 -30
- package/dist/index.js +1013 -1001
- package/dist/mcp/run-stdio.cjs +2075 -1313
- package/dist/mcp/run-stdio.js +743 -785
- package/dist/{orchestrator-KY2KBBE2.js → orchestrator-FPGVKEID.js} +7 -7
- package/dist/{orchestrator-E5WK57DA.js → orchestrator-HWJHK5NL.js} +7 -7
- package/dist/{thread-store-B3ZM4QEZ.js → thread-store-7XBOUQ5K.js} +1 -1
- package/dist/{thread-store-SHCUBWB3.js → thread-store-ZQZDREVH.js} +1 -1
- package/dist/{workspaces-UP6DQ3WL.js → workspaces-7AJ5RZ3P.js} +7 -3
- package/dist/{workspaces-WU2RNLDV.js → workspaces-F76BWCSI.js} +7 -3
- package/dist/{worktree-L57T3KH7.js → worktree-NKLNVNXX.js} +20 -15
- package/dist/{worktree-Q6UJHG25.js → worktree-YTZC3UQN.js} +14 -9
- package/package.json +1 -1
- package/dist/chunk-FWNL62VA.js +0 -308
- package/dist/chunk-JANCPF7Z.js +0 -310
package/dist/mcp/run-stdio.js
CHANGED
|
@@ -18,9 +18,9 @@ import {
|
|
|
18
18
|
searchAbleTimeTasks,
|
|
19
19
|
toAbleTimeIssueInfo,
|
|
20
20
|
updateAbleTimeTask
|
|
21
|
-
} from "../chunk-
|
|
22
|
-
import "../chunk-
|
|
23
|
-
import "../chunk-
|
|
21
|
+
} from "../chunk-BS3QLVPL.js";
|
|
22
|
+
import "../chunk-I6HSGKMZ.js";
|
|
23
|
+
import "../chunk-PY4UCMRI.js";
|
|
24
24
|
import {
|
|
25
25
|
AGENT_GIT_ACTIONS,
|
|
26
26
|
BOARD_COLUMN_DEFS,
|
|
@@ -29,8 +29,11 @@ import {
|
|
|
29
29
|
HOME_BOARD_CACHE_TTL_MS,
|
|
30
30
|
MAX_RUN_LOG_CHARS,
|
|
31
31
|
agentBlockFields,
|
|
32
|
+
applyIssueListWindow,
|
|
33
|
+
applyWorkspaceTags,
|
|
32
34
|
assembleHomeBoard,
|
|
33
35
|
chatBlockedSuffix,
|
|
36
|
+
clampMcpIssueLimit,
|
|
34
37
|
createSchedule,
|
|
35
38
|
dedupeBoardIssues,
|
|
36
39
|
dedupeBoardPrs,
|
|
@@ -40,15 +43,23 @@ import {
|
|
|
40
43
|
findBoardPr,
|
|
41
44
|
findLiveThreadForCreate,
|
|
42
45
|
fireSchedule,
|
|
46
|
+
formatGitHubSearchUpdatedSince,
|
|
47
|
+
formatListedIssuesForMcp,
|
|
43
48
|
getOrchestrator,
|
|
44
49
|
getSchedule,
|
|
45
50
|
isAccountWideIssueSource,
|
|
46
51
|
isInjectedNoticeText,
|
|
52
|
+
issueMatchesUpdatedSince,
|
|
47
53
|
issueNeedsWorkspacePick,
|
|
48
54
|
listSchedules,
|
|
49
55
|
localhostPreviewUrl,
|
|
56
|
+
mcpIssueUpdatedSinceSchema,
|
|
57
|
+
mcpJson,
|
|
50
58
|
notifyOrchestrator,
|
|
59
|
+
parseIssueSince,
|
|
60
|
+
previewIssueCommentBody,
|
|
51
61
|
readTurnLive,
|
|
62
|
+
registerViewerContextTools,
|
|
52
63
|
resolveNotifyCallerThread,
|
|
53
64
|
resolveOrchChildFollowUp,
|
|
54
65
|
resolveScheduleThreadId,
|
|
@@ -58,8 +69,9 @@ import {
|
|
|
58
69
|
stampNotifyBlock,
|
|
59
70
|
stampPlanBlock,
|
|
60
71
|
syncBoardPins,
|
|
61
|
-
updateSchedule
|
|
62
|
-
|
|
72
|
+
updateSchedule,
|
|
73
|
+
workspaceTagTargetRef
|
|
74
|
+
} from "../chunk-BYZ7F2BQ.js";
|
|
63
75
|
import "../chunk-LX5R5JOR.js";
|
|
64
76
|
import {
|
|
65
77
|
PRESENT_PLAN_REQUIRES_PLAN_MODE,
|
|
@@ -78,7 +90,7 @@ import {
|
|
|
78
90
|
sideboardMcpProfile,
|
|
79
91
|
trustSystemCertificates,
|
|
80
92
|
worktreeMcpToolNames
|
|
81
|
-
} from "../chunk-
|
|
93
|
+
} from "../chunk-ADXVTKLC.js";
|
|
82
94
|
import "../chunk-TQRSVE7D.js";
|
|
83
95
|
import "../chunk-P2BSMNHB.js";
|
|
84
96
|
import {
|
|
@@ -94,17 +106,16 @@ import {
|
|
|
94
106
|
waitForDetachedJob,
|
|
95
107
|
waitForTurnToolResult,
|
|
96
108
|
workspaceAgentChatSummaries
|
|
97
|
-
} from "../chunk-
|
|
109
|
+
} from "../chunk-EIEAR62M.js";
|
|
98
110
|
import {
|
|
99
111
|
clipToolResultForStore,
|
|
100
112
|
isInternalAgentStatusText,
|
|
101
113
|
looksLikeHugeToolResultDump
|
|
102
|
-
} from "../chunk-
|
|
114
|
+
} from "../chunk-W7HIK2A6.js";
|
|
103
115
|
import {
|
|
104
116
|
GLOBAL_WORKSPACE_ID,
|
|
105
|
-
isCloudCoordinatorThread
|
|
106
|
-
|
|
107
|
-
} from "../chunk-TZ5JHIYK.js";
|
|
117
|
+
isCloudCoordinatorThread
|
|
118
|
+
} from "../chunk-2SJXQ4UX.js";
|
|
108
119
|
import {
|
|
109
120
|
canonicalizeRepoPath,
|
|
110
121
|
formatGhLandError,
|
|
@@ -115,7 +126,7 @@ import {
|
|
|
115
126
|
resolveListPrsOptions,
|
|
116
127
|
resolveRepoRoot,
|
|
117
128
|
warmGithubAgentAuth
|
|
118
|
-
} from "../chunk-
|
|
129
|
+
} from "../chunk-5MGLNVNK.js";
|
|
119
130
|
import {
|
|
120
131
|
CLI_DIR
|
|
121
132
|
} from "../chunk-N2WW62LF.js";
|
|
@@ -131,13 +142,11 @@ import {
|
|
|
131
142
|
isAbleTimeConnected,
|
|
132
143
|
isLinearConnected,
|
|
133
144
|
loadAppSettings,
|
|
134
|
-
readViewerContext,
|
|
135
145
|
resolveEffectiveIssueSource,
|
|
136
146
|
resolveProjectReviewLabel,
|
|
137
147
|
resolveViewerProfile,
|
|
138
148
|
resolveViewerProfileForRepo,
|
|
139
|
-
saveLinearOAuth
|
|
140
|
-
writeViewerContext
|
|
149
|
+
saveLinearOAuth
|
|
141
150
|
} from "../chunk-WRIMAEF2.js";
|
|
142
151
|
import "../chunk-NSTQ6QKD.js";
|
|
143
152
|
import {
|
|
@@ -145,7 +154,9 @@ import {
|
|
|
145
154
|
} from "../chunk-WWUABK6B.js";
|
|
146
155
|
import "../chunk-FJZWM2ON.js";
|
|
147
156
|
import "../chunk-UN5Q7POQ.js";
|
|
148
|
-
import
|
|
157
|
+
import {
|
|
158
|
+
normalizeWorkspaceTags
|
|
159
|
+
} from "../chunk-JPH7PYSK.js";
|
|
149
160
|
import "../chunk-4RZRHTSZ.js";
|
|
150
161
|
import "../chunk-KGZBYWZ3.js";
|
|
151
162
|
import {
|
|
@@ -240,7 +251,7 @@ function createClippedMcpServer() {
|
|
|
240
251
|
}
|
|
241
252
|
|
|
242
253
|
// src/mcp/server.ts
|
|
243
|
-
import { z as
|
|
254
|
+
import { z as z6 } from "zod";
|
|
244
255
|
import { randomUUID as randomUUID3 } from "crypto";
|
|
245
256
|
import { basename } from "path";
|
|
246
257
|
|
|
@@ -264,7 +275,7 @@ function extractGitHubIssueAttachments(issue) {
|
|
|
264
275
|
const texts = [issue.body ?? "", ...issue.comments.map((comment) => comment.body)];
|
|
265
276
|
return mergeIssueAttachments(
|
|
266
277
|
texts.flatMap(
|
|
267
|
-
(
|
|
278
|
+
(text5) => extractHttpUrls(text5).filter(isGitHubIssueAttachmentUrl).map((url) => ({
|
|
268
279
|
id: url,
|
|
269
280
|
name: url.split("/").pop() || "attachment",
|
|
270
281
|
url
|
|
@@ -293,74 +304,127 @@ async function downloadGitHubIssueAttachment(input, opts) {
|
|
|
293
304
|
});
|
|
294
305
|
}
|
|
295
306
|
|
|
296
|
-
// src/integrations/
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
return
|
|
303
|
-
}
|
|
304
|
-
function
|
|
305
|
-
return
|
|
306
|
-
}
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
const
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
307
|
+
// src/integrations/github-projects.ts
|
|
308
|
+
function rewriteGitHubProjectReadError(detail) {
|
|
309
|
+
const text5 = detail.trim() || "gh could not read GitHub Projects";
|
|
310
|
+
if (/read:project|project scope|missing required scopes/i.test(text5)) {
|
|
311
|
+
return `${text5} \u2014 run \`gh auth refresh -s read:project\` so Sideboard can read GitHub Projects.`;
|
|
312
|
+
}
|
|
313
|
+
return text5;
|
|
314
|
+
}
|
|
315
|
+
function githubProjectsUnreadable(result) {
|
|
316
|
+
return /project|scope|unknown json field/i.test(`${result.stderr}
|
|
317
|
+
${result.stdout}`);
|
|
318
|
+
}
|
|
319
|
+
function githubProjectsOnIssue(raw) {
|
|
320
|
+
const out = [];
|
|
321
|
+
const seen = /* @__PURE__ */ new Set();
|
|
322
|
+
const fromItems = raw.projectItems ?? raw.projects ?? raw.projectItemsV2;
|
|
323
|
+
if (!Array.isArray(fromItems)) return out;
|
|
324
|
+
for (const item of fromItems) {
|
|
325
|
+
if (typeof item === "string") {
|
|
326
|
+
const name2 = item.trim();
|
|
327
|
+
const key2 = name2.toLowerCase();
|
|
328
|
+
if (!name2 || seen.has(key2)) continue;
|
|
329
|
+
seen.add(key2);
|
|
330
|
+
out.push({ name: name2 });
|
|
331
|
+
continue;
|
|
332
|
+
}
|
|
333
|
+
if (!item || typeof item !== "object") continue;
|
|
334
|
+
const rec = item;
|
|
335
|
+
const nested = rec.project && typeof rec.project === "object" ? rec.project : rec;
|
|
336
|
+
const id = String(nested.id ?? rec.id ?? "").trim();
|
|
337
|
+
const name = String(nested.title ?? nested.name ?? rec.title ?? rec.name ?? "").trim();
|
|
338
|
+
const label = name || id;
|
|
339
|
+
const key = label.toLowerCase();
|
|
340
|
+
if (!label || seen.has(key)) continue;
|
|
341
|
+
seen.add(key);
|
|
342
|
+
out.push({ ...id ? { id } : {}, name: label });
|
|
343
|
+
}
|
|
344
|
+
return out;
|
|
345
|
+
}
|
|
346
|
+
async function readGitHubIssueProjects(number, cwd, repoArgs) {
|
|
347
|
+
const result = await gh(
|
|
348
|
+
["issue", "view", String(number), ...repoArgs, "--json", "projectItems"],
|
|
349
|
+
cwd,
|
|
350
|
+
{ reject: false }
|
|
351
|
+
);
|
|
352
|
+
if (result.exitCode !== 0 || !result.stdout.trim()) {
|
|
353
|
+
const detail = (result.stderr || result.stdout).trim();
|
|
354
|
+
if (detail && githubProjectsUnreadable(result)) {
|
|
355
|
+
return { projects: [], error: rewriteGitHubProjectReadError(detail) };
|
|
356
|
+
}
|
|
357
|
+
return { projects: [] };
|
|
358
|
+
}
|
|
359
|
+
try {
|
|
360
|
+
const parsed = JSON.parse(result.stdout);
|
|
361
|
+
return { projects: githubProjectsOnIssue(parsed) };
|
|
362
|
+
} catch {
|
|
363
|
+
return { projects: [] };
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
async function listGitHubIssueProjectNames(number, cwd, repoArgs) {
|
|
367
|
+
const read = await readGitHubIssueProjects(number, cwd, repoArgs);
|
|
368
|
+
return read.projects.map((project) => project.name).filter(Boolean);
|
|
369
|
+
}
|
|
370
|
+
function parseGitHubProjectList(stdout) {
|
|
371
|
+
let parsed;
|
|
372
|
+
try {
|
|
373
|
+
parsed = JSON.parse(stdout);
|
|
374
|
+
} catch {
|
|
375
|
+
return [];
|
|
376
|
+
}
|
|
377
|
+
const rows = Array.isArray(parsed) ? parsed : parsed && typeof parsed === "object" && Array.isArray(parsed.projects) ? parsed.projects ?? [] : [];
|
|
378
|
+
const out = [];
|
|
379
|
+
for (const row of rows) {
|
|
380
|
+
if (!row || typeof row !== "object") continue;
|
|
381
|
+
const rec = row;
|
|
382
|
+
const title = String(rec.title ?? rec.name ?? "").trim();
|
|
383
|
+
if (!title) continue;
|
|
384
|
+
const ownerRec = rec.owner && typeof rec.owner === "object" ? rec.owner : null;
|
|
385
|
+
const owner = ownerRec?.login?.trim() || (typeof rec.owner === "string" ? rec.owner.trim() : "");
|
|
386
|
+
const number = Number(rec.number);
|
|
387
|
+
out.push({
|
|
388
|
+
...typeof rec.id === "string" && rec.id.trim() ? { id: rec.id.trim() } : {},
|
|
389
|
+
...Number.isFinite(number) ? { number } : {},
|
|
390
|
+
title,
|
|
391
|
+
...typeof rec.url === "string" && rec.url.trim() ? { url: rec.url.trim() } : {},
|
|
392
|
+
...typeof rec.closed === "boolean" ? { closed: rec.closed } : {},
|
|
393
|
+
...owner ? { owner } : {}
|
|
394
|
+
});
|
|
395
|
+
}
|
|
396
|
+
return out;
|
|
397
|
+
}
|
|
398
|
+
async function listGitHubProjects(opts) {
|
|
399
|
+
const cwd = await resolveRepoRoot((opts?.repoPath ?? "").trim() || process.cwd());
|
|
400
|
+
const slug = await resolveGithubRepoSlug(cwd);
|
|
401
|
+
const owners = /* @__PURE__ */ new Set([""]);
|
|
402
|
+
const repoOwner = slug?.split("/")[0]?.trim();
|
|
403
|
+
if (repoOwner) owners.add(repoOwner);
|
|
404
|
+
const out = [];
|
|
405
|
+
const seen = /* @__PURE__ */ new Set();
|
|
406
|
+
let lastError = "";
|
|
407
|
+
let anyOk = false;
|
|
408
|
+
for (const owner of owners) {
|
|
409
|
+
const args = ["project", "list", "--limit", "100", "--format", "json"];
|
|
410
|
+
if (owner) args.push("--owner", owner);
|
|
411
|
+
const result = await gh(args, cwd, { reject: false });
|
|
412
|
+
if (result.exitCode !== 0 || !result.stdout.trim()) {
|
|
413
|
+
lastError = (result.stderr || result.stdout).trim() || lastError;
|
|
414
|
+
continue;
|
|
318
415
|
}
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
if (
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
}
|
|
332
|
-
const relative = lower.match(RELATIVE_RE);
|
|
333
|
-
if (relative) {
|
|
334
|
-
const n = Number(relative[1]);
|
|
335
|
-
if (!Number.isInteger(n) || n < 1) throw new Error(ISSUE_SINCE_ERROR);
|
|
336
|
-
const unit = relative[2];
|
|
337
|
-
if (unit.startsWith("h")) return new Date(now.getTime() - n * 36e5).toISOString();
|
|
338
|
-
if (unit.startsWith("w")) return addLocalDays(startOfLocalDay(now), -7 * n).toISOString();
|
|
339
|
-
return addLocalDays(startOfLocalDay(now), -n).toISOString();
|
|
340
|
-
}
|
|
341
|
-
throw new Error(ISSUE_SINCE_ERROR);
|
|
342
|
-
}
|
|
343
|
-
function formatGitHubSearchUpdatedSince(iso) {
|
|
344
|
-
return iso.replace(/\.\d{3}Z$/, "Z");
|
|
345
|
-
}
|
|
346
|
-
function issueMatchesUpdatedSince(issue, sinceIso) {
|
|
347
|
-
const ts = issue.updatedAt || issue.createdAt;
|
|
348
|
-
if (!ts) return false;
|
|
349
|
-
const value = Date.parse(ts);
|
|
350
|
-
const since = Date.parse(sinceIso);
|
|
351
|
-
return Number.isFinite(value) && Number.isFinite(since) && value >= since;
|
|
352
|
-
}
|
|
353
|
-
var ISSUE_COMMENT_PREVIEW_CHARS = 240;
|
|
354
|
-
function previewIssueCommentBody(body) {
|
|
355
|
-
const flat = body.replace(/\s+/g, " ").trim();
|
|
356
|
-
if (flat.length <= ISSUE_COMMENT_PREVIEW_CHARS) return flat;
|
|
357
|
-
return `${flat.slice(0, ISSUE_COMMENT_PREVIEW_CHARS - 1).trimEnd()}\u2026`;
|
|
358
|
-
}
|
|
359
|
-
function issueActivityKind(issue, sinceIso) {
|
|
360
|
-
const created = issue.createdAt ? Date.parse(issue.createdAt) : Number.NaN;
|
|
361
|
-
const since = Date.parse(sinceIso);
|
|
362
|
-
if (Number.isFinite(created) && Number.isFinite(since) && created >= since) return "created";
|
|
363
|
-
return "updated";
|
|
416
|
+
anyOk = true;
|
|
417
|
+
for (const project of parseGitHubProjectList(result.stdout)) {
|
|
418
|
+
const key = (project.id || `${project.owner ?? ""}:${project.number ?? ""}:${project.title}`).toLowerCase();
|
|
419
|
+
if (seen.has(key)) continue;
|
|
420
|
+
seen.add(key);
|
|
421
|
+
out.push(project);
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
if (!anyOk) {
|
|
425
|
+
throw new Error(rewriteGitHubProjectReadError(lastError || "gh project list failed"));
|
|
426
|
+
}
|
|
427
|
+
return out;
|
|
364
428
|
}
|
|
365
429
|
|
|
366
430
|
// src/integrations/github-issues.ts
|
|
@@ -529,7 +593,11 @@ async function getGitHubIssue(id, opts) {
|
|
|
529
593
|
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
530
594
|
throw new Error(`GitHub issue not found: #${number}`);
|
|
531
595
|
}
|
|
532
|
-
|
|
596
|
+
const issue = toGitHubIssue(parsed);
|
|
597
|
+
const projects = await readGitHubIssueProjects(number, cwd, repoArgs);
|
|
598
|
+
issue.projects = projects.projects;
|
|
599
|
+
if (projects.error) issue.projectsError = projects.error;
|
|
600
|
+
return issue;
|
|
533
601
|
}
|
|
534
602
|
async function commentGitHubIssue(input, opts) {
|
|
535
603
|
const number = parseGitHubIssueNumber(input.id);
|
|
@@ -567,46 +635,6 @@ function githubRelationEditFlag(type, remove = false) {
|
|
|
567
635
|
function labelsEqual(a, b) {
|
|
568
636
|
return a.trim().toLowerCase() === b.trim().toLowerCase();
|
|
569
637
|
}
|
|
570
|
-
function parseProjectTitles(raw) {
|
|
571
|
-
const out = [];
|
|
572
|
-
const seen = /* @__PURE__ */ new Set();
|
|
573
|
-
const push = (name) => {
|
|
574
|
-
const title = name.trim();
|
|
575
|
-
const key = title.toLowerCase();
|
|
576
|
-
if (!title || seen.has(key)) return;
|
|
577
|
-
seen.add(key);
|
|
578
|
-
out.push(title);
|
|
579
|
-
};
|
|
580
|
-
const fromItems = raw.projectItems ?? raw.projects ?? raw.projectItemsV2;
|
|
581
|
-
if (Array.isArray(fromItems)) {
|
|
582
|
-
for (const item of fromItems) {
|
|
583
|
-
if (typeof item === "string") {
|
|
584
|
-
push(item);
|
|
585
|
-
continue;
|
|
586
|
-
}
|
|
587
|
-
if (!item || typeof item !== "object") continue;
|
|
588
|
-
const rec = item;
|
|
589
|
-
const nested = rec.project && typeof rec.project === "object" ? rec.project : rec;
|
|
590
|
-
const title = String(nested.title ?? nested.name ?? rec.title ?? rec.name ?? "").trim();
|
|
591
|
-
if (title) push(title);
|
|
592
|
-
}
|
|
593
|
-
}
|
|
594
|
-
return out;
|
|
595
|
-
}
|
|
596
|
-
async function listGitHubIssueProjects(number, cwd, repoArgs) {
|
|
597
|
-
const result = await gh(
|
|
598
|
-
["issue", "view", String(number), ...repoArgs, "--json", "projectItems"],
|
|
599
|
-
cwd,
|
|
600
|
-
{ reject: false }
|
|
601
|
-
);
|
|
602
|
-
if (result.exitCode !== 0 || !result.stdout.trim()) return [];
|
|
603
|
-
try {
|
|
604
|
-
const parsed = JSON.parse(result.stdout);
|
|
605
|
-
return parseProjectTitles(parsed);
|
|
606
|
-
} catch {
|
|
607
|
-
return [];
|
|
608
|
-
}
|
|
609
|
-
}
|
|
610
638
|
async function updateGitHubIssue(input, opts) {
|
|
611
639
|
const number = parseGitHubIssueNumber(input.id);
|
|
612
640
|
const title = input.title?.trim();
|
|
@@ -646,7 +674,7 @@ async function updateGitHubIssue(input, opts) {
|
|
|
646
674
|
if (remove.length) args.push("--remove-label", remove.join(","));
|
|
647
675
|
}
|
|
648
676
|
if (input.project !== void 0) {
|
|
649
|
-
const current = await
|
|
677
|
+
const current = await listGitHubIssueProjectNames(number, cwd, repoArgs);
|
|
650
678
|
if (isGitHubNoneToken(input.project)) {
|
|
651
679
|
for (const name of current) args.push("--remove-project", name);
|
|
652
680
|
} else {
|
|
@@ -945,6 +973,7 @@ var LIST_ISSUE_FIELDS = `
|
|
|
945
973
|
team { id key }
|
|
946
974
|
labels(first: 10) { nodes { name } }
|
|
947
975
|
cycle { id name number startsAt endsAt completedAt }
|
|
976
|
+
project { id name }
|
|
948
977
|
`;
|
|
949
978
|
var ISSUE_REF_FIELDS = `
|
|
950
979
|
id
|
|
@@ -1121,9 +1150,10 @@ query SideboardTeamLabels($id: String!, $first: Int!, $after: String) {
|
|
|
1121
1150
|
}
|
|
1122
1151
|
`;
|
|
1123
1152
|
var PROJECTS_QUERY = `
|
|
1124
|
-
query SideboardProjects($first: Int!, $filter: ProjectFilter) {
|
|
1125
|
-
projects(first: $first, filter: $filter) {
|
|
1153
|
+
query SideboardProjects($first: Int!, $filter: ProjectFilter, $after: String) {
|
|
1154
|
+
projects(first: $first, filter: $filter, after: $after) {
|
|
1126
1155
|
nodes { id name slugId }
|
|
1156
|
+
pageInfo { hasNextPage endCursor }
|
|
1127
1157
|
}
|
|
1128
1158
|
}
|
|
1129
1159
|
`;
|
|
@@ -1345,6 +1375,7 @@ function toIssueInfo(issue) {
|
|
|
1345
1375
|
assignees: issue.assignee?.name ? [issue.assignee.name] : void 0,
|
|
1346
1376
|
cycle: issue.cycle ?? null,
|
|
1347
1377
|
teamKey: issue.team?.key || void 0,
|
|
1378
|
+
...issue.project ? { projects: [issue.project] } : {},
|
|
1348
1379
|
...issue.createdAt ? { createdAt: issue.createdAt } : {},
|
|
1349
1380
|
...issue.updatedAt ? { updatedAt: issue.updatedAt } : {}
|
|
1350
1381
|
};
|
|
@@ -1510,30 +1541,35 @@ async function listLinearLabels(opts) {
|
|
|
1510
1541
|
}
|
|
1511
1542
|
return listLinearWorkspaceLabels(opts);
|
|
1512
1543
|
}
|
|
1513
|
-
async function
|
|
1514
|
-
const
|
|
1515
|
-
|
|
1516
|
-
|
|
1517
|
-
|
|
1518
|
-
|
|
1519
|
-
|
|
1520
|
-
|
|
1521
|
-
|
|
1522
|
-
|
|
1523
|
-
|
|
1524
|
-
|
|
1525
|
-
|
|
1544
|
+
async function queryLinearProjects(filter, opts) {
|
|
1545
|
+
const out = [];
|
|
1546
|
+
let after;
|
|
1547
|
+
for (let page = 0; page < 4; page++) {
|
|
1548
|
+
const json = await linearGraphql(PROJECTS_QUERY, { first: 50, filter: filter ?? {}, ...after ? { after } : {} }, opts);
|
|
1549
|
+
for (const node of json.projects?.nodes ?? []) {
|
|
1550
|
+
const id = String(node.id ?? "").trim();
|
|
1551
|
+
if (!id) continue;
|
|
1552
|
+
out.push({ id, name: String(node.name ?? ""), ...node.slugId ? { slugId: String(node.slugId) } : {} });
|
|
1553
|
+
}
|
|
1554
|
+
const pageInfo = json.projects?.pageInfo;
|
|
1555
|
+
if (!pageInfo?.hasNextPage || !pageInfo.endCursor) break;
|
|
1556
|
+
after = pageInfo.endCursor;
|
|
1557
|
+
}
|
|
1558
|
+
return out;
|
|
1559
|
+
}
|
|
1560
|
+
function listLinearProjects(opts) {
|
|
1561
|
+
return queryLinearProjects(void 0, opts);
|
|
1526
1562
|
}
|
|
1527
1563
|
async function resolveLinearProjectId(project, opts) {
|
|
1528
1564
|
if (isLinearNoneToken(project)) return null;
|
|
1529
1565
|
const s = (project ?? "").trim();
|
|
1530
1566
|
const filter = UUID_RE.test(s) ? { id: { eq: s } } : { name: { eqIgnoreCase: s } };
|
|
1531
|
-
const matched = await
|
|
1567
|
+
const matched = await queryLinearProjects(filter, opts);
|
|
1532
1568
|
try {
|
|
1533
1569
|
return resolveLinearProject(matched, s);
|
|
1534
1570
|
} catch {
|
|
1535
1571
|
if (UUID_RE.test(s)) return s;
|
|
1536
|
-
const listed = await
|
|
1572
|
+
const listed = await queryLinearProjects(void 0, opts);
|
|
1537
1573
|
return resolveLinearProject(listed, s);
|
|
1538
1574
|
}
|
|
1539
1575
|
}
|
|
@@ -1915,32 +1951,43 @@ function issueMatchesAssignee(issue, assignee, viewerName = "") {
|
|
|
1915
1951
|
async function listGitHubIssues(repoPath, opts) {
|
|
1916
1952
|
const limit = Math.max(1, Math.min(1e3, opts?.limit ?? 200));
|
|
1917
1953
|
const slug = await resolveGithubRepoSlug(repoPath);
|
|
1918
|
-
const
|
|
1919
|
-
|
|
1920
|
-
|
|
1921
|
-
|
|
1922
|
-
|
|
1923
|
-
|
|
1924
|
-
|
|
1925
|
-
|
|
1926
|
-
|
|
1927
|
-
|
|
1928
|
-
|
|
1929
|
-
|
|
1930
|
-
|
|
1931
|
-
|
|
1932
|
-
|
|
1933
|
-
|
|
1934
|
-
searchParts.push(
|
|
1935
|
-
|
|
1936
|
-
|
|
1937
|
-
|
|
1938
|
-
|
|
1939
|
-
|
|
1954
|
+
const plainFields = "number,title,url,labels,assignees,createdAt,updatedAt";
|
|
1955
|
+
const buildArgs = (jsonFields) => {
|
|
1956
|
+
const args = [
|
|
1957
|
+
"issue",
|
|
1958
|
+
"list",
|
|
1959
|
+
"--json",
|
|
1960
|
+
jsonFields,
|
|
1961
|
+
"--limit",
|
|
1962
|
+
String(limit),
|
|
1963
|
+
"--state",
|
|
1964
|
+
"open"
|
|
1965
|
+
];
|
|
1966
|
+
const query = opts?.query?.trim() ?? "";
|
|
1967
|
+
const assignee = opts?.assignee?.trim() ?? "";
|
|
1968
|
+
const key = assignee.toLowerCase();
|
|
1969
|
+
const searchParts = [];
|
|
1970
|
+
if (query) searchParts.push(query);
|
|
1971
|
+
if (opts?.updatedSince) {
|
|
1972
|
+
searchParts.push(`updated:>=${formatGitHubSearchUpdatedSince(opts.updatedSince)}`);
|
|
1973
|
+
}
|
|
1974
|
+
if (key === "unassigned" || key === "none" || key === "null") {
|
|
1975
|
+
searchParts.push("no:assignee");
|
|
1976
|
+
} else if (key && key !== "all" && key !== "*") {
|
|
1977
|
+
args.push("--assignee", key === "me" || key === "@me" ? "@me" : assignee);
|
|
1978
|
+
}
|
|
1979
|
+
if (searchParts.length) args.push("--search", searchParts.join(" "));
|
|
1980
|
+
if (slug) args.push("--repo", slug);
|
|
1981
|
+
return args;
|
|
1982
|
+
};
|
|
1983
|
+
let { stdout, exitCode } = await gh(
|
|
1984
|
+
buildArgs(`${plainFields},projectItems`),
|
|
1985
|
+
repoPath,
|
|
1986
|
+
{ reject: false }
|
|
1987
|
+
);
|
|
1988
|
+
if (exitCode !== 0) {
|
|
1989
|
+
({ stdout, exitCode } = await gh(buildArgs(plainFields), repoPath, { reject: false }));
|
|
1940
1990
|
}
|
|
1941
|
-
if (searchParts.length) args.push("--search", searchParts.join(" "));
|
|
1942
|
-
if (slug) args.push("--repo", slug);
|
|
1943
|
-
const { stdout, exitCode } = await gh(args, repoPath, { reject: false });
|
|
1944
1991
|
if (exitCode !== 0 || !stdout.trim()) return [];
|
|
1945
1992
|
let parsed;
|
|
1946
1993
|
try {
|
|
@@ -1959,6 +2006,9 @@ async function listGitHubIssues(repoPath, opts) {
|
|
|
1959
2006
|
const assignees = (item.assignees ?? []).map((a) => typeof a === "string" ? a : String(a?.login ?? "")).map((login) => login.trim()).filter(Boolean);
|
|
1960
2007
|
const createdAt = item.createdAt?.trim() || void 0;
|
|
1961
2008
|
const updatedAt = item.updatedAt?.trim() || void 0;
|
|
2009
|
+
const record = raw;
|
|
2010
|
+
const projects = githubProjectsOnIssue(record);
|
|
2011
|
+
const readProjects = "projectItems" in record || "projects" in record || "projectItemsV2" in record;
|
|
1962
2012
|
return {
|
|
1963
2013
|
id: Number.isFinite(number) ? `gh-${number}` : identifier,
|
|
1964
2014
|
identifier,
|
|
@@ -1968,6 +2018,7 @@ async function listGitHubIssues(repoPath, opts) {
|
|
|
1968
2018
|
provider: "github",
|
|
1969
2019
|
assignee: assignees[0],
|
|
1970
2020
|
assignees,
|
|
2021
|
+
...readProjects ? { projects } : {},
|
|
1971
2022
|
...createdAt ? { createdAt } : {},
|
|
1972
2023
|
...updatedAt ? { updatedAt } : {}
|
|
1973
2024
|
};
|
|
@@ -2101,13 +2152,164 @@ function resolveRunScriptThreadRef(ref) {
|
|
|
2101
2152
|
return thread.id;
|
|
2102
2153
|
}
|
|
2103
2154
|
|
|
2155
|
+
// src/mcp/create-thread-agent.ts
|
|
2156
|
+
function resolveOrchCreateThreadOptions(input) {
|
|
2157
|
+
const { resolveNewThreadOptions } = input;
|
|
2158
|
+
const account = resolveNewThreadOptions({});
|
|
2159
|
+
const requested = input.requestedAgent;
|
|
2160
|
+
const parent = input.parentAgent ?? void 0;
|
|
2161
|
+
const cursorReady = input.cursorReady ?? isCursorApiKeyConfigured();
|
|
2162
|
+
let agent = account.agent;
|
|
2163
|
+
let ignoredAgent;
|
|
2164
|
+
let honorRequestedModel = false;
|
|
2165
|
+
if (requested) {
|
|
2166
|
+
const parentEcho = Boolean(parent) && requested === parent && requested !== account.agent;
|
|
2167
|
+
const cursorAutofill = requested === "cursor" && account.agent !== "cursor";
|
|
2168
|
+
if (parentEcho || cursorAutofill) {
|
|
2169
|
+
ignoredAgent = requested;
|
|
2170
|
+
} else {
|
|
2171
|
+
agent = requested;
|
|
2172
|
+
honorRequestedModel = input.requestedModel !== void 0;
|
|
2173
|
+
}
|
|
2174
|
+
}
|
|
2175
|
+
let coercedFrom;
|
|
2176
|
+
if (agent === "codex" && parent === "codex" && cursorReady) {
|
|
2177
|
+
coercedFrom = requested ?? "codex";
|
|
2178
|
+
agent = "cursor";
|
|
2179
|
+
}
|
|
2180
|
+
const opts = resolveNewThreadOptions({
|
|
2181
|
+
agent,
|
|
2182
|
+
model: honorRequestedModel ? input.requestedModel : void 0
|
|
2183
|
+
});
|
|
2184
|
+
return {
|
|
2185
|
+
...opts,
|
|
2186
|
+
...ignoredAgent ? { ignoredAgent } : {},
|
|
2187
|
+
...coercedFrom ? { coercedFrom } : {}
|
|
2188
|
+
};
|
|
2189
|
+
}
|
|
2190
|
+
function orchCreateThreadAgentNote(input) {
|
|
2191
|
+
if (input.coercedFrom) {
|
|
2192
|
+
return `Avoid nested Codex under a Codex orchestrator \u2014 used agent=${input.agent}`;
|
|
2193
|
+
}
|
|
2194
|
+
if (input.ignoredAgent) {
|
|
2195
|
+
return `Ignored agent=${input.ignoredAgent} \u2014 used Account default agent=${input.agent}`;
|
|
2196
|
+
}
|
|
2197
|
+
return void 0;
|
|
2198
|
+
}
|
|
2199
|
+
|
|
2200
|
+
// src/mcp/create-orch-child.ts
|
|
2201
|
+
var MAX_ORCH_THREADS = 5;
|
|
2202
|
+
var CREATE_THREAD_TIMEOUT_MS = 9e4;
|
|
2203
|
+
function withTimeout(promise, ms, label) {
|
|
2204
|
+
return new Promise((resolve, reject) => {
|
|
2205
|
+
const timer = setTimeout(() => {
|
|
2206
|
+
reject(new Error(`${label} timed out after ${Math.round(ms / 1e3)}s`));
|
|
2207
|
+
}, ms);
|
|
2208
|
+
promise.then(
|
|
2209
|
+
(value) => {
|
|
2210
|
+
clearTimeout(timer);
|
|
2211
|
+
resolve(value);
|
|
2212
|
+
},
|
|
2213
|
+
(err) => {
|
|
2214
|
+
clearTimeout(timer);
|
|
2215
|
+
reject(err);
|
|
2216
|
+
}
|
|
2217
|
+
);
|
|
2218
|
+
});
|
|
2219
|
+
}
|
|
2220
|
+
async function createOrchChildThread(orch, args, resolveNewThreadOptions) {
|
|
2221
|
+
const envParentId = process.env.SIDEBOARD_ORCHESTRATOR_THREAD_ID?.trim() || "";
|
|
2222
|
+
let parentId = args.parentChatId?.trim() || args.parentThreadId?.trim() || "";
|
|
2223
|
+
let parent = parentId ? orch.getThread(parentId) : null;
|
|
2224
|
+
let parentCorrectedFrom;
|
|
2225
|
+
if (envParentId) {
|
|
2226
|
+
const envParent = orch.getThread(envParentId);
|
|
2227
|
+
if (envParent) {
|
|
2228
|
+
if (parentId && parentId !== envParentId) parentCorrectedFrom = parentId;
|
|
2229
|
+
else if (!parentId) parentCorrectedFrom = void 0;
|
|
2230
|
+
parentId = envParentId;
|
|
2231
|
+
parent = envParent;
|
|
2232
|
+
}
|
|
2233
|
+
}
|
|
2234
|
+
if (parentId && !parent) {
|
|
2235
|
+
parentCorrectedFrom = parentId;
|
|
2236
|
+
parentId = "";
|
|
2237
|
+
parent = null;
|
|
2238
|
+
}
|
|
2239
|
+
if (parentId) {
|
|
2240
|
+
const children = orch.getThreads(false).filter((t) => t.parentThreadId === parentId);
|
|
2241
|
+
if (children.length >= MAX_ORCH_THREADS) {
|
|
2242
|
+
return {
|
|
2243
|
+
ok: false,
|
|
2244
|
+
text: `Thread-creation cap (${MAX_ORCH_THREADS}) reached for this orchestration session`
|
|
2245
|
+
};
|
|
2246
|
+
}
|
|
2247
|
+
}
|
|
2248
|
+
const opts = resolveOrchCreateThreadOptions({
|
|
2249
|
+
requestedAgent: args.agent,
|
|
2250
|
+
requestedModel: args.model,
|
|
2251
|
+
parentAgent: parent?.agent,
|
|
2252
|
+
resolveNewThreadOptions
|
|
2253
|
+
});
|
|
2254
|
+
const agentNote = orchCreateThreadAgentNote(opts);
|
|
2255
|
+
try {
|
|
2256
|
+
const priorIds = new Set(orch.getThreads(false).map((t) => t.id));
|
|
2257
|
+
const thread = await withTimeout(
|
|
2258
|
+
orch.createThread({
|
|
2259
|
+
sourceType: args.sourceType,
|
|
2260
|
+
sourceRef: args.sourceRef,
|
|
2261
|
+
agent: opts.agent,
|
|
2262
|
+
model: opts.model,
|
|
2263
|
+
effort: opts.effort,
|
|
2264
|
+
fast: opts.fast,
|
|
2265
|
+
repoPath: args.repoPath,
|
|
2266
|
+
title: args.title,
|
|
2267
|
+
parentThreadId: parentId || null,
|
|
2268
|
+
cowboy: args.cowboy || void 0,
|
|
2269
|
+
attachments: args.attachments,
|
|
2270
|
+
tags: args.tags === void 0 ? void 0 : normalizeWorkspaceTags(args.tags)
|
|
2271
|
+
}),
|
|
2272
|
+
CREATE_THREAD_TIMEOUT_MS,
|
|
2273
|
+
"create_workspace"
|
|
2274
|
+
);
|
|
2275
|
+
const alreadyStarted = priorIds.has(thread.id);
|
|
2276
|
+
const parentNote = parentCorrectedFrom ? parentId ? `Ignored unknown/stale parentThreadId ${parentCorrectedFrom}; nested under ${parentId}` : `Ignored unknown/stale parentThreadId ${parentCorrectedFrom}; created without parent` : void 0;
|
|
2277
|
+
return {
|
|
2278
|
+
ok: true,
|
|
2279
|
+
text: JSON.stringify({
|
|
2280
|
+
id: thread.id,
|
|
2281
|
+
title: thread.title,
|
|
2282
|
+
branchName: thread.branchName,
|
|
2283
|
+
worktreePath: thread.worktreePath,
|
|
2284
|
+
agent: thread.agent,
|
|
2285
|
+
model: thread.model,
|
|
2286
|
+
status: thread.status,
|
|
2287
|
+
cowboy: Boolean(thread.cowboy),
|
|
2288
|
+
link: `sideboard://chat/${thread.id}`,
|
|
2289
|
+
parentChatId: thread.parentThreadId,
|
|
2290
|
+
parentThreadId: thread.parentThreadId,
|
|
2291
|
+
tags: thread.tags ?? [],
|
|
2292
|
+
chats: workspaceAgentChatSummaries(thread.worktreePath),
|
|
2293
|
+
...alreadyStarted ? { alreadyStarted: true } : {},
|
|
2294
|
+
...opts.coercedFrom ? { agentCoercedFrom: opts.coercedFrom } : {},
|
|
2295
|
+
...opts.ignoredAgent ? { agentIgnored: opts.ignoredAgent } : {},
|
|
2296
|
+
...agentNote ? { note: agentNote } : {},
|
|
2297
|
+
...parentCorrectedFrom ? { parentCorrectedFrom, parentNote } : {}
|
|
2298
|
+
})
|
|
2299
|
+
};
|
|
2300
|
+
} catch (err) {
|
|
2301
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
2302
|
+
return { ok: false, text: `create_workspace failed: ${message}` };
|
|
2303
|
+
}
|
|
2304
|
+
}
|
|
2305
|
+
|
|
2104
2306
|
// src/mcp/thread-visibility.ts
|
|
2105
2307
|
function lastMessagePreview(messages, max = 160) {
|
|
2106
2308
|
if (!messages?.length) return null;
|
|
2107
2309
|
for (let i = messages.length - 1; i >= 0; i--) {
|
|
2108
|
-
const
|
|
2109
|
-
if (!
|
|
2110
|
-
const flat =
|
|
2310
|
+
const text5 = messages[i]?.text?.trim();
|
|
2311
|
+
if (!text5 || isInternalAgentStatusText(text5) || isInjectedNoticeText(text5)) continue;
|
|
2312
|
+
const flat = text5.replace(/\s+/g, " ");
|
|
2111
2313
|
return flat.length > max ? `${flat.slice(0, max)}\u2026` : flat;
|
|
2112
2314
|
}
|
|
2113
2315
|
return null;
|
|
@@ -2124,97 +2326,28 @@ function childThreadRefs(parentId, threads) {
|
|
|
2124
2326
|
}
|
|
2125
2327
|
|
|
2126
2328
|
// src/mcp/abletime-tools.ts
|
|
2127
|
-
import { z as
|
|
2329
|
+
import { z as z2 } from "zod";
|
|
2128
2330
|
|
|
2129
|
-
// src/mcp/issue-
|
|
2331
|
+
// src/mcp/issue-payload.ts
|
|
2130
2332
|
import { z } from "zod";
|
|
2131
|
-
|
|
2132
|
-
|
|
2133
|
-
|
|
2134
|
-
|
|
2135
|
-
|
|
2136
|
-
|
|
2137
|
-
|
|
2138
|
-
|
|
2139
|
-
|
|
2140
|
-
|
|
2141
|
-
|
|
2142
|
-
};
|
|
2333
|
+
|
|
2334
|
+
// src/mcp/smart-crush.ts
|
|
2335
|
+
var MUST_KEEP_TEXT_RE = /\b(error|exception|failed|failure|fatal|critical|panic|traceback|npm err|assert)\b/i;
|
|
2336
|
+
var CRUSH_MIN_ITEMS = 5;
|
|
2337
|
+
var CRUSH_MIN_CHARS = 800;
|
|
2338
|
+
var CRUSH_COMMENT_MAX_CHARS = 16e3;
|
|
2339
|
+
var CRUSH_BODY_MIN_CHARS = 12e3;
|
|
2340
|
+
var CRUSH_BODY_MAX_CHARS = 8e3;
|
|
2341
|
+
var NEAR_DUP_JACCARD = 0.92;
|
|
2342
|
+
function looksLikeMustKeep(text5) {
|
|
2343
|
+
return MUST_KEEP_TEXT_RE.test(text5);
|
|
2143
2344
|
}
|
|
2144
|
-
|
|
2145
|
-
|
|
2146
|
-
function compactIssueRow(issue, opts) {
|
|
2147
|
-
return {
|
|
2148
|
-
identifier: issue.identifier,
|
|
2149
|
-
title: issue.title,
|
|
2150
|
-
...issue.assignee ? { assignee: issue.assignee } : {},
|
|
2151
|
-
...issue.labels.length > 0 ? { labels: issue.labels } : {},
|
|
2152
|
-
...issue.teamKey ? { team: issue.teamKey } : {},
|
|
2153
|
-
...issue.cycle?.name ? { cycle: issue.cycle.name } : {},
|
|
2154
|
-
...issue.createdAt ? { createdAt: issue.createdAt } : {},
|
|
2155
|
-
...issue.updatedAt ? { updatedAt: issue.updatedAt } : {},
|
|
2156
|
-
...opts?.since ? { kind: issueActivityKind(issue, opts.since) } : {}
|
|
2157
|
-
};
|
|
2158
|
-
}
|
|
2159
|
-
function formatMcpIssueList(input) {
|
|
2160
|
-
const truncated = input.truncated || Boolean(input.commentsTruncated);
|
|
2161
|
-
const comments = input.comments ?? [];
|
|
2162
|
-
return {
|
|
2163
|
-
...input.source ? { source: input.source } : {},
|
|
2164
|
-
...input.viewer ? { viewer: input.viewer } : {},
|
|
2165
|
-
limit: input.limit,
|
|
2166
|
-
returned: input.issues.length,
|
|
2167
|
-
truncated,
|
|
2168
|
-
...input.since ? { since: input.since } : {},
|
|
2169
|
-
...truncated ? {
|
|
2170
|
-
hint: input.since ? "More issues or comments match. Pass a higher limit (max 250) or a tighter query." : "More issues match. Pass a higher limit (max 250) or a tighter query."
|
|
2171
|
-
} : {},
|
|
2172
|
-
issues: input.issues.map((issue) => compactIssueRow(issue, { since: input.since })),
|
|
2173
|
-
...input.since || comments.length > 0 ? { comments } : {}
|
|
2174
|
-
};
|
|
2175
|
-
}
|
|
2176
|
-
function formatListedIssuesForMcp(result, page) {
|
|
2177
|
-
const windowed = applyIssueListWindow(result.issues, page);
|
|
2178
|
-
const comments = result.comments ?? [];
|
|
2179
|
-
const commentWindow = applyIssueListWindow(comments, page);
|
|
2180
|
-
return formatMcpIssueList({
|
|
2181
|
-
source: result.source,
|
|
2182
|
-
viewer: result.viewer?.name || result.viewer?.login,
|
|
2183
|
-
limit: page,
|
|
2184
|
-
issues: windowed.items,
|
|
2185
|
-
truncated: windowed.truncated,
|
|
2186
|
-
since: result.since,
|
|
2187
|
-
comments: commentWindow.items,
|
|
2188
|
-
commentsTruncated: commentWindow.truncated
|
|
2189
|
-
});
|
|
2190
|
-
}
|
|
2191
|
-
function mcpJson(payload, isError = false) {
|
|
2192
|
-
return {
|
|
2193
|
-
content: [{ type: "text", text: JSON.stringify(payload) }],
|
|
2194
|
-
...isError ? { isError: true } : {}
|
|
2195
|
-
};
|
|
2196
|
-
}
|
|
2197
|
-
|
|
2198
|
-
// src/mcp/issue-payload.ts
|
|
2199
|
-
import { z as z2 } from "zod";
|
|
2200
|
-
|
|
2201
|
-
// src/mcp/smart-crush.ts
|
|
2202
|
-
var MUST_KEEP_TEXT_RE = /\b(error|exception|failed|failure|fatal|critical|panic|traceback|npm err|assert)\b/i;
|
|
2203
|
-
var CRUSH_MIN_ITEMS = 5;
|
|
2204
|
-
var CRUSH_MIN_CHARS = 800;
|
|
2205
|
-
var CRUSH_COMMENT_MAX_CHARS = 16e3;
|
|
2206
|
-
var CRUSH_BODY_MIN_CHARS = 12e3;
|
|
2207
|
-
var CRUSH_BODY_MAX_CHARS = 8e3;
|
|
2208
|
-
var NEAR_DUP_JACCARD = 0.92;
|
|
2209
|
-
function looksLikeMustKeep(text6) {
|
|
2210
|
-
return MUST_KEEP_TEXT_RE.test(text6);
|
|
2211
|
-
}
|
|
2212
|
-
function normalizeText(text6) {
|
|
2213
|
-
return text6.replace(/\s+/g, " ").trim().toLowerCase();
|
|
2345
|
+
function normalizeText(text5) {
|
|
2346
|
+
return text5.replace(/\s+/g, " ").trim().toLowerCase();
|
|
2214
2347
|
}
|
|
2215
|
-
function wordSet(
|
|
2348
|
+
function wordSet(text5) {
|
|
2216
2349
|
return new Set(
|
|
2217
|
-
|
|
2350
|
+
text5.toLowerCase().split(/\W+/).filter((w) => w.length > 2)
|
|
2218
2351
|
);
|
|
2219
2352
|
}
|
|
2220
2353
|
function jaccard(a, b) {
|
|
@@ -2226,11 +2359,11 @@ function jaccard(a, b) {
|
|
|
2226
2359
|
const union = a.size + b.size - inter;
|
|
2227
2360
|
return union === 0 ? 0 : inter / union;
|
|
2228
2361
|
}
|
|
2229
|
-
function importance(
|
|
2230
|
-
let score =
|
|
2231
|
-
if (/```/.test(
|
|
2232
|
-
if (/https?:\/\//.test(
|
|
2233
|
-
if (looksLikeMustKeep(
|
|
2362
|
+
function importance(text5) {
|
|
2363
|
+
let score = text5.length;
|
|
2364
|
+
if (/```/.test(text5)) score += 400;
|
|
2365
|
+
if (/https?:\/\//.test(text5)) score += 50;
|
|
2366
|
+
if (looksLikeMustKeep(text5)) score += 1e4;
|
|
2234
2367
|
return score;
|
|
2235
2368
|
}
|
|
2236
2369
|
function meanStd(values) {
|
|
@@ -2244,15 +2377,15 @@ function dedupTextItems(items, getText) {
|
|
|
2244
2377
|
const keptNorm = [];
|
|
2245
2378
|
const keptWords = [];
|
|
2246
2379
|
for (const item of items) {
|
|
2247
|
-
const
|
|
2248
|
-
const norm = normalizeText(
|
|
2380
|
+
const text5 = getText(item);
|
|
2381
|
+
const norm = normalizeText(text5);
|
|
2249
2382
|
if (!norm) {
|
|
2250
2383
|
kept.push(item);
|
|
2251
2384
|
keptNorm.push(norm);
|
|
2252
|
-
keptWords.push(wordSet(
|
|
2385
|
+
keptWords.push(wordSet(text5));
|
|
2253
2386
|
continue;
|
|
2254
2387
|
}
|
|
2255
|
-
const words = wordSet(
|
|
2388
|
+
const words = wordSet(text5);
|
|
2256
2389
|
let dupAt = -1;
|
|
2257
2390
|
for (let i = 0; i < kept.length; i++) {
|
|
2258
2391
|
if (keptNorm[i] === norm) {
|
|
@@ -2270,7 +2403,7 @@ function dedupTextItems(items, getText) {
|
|
|
2270
2403
|
keptWords.push(words);
|
|
2271
2404
|
continue;
|
|
2272
2405
|
}
|
|
2273
|
-
if (
|
|
2406
|
+
if (text5.length > getText(kept[dupAt]).length) {
|
|
2274
2407
|
kept[dupAt] = item;
|
|
2275
2408
|
keptNorm[dupAt] = norm;
|
|
2276
2409
|
keptWords[dupAt] = words;
|
|
@@ -2311,8 +2444,8 @@ function crushTextItems(items, getText, opts = {}) {
|
|
|
2311
2444
|
const outlierFloor = mean + 2 * std;
|
|
2312
2445
|
const mustKeep = /* @__PURE__ */ new Set();
|
|
2313
2446
|
unique.forEach((item, i) => {
|
|
2314
|
-
const
|
|
2315
|
-
if (looksLikeMustKeep(
|
|
2447
|
+
const text5 = getText(item);
|
|
2448
|
+
if (looksLikeMustKeep(text5) || text5.length > outlierFloor) mustKeep.add(i);
|
|
2316
2449
|
});
|
|
2317
2450
|
const remainingIdx = unique.map((_, i) => i).filter((i) => !mustKeep.has(i));
|
|
2318
2451
|
let used = [...mustKeep].reduce((n, i) => n + lengths[i], 0);
|
|
@@ -2354,9 +2487,9 @@ function crushTextItems(items, getText, opts = {}) {
|
|
|
2354
2487
|
reason: "budget"
|
|
2355
2488
|
};
|
|
2356
2489
|
}
|
|
2357
|
-
function splitBlocks(
|
|
2358
|
-
if (/\n\n/.test(
|
|
2359
|
-
return
|
|
2490
|
+
function splitBlocks(text5) {
|
|
2491
|
+
if (/\n\n/.test(text5)) return text5.split(/\n\n+/);
|
|
2492
|
+
return text5.split("\n");
|
|
2360
2493
|
}
|
|
2361
2494
|
function capOversizedBlock(block, maxChars) {
|
|
2362
2495
|
if (block.length <= maxChars) return block;
|
|
@@ -2368,8 +2501,8 @@ function capOversizedBlock(block, maxChars) {
|
|
|
2368
2501
|
|
|
2369
2502
|
${block.slice(-tail)}`;
|
|
2370
2503
|
}
|
|
2371
|
-
function crushMarkdown(
|
|
2372
|
-
const original =
|
|
2504
|
+
function crushMarkdown(text5, opts) {
|
|
2505
|
+
const original = text5 ?? "";
|
|
2373
2506
|
const minChars = opts?.minChars ?? CRUSH_BODY_MIN_CHARS;
|
|
2374
2507
|
const maxChars = opts?.maxChars ?? CRUSH_BODY_MAX_CHARS;
|
|
2375
2508
|
if (original.length < minChars) {
|
|
@@ -2441,7 +2574,7 @@ function crushMarkdown(text6, opts) {
|
|
|
2441
2574
|
|
|
2442
2575
|
// src/mcp/issue-payload.ts
|
|
2443
2576
|
var MCP_ISSUE_INCLUDE_VALUES = ["default", "full"];
|
|
2444
|
-
var mcpIssueIncludeSchema =
|
|
2577
|
+
var mcpIssueIncludeSchema = z.enum(MCP_ISSUE_INCLUDE_VALUES).optional().describe(
|
|
2445
2578
|
"default applies SmartCrusher-style crush (dedup + budget; small unique tickets pass through). full returns the uncompressed vendor payload. Re-call with include=full if crush.truncated."
|
|
2446
2579
|
);
|
|
2447
2580
|
var MCP_ISSUE_CRUSH_HINT = "Pass include=full to re-fetch the uncompressed ticket from the vendor.";
|
|
@@ -2459,13 +2592,13 @@ function withComments(comments, meta) {
|
|
|
2459
2592
|
}
|
|
2460
2593
|
return { comments: crushed.items, meta };
|
|
2461
2594
|
}
|
|
2462
|
-
function withBody(
|
|
2463
|
-
const crushed = crushMarkdown(
|
|
2595
|
+
function withBody(text5, meta) {
|
|
2596
|
+
const crushed = crushMarkdown(text5);
|
|
2464
2597
|
if (crushed.truncated) {
|
|
2465
2598
|
meta.body = { originalChars: crushed.originalChars };
|
|
2466
2599
|
return { value: crushed.text, meta };
|
|
2467
2600
|
}
|
|
2468
|
-
return { value:
|
|
2601
|
+
return { value: text5, meta };
|
|
2469
2602
|
}
|
|
2470
2603
|
function attachCrush(payload, meta) {
|
|
2471
2604
|
if (!meta.comments && !meta.body) return payload;
|
|
@@ -2587,7 +2720,7 @@ function registerAbleTimeTools(server) {
|
|
|
2587
2720
|
server.tool(
|
|
2588
2721
|
"abletime_list_tasks",
|
|
2589
2722
|
"List open AbleTime tasks (excludes done/archived). Optional projectId filter. Pass updatedSince for tasks updated since then and any comments on that page.",
|
|
2590
|
-
{ projectId:
|
|
2723
|
+
{ projectId: z2.string().optional(), updatedSince: mcpIssueUpdatedSinceSchema },
|
|
2591
2724
|
async ({ projectId, updatedSince }) => {
|
|
2592
2725
|
try {
|
|
2593
2726
|
const tasks = await listAbleTimeTasks({ projectId });
|
|
@@ -2600,7 +2733,7 @@ function registerAbleTimeTools(server) {
|
|
|
2600
2733
|
server.tool(
|
|
2601
2734
|
"abletime_search_tasks",
|
|
2602
2735
|
"Search AbleTime tasks by text, reference, or tag. Pass updatedSince for tasks updated since then and any comments on that page.",
|
|
2603
|
-
{ query:
|
|
2736
|
+
{ query: z2.string(), updatedSince: mcpIssueUpdatedSinceSchema },
|
|
2604
2737
|
async ({ query, updatedSince }) => {
|
|
2605
2738
|
try {
|
|
2606
2739
|
const tasks = await searchAbleTimeTasks(query);
|
|
@@ -2612,8 +2745,8 @@ function registerAbleTimeTools(server) {
|
|
|
2612
2745
|
);
|
|
2613
2746
|
server.tool(
|
|
2614
2747
|
"abletime_get_task",
|
|
2615
|
-
"Get one AbleTime task by id or reference (e.g. CRM-232): description, state, comments, attachments. Re-fetch to read new comments. Default crushes redundant comments and huge pasted bodies (SmartCrusher-style; small unique tickets pass through). Pass include=full for the uncompressed vendor payload. To save a file, call abletime_download_attachment with the attachment id or url (writes `.context/attachments/`; do not dump bytes into the tool result).",
|
|
2616
|
-
{ id:
|
|
2748
|
+
"Get one AbleTime task by id or reference (e.g. CRM-232): description, state, project, comments, attachments. Re-fetch to read new comments. Default crushes redundant comments and huge pasted bodies (SmartCrusher-style; small unique tickets pass through). Pass include=full for the uncompressed vendor payload. To save a file, call abletime_download_attachment with the attachment id or url (writes `.context/attachments/`; do not dump bytes into the tool result).",
|
|
2749
|
+
{ id: z2.string(), include: mcpIssueIncludeSchema },
|
|
2617
2750
|
async ({ id, include }) => {
|
|
2618
2751
|
try {
|
|
2619
2752
|
return text(formatAbleTimeTaskPayload(await getAbleTimeTask(id), include));
|
|
@@ -2626,9 +2759,9 @@ function registerAbleTimeTools(server) {
|
|
|
2626
2759
|
"abletime_download_attachment",
|
|
2627
2760
|
"Download an AbleTime task file into `.context/attachments/` using Account AbleTime auth. Pass the attachment id or URL from abletime_get_task. Returns the worktree-relative path \u2014 Read that file; do not dump bytes into chat.",
|
|
2628
2761
|
{
|
|
2629
|
-
id:
|
|
2630
|
-
name:
|
|
2631
|
-
repoPath:
|
|
2762
|
+
id: z2.string().describe("Attachment id or URL from abletime_get_task."),
|
|
2763
|
+
name: z2.string().optional().describe("Optional filename override."),
|
|
2764
|
+
repoPath: z2.string().optional().describe("Workspace / worktree path. Omit on a worktree turn (uses cwd).")
|
|
2632
2765
|
},
|
|
2633
2766
|
async ({ id, name, repoPath }) => {
|
|
2634
2767
|
try {
|
|
@@ -2641,7 +2774,7 @@ function registerAbleTimeTools(server) {
|
|
|
2641
2774
|
server.tool(
|
|
2642
2775
|
"abletime_comment",
|
|
2643
2776
|
"Add a markdown comment on an AbleTime task (id or CRM-232). When reviewing a PR or ticket, show the draft in chat and wait until they type a next step \u2014 do not ask_user after the review. The author sees this immediately.",
|
|
2644
|
-
{ id:
|
|
2777
|
+
{ id: z2.string(), body: z2.string() },
|
|
2645
2778
|
async (args) => {
|
|
2646
2779
|
try {
|
|
2647
2780
|
return text(await commentAbleTimeTask(args));
|
|
@@ -2650,23 +2783,23 @@ function registerAbleTimeTools(server) {
|
|
|
2650
2783
|
}
|
|
2651
2784
|
}
|
|
2652
2785
|
);
|
|
2653
|
-
const relationInputSchema =
|
|
2654
|
-
type:
|
|
2655
|
-
issue:
|
|
2786
|
+
const relationInputSchema = z2.object({
|
|
2787
|
+
type: z2.string().describe("blockedBy (this waits on issue) or blocks (issue waits on this)."),
|
|
2788
|
+
issue: z2.string().describe("Related task id or reference (CRM-232).")
|
|
2656
2789
|
});
|
|
2657
2790
|
server.tool(
|
|
2658
2791
|
"abletime_update_task",
|
|
2659
2792
|
'Update an AbleTime task (id or CRM-232). Pass title, description, state, labels, project, parent, and/or relations. labels replaces tags. project is name or id (tasks stay in a project). parent is CRM-232 or "none". relations sets the wait-on dependency (blockedBy/blocks); removeRelations clears it. When reviewing a PR or ticket, wait until they type a next step before changing the task \u2014 do not ask_user after the review. The author is notified.',
|
|
2660
2793
|
{
|
|
2661
|
-
id:
|
|
2662
|
-
title:
|
|
2663
|
-
description:
|
|
2664
|
-
state:
|
|
2665
|
-
labels:
|
|
2666
|
-
project:
|
|
2667
|
-
parent:
|
|
2668
|
-
relations:
|
|
2669
|
-
removeRelations:
|
|
2794
|
+
id: z2.string(),
|
|
2795
|
+
title: z2.string().optional(),
|
|
2796
|
+
description: z2.string().optional(),
|
|
2797
|
+
state: z2.string().optional(),
|
|
2798
|
+
labels: z2.array(z2.string()).optional().describe("Replace tags/labels. Empty array clears when the host accepts it."),
|
|
2799
|
+
project: z2.string().nullable().optional().describe("Project name or id. AbleTime tasks stay in a project."),
|
|
2800
|
+
parent: z2.string().nullable().optional().describe('Parent task id or reference (CRM-232), or "none" to unset.'),
|
|
2801
|
+
relations: z2.array(relationInputSchema).optional().describe("Set a wait-on dependency: blockedBy or blocks."),
|
|
2802
|
+
removeRelations: z2.array(relationInputSchema).optional().describe("Clear a wait-on dependency.")
|
|
2670
2803
|
},
|
|
2671
2804
|
async (args) => {
|
|
2672
2805
|
try {
|
|
@@ -2685,12 +2818,12 @@ function registerAbleTimeTools(server) {
|
|
|
2685
2818
|
"abletime_create_task",
|
|
2686
2819
|
"Create an AbleTime task in a project. Call abletime_list_projects if you do not have a project id. Pass parent (CRM-232) for a spin-off. New tasks start in todo (or backlog).",
|
|
2687
2820
|
{
|
|
2688
|
-
title:
|
|
2689
|
-
description:
|
|
2690
|
-
projectId:
|
|
2691
|
-
categoryId:
|
|
2692
|
-
state:
|
|
2693
|
-
parent:
|
|
2821
|
+
title: z2.string(),
|
|
2822
|
+
description: z2.string().optional(),
|
|
2823
|
+
projectId: z2.string().optional(),
|
|
2824
|
+
categoryId: z2.string().optional(),
|
|
2825
|
+
state: z2.enum(["backlog", "todo"]).optional(),
|
|
2826
|
+
parent: z2.string().optional().describe("Parent task id or reference (CRM-232) for a spin-off.")
|
|
2694
2827
|
},
|
|
2695
2828
|
async (args) => {
|
|
2696
2829
|
try {
|
|
@@ -2704,10 +2837,10 @@ function registerAbleTimeTools(server) {
|
|
|
2704
2837
|
"abletime_ensure_task",
|
|
2705
2838
|
"Find or create an AbleTime task for this work, then create_workspace with sourceType=ticket.",
|
|
2706
2839
|
{
|
|
2707
|
-
title:
|
|
2708
|
-
description:
|
|
2709
|
-
projectId:
|
|
2710
|
-
categoryId:
|
|
2840
|
+
title: z2.string(),
|
|
2841
|
+
description: z2.string().optional(),
|
|
2842
|
+
projectId: z2.string().optional(),
|
|
2843
|
+
categoryId: z2.string().optional()
|
|
2711
2844
|
},
|
|
2712
2845
|
async (args) => {
|
|
2713
2846
|
try {
|
|
@@ -2721,7 +2854,7 @@ function registerAbleTimeTools(server) {
|
|
|
2721
2854
|
}
|
|
2722
2855
|
|
|
2723
2856
|
// src/mcp/github-tools.ts
|
|
2724
|
-
import { z as
|
|
2857
|
+
import { z as z3 } from "zod";
|
|
2725
2858
|
function text2(payload, isError = false) {
|
|
2726
2859
|
return mcpJson(payload, isError);
|
|
2727
2860
|
}
|
|
@@ -2731,15 +2864,15 @@ function fail2(err) {
|
|
|
2731
2864
|
true
|
|
2732
2865
|
);
|
|
2733
2866
|
}
|
|
2734
|
-
var repoPathSchema =
|
|
2867
|
+
var repoPathSchema = z3.string().optional().describe("Workspace / worktree path. Omit on a worktree turn (uses cwd).");
|
|
2735
2868
|
function registerGithubIssueTools(server) {
|
|
2736
2869
|
server.tool(
|
|
2737
2870
|
"github_search_issues",
|
|
2738
2871
|
"Search or list open GitHub issues in this repo via Account gh. Default 40; pass query and/or limit (max 250) when truncated. assignee: me, unassigned, all, or a login. Pass updatedSince for new/updated tickets and new comments (e.g. \u201Cany updates since yesterday?\u201D).",
|
|
2739
2872
|
{
|
|
2740
|
-
query:
|
|
2741
|
-
assignee:
|
|
2742
|
-
limit:
|
|
2873
|
+
query: z3.string().optional().describe("Search text (title, body, identifiers). Omit to list by assignee only."),
|
|
2874
|
+
assignee: z3.string().optional().describe("me, unassigned, all, or a GitHub login. Default: all when query is set, otherwise me."),
|
|
2875
|
+
limit: z3.number().int().positive().max(250).optional().describe("Page size (default 40, max 250). Raise when truncated is true."),
|
|
2743
2876
|
updatedSince: mcpIssueUpdatedSinceSchema,
|
|
2744
2877
|
repoPath: repoPathSchema
|
|
2745
2878
|
},
|
|
@@ -2776,10 +2909,22 @@ function registerGithubIssueTools(server) {
|
|
|
2776
2909
|
}
|
|
2777
2910
|
}
|
|
2778
2911
|
);
|
|
2912
|
+
server.tool(
|
|
2913
|
+
"github_list_projects",
|
|
2914
|
+
"List open GitHub Projects for the signed-in user and this repo owner (title, number, id). Pass a title as project on github_update_issue. If GitHub says the token is missing read:project, run `gh auth refresh -s read:project`.",
|
|
2915
|
+
{ repoPath: repoPathSchema },
|
|
2916
|
+
async ({ repoPath }) => {
|
|
2917
|
+
try {
|
|
2918
|
+
return text2({ projects: await listGitHubProjects({ repoPath }) });
|
|
2919
|
+
} catch (err) {
|
|
2920
|
+
return fail2(err);
|
|
2921
|
+
}
|
|
2922
|
+
}
|
|
2923
|
+
);
|
|
2779
2924
|
server.tool(
|
|
2780
2925
|
"github_get_issue",
|
|
2781
|
-
"Get a GitHub issue (#123 or URL): body, comments, state, attachments. Re-fetch to read new comments. Default crushes redundant comments and huge pasted bodies (SmartCrusher-style; small unique tickets pass through). Pass include=full for the uncompressed vendor payload. Uses Account gh. To save a file, call github_download_attachment with the attachment URL (writes `.context/attachments/`; do not dump bytes into the tool result).",
|
|
2782
|
-
{ id:
|
|
2926
|
+
"Get a GitHub issue (#123 or URL): body, comments, state, projects, attachments. Re-fetch to read new comments. projectsError means GitHub refused Projects (usually missing read:project \u2014 run `gh auth refresh -s read:project`). Default crushes redundant comments and huge pasted bodies (SmartCrusher-style; small unique tickets pass through). Pass include=full for the uncompressed vendor payload. Uses Account gh. To save a file, call github_download_attachment with the attachment URL (writes `.context/attachments/`; do not dump bytes into the tool result).",
|
|
2927
|
+
{ id: z3.string(), include: mcpIssueIncludeSchema, repoPath: repoPathSchema },
|
|
2783
2928
|
async ({ id, include, repoPath }) => {
|
|
2784
2929
|
try {
|
|
2785
2930
|
return text2(formatGitHubIssuePayload(await getGitHubIssue(id, { repoPath }), include));
|
|
@@ -2792,8 +2937,8 @@ function registerGithubIssueTools(server) {
|
|
|
2792
2937
|
"github_download_attachment",
|
|
2793
2938
|
"Download a GitHub issue file (github.com/user-attachments/\u2026 or githubusercontent.com) into `.context/attachments/` using Account gh. Pass the URL from github_get_issue attachments. Returns the worktree-relative path \u2014 Read that file; do not dump bytes into chat.",
|
|
2794
2939
|
{
|
|
2795
|
-
url:
|
|
2796
|
-
name:
|
|
2940
|
+
url: z3.string().describe("Attachment URL from github_get_issue."),
|
|
2941
|
+
name: z3.string().optional().describe("Optional filename override."),
|
|
2797
2942
|
repoPath: repoPathSchema
|
|
2798
2943
|
},
|
|
2799
2944
|
async ({ url, name, repoPath }) => {
|
|
@@ -2807,7 +2952,7 @@ function registerGithubIssueTools(server) {
|
|
|
2807
2952
|
server.tool(
|
|
2808
2953
|
"github_comment",
|
|
2809
2954
|
"Add a markdown comment on a GitHub issue (#123 or URL). Uses Account gh. When reviewing a PR or ticket, show the draft in chat and wait until they type a next step \u2014 do not ask_user after the review. The author sees this immediately.",
|
|
2810
|
-
{ id:
|
|
2955
|
+
{ id: z3.string(), body: z3.string(), repoPath: repoPathSchema },
|
|
2811
2956
|
async ({ id, body, repoPath }) => {
|
|
2812
2957
|
try {
|
|
2813
2958
|
return text2(await commentGitHubIssue({ id, body }, { repoPath }));
|
|
@@ -2816,23 +2961,23 @@ function registerGithubIssueTools(server) {
|
|
|
2816
2961
|
}
|
|
2817
2962
|
}
|
|
2818
2963
|
);
|
|
2819
|
-
const relationInputSchema =
|
|
2820
|
-
type:
|
|
2821
|
-
issue:
|
|
2964
|
+
const relationInputSchema = z3.object({
|
|
2965
|
+
type: z3.string().describe("blocks or blockedBy (this issue's view)."),
|
|
2966
|
+
issue: z3.string().describe("Related issue #123 or URL.")
|
|
2822
2967
|
});
|
|
2823
2968
|
server.tool(
|
|
2824
2969
|
"github_update_issue",
|
|
2825
2970
|
'Update a GitHub issue (#123). Pass title, body, state (open|closed), labels, project, parent, and/or relations. labels replaces all names (pass [] to clear). project is a Project title, or "none" to remove. parent is #123 or "none". relations adds blockedBy/blocks; removeRelations drops them (`gh` 2.94+). When reviewing a PR or ticket, wait until they type a next step before changing the issue \u2014 do not ask_user after the review. The author is notified.',
|
|
2826
2971
|
{
|
|
2827
|
-
id:
|
|
2828
|
-
title:
|
|
2829
|
-
body:
|
|
2830
|
-
state:
|
|
2831
|
-
labels:
|
|
2832
|
-
project:
|
|
2833
|
-
parent:
|
|
2834
|
-
relations:
|
|
2835
|
-
removeRelations:
|
|
2972
|
+
id: z3.string(),
|
|
2973
|
+
title: z3.string().optional(),
|
|
2974
|
+
body: z3.string().optional(),
|
|
2975
|
+
state: z3.string().optional().describe("open or closed"),
|
|
2976
|
+
labels: z3.array(z3.string()).optional().describe("Replace all labels. Empty array clears. Call github_get_issue first to keep existing."),
|
|
2977
|
+
project: z3.string().nullable().optional().describe('GitHub Project title, or "none" to remove current projects.'),
|
|
2978
|
+
parent: z3.string().nullable().optional().describe('Parent issue #123 or URL, or "none" to unset.'),
|
|
2979
|
+
relations: z3.array(relationInputSchema).optional().describe("Add blockedBy or blocks relations (issue #123 or URL)."),
|
|
2980
|
+
removeRelations: z3.array(relationInputSchema).optional().describe("Remove blockedBy or blocks relations."),
|
|
2836
2981
|
repoPath: repoPathSchema
|
|
2837
2982
|
},
|
|
2838
2983
|
async (args) => {
|
|
@@ -2847,9 +2992,9 @@ function registerGithubIssueTools(server) {
|
|
|
2847
2992
|
"github_create_issue",
|
|
2848
2993
|
"Create a GitHub issue. Pass parent (#123) to mark a spin-off in the body.",
|
|
2849
2994
|
{
|
|
2850
|
-
title:
|
|
2851
|
-
body:
|
|
2852
|
-
parent:
|
|
2995
|
+
title: z3.string(),
|
|
2996
|
+
body: z3.string().optional(),
|
|
2997
|
+
parent: z3.string().optional().describe("Parent issue #123 or URL for a spin-off."),
|
|
2853
2998
|
repoPath: repoPathSchema
|
|
2854
2999
|
},
|
|
2855
3000
|
async (args) => {
|
|
@@ -2863,7 +3008,7 @@ function registerGithubIssueTools(server) {
|
|
|
2863
3008
|
}
|
|
2864
3009
|
|
|
2865
3010
|
// src/mcp/linear-tools.ts
|
|
2866
|
-
import { z as
|
|
3011
|
+
import { z as z4 } from "zod";
|
|
2867
3012
|
function text3(payload, isError = false) {
|
|
2868
3013
|
return mcpJson(payload, isError);
|
|
2869
3014
|
}
|
|
@@ -2873,7 +3018,7 @@ function fail3(err) {
|
|
|
2873
3018
|
true
|
|
2874
3019
|
);
|
|
2875
3020
|
}
|
|
2876
|
-
var prioritySchema =
|
|
3021
|
+
var prioritySchema = z4.number().int().min(0).max(4).optional().describe("0 none, 1 urgent, 2 high, 3 medium, 4 low");
|
|
2877
3022
|
function registerLinearTools(server) {
|
|
2878
3023
|
server.tool(
|
|
2879
3024
|
"linear_list_teams",
|
|
@@ -2887,13 +3032,25 @@ function registerLinearTools(server) {
|
|
|
2887
3032
|
}
|
|
2888
3033
|
}
|
|
2889
3034
|
);
|
|
3035
|
+
server.tool(
|
|
3036
|
+
"linear_list_projects",
|
|
3037
|
+
"List Linear projects (id, name, slugId). Pass a name or id as project on linear_update_issue. linear_get_issue and search rows also include the project a ticket is on.",
|
|
3038
|
+
{},
|
|
3039
|
+
async () => {
|
|
3040
|
+
try {
|
|
3041
|
+
return text3({ projects: await listLinearProjects() });
|
|
3042
|
+
} catch (err) {
|
|
3043
|
+
return fail3(err);
|
|
3044
|
+
}
|
|
3045
|
+
}
|
|
3046
|
+
);
|
|
2890
3047
|
server.tool(
|
|
2891
3048
|
"linear_search_issues",
|
|
2892
3049
|
"Search or list open Linear issues. Default 40; pass query and/or limit (max 250) when truncated. assignee: me, unassigned, all (default with query), or a user id/name. Pass updatedSince for new/updated tickets and new comments (e.g. \u201Cany updates since yesterday?\u201D).",
|
|
2893
3050
|
{
|
|
2894
|
-
query:
|
|
2895
|
-
assignee:
|
|
2896
|
-
limit:
|
|
3051
|
+
query: z4.string().optional().describe("Search text (identifier, title, description). Omit to list by assignee only."),
|
|
3052
|
+
assignee: z4.string().optional().describe("me, unassigned, all, a Linear user id, or a display name. Default: all when query is set, otherwise me."),
|
|
3053
|
+
limit: z4.number().int().positive().max(250).optional().describe("Page size (default 40, max 250). Raise when truncated is true."),
|
|
2897
3054
|
updatedSince: mcpIssueUpdatedSinceSchema
|
|
2898
3055
|
},
|
|
2899
3056
|
async ({ query, assignee, limit, updatedSince }) => {
|
|
@@ -2932,8 +3089,8 @@ function registerLinearTools(server) {
|
|
|
2932
3089
|
);
|
|
2933
3090
|
server.tool(
|
|
2934
3091
|
"linear_get_issue",
|
|
2935
|
-
"Get a Linear issue by uuid or identifier (ENG-123): description, comments, relations, parent/children, attachments. Default crushes redundant comments and huge pasted bodies (SmartCrusher-style; small unique tickets pass through). Pass include=full for the uncompressed vendor payload. To save a file, call linear_download_attachment with the attachment id or url (writes `.context/attachments/`; do not dump bytes into the tool result).",
|
|
2936
|
-
{ id:
|
|
3092
|
+
"Get a Linear issue by uuid or identifier (ENG-123): description, comments, relations, parent/children, project, attachments. Default crushes redundant comments and huge pasted bodies (SmartCrusher-style; small unique tickets pass through). Pass include=full for the uncompressed vendor payload. To save a file, call linear_download_attachment with the attachment id or url (writes `.context/attachments/`; do not dump bytes into the tool result).",
|
|
3093
|
+
{ id: z4.string(), include: mcpIssueIncludeSchema },
|
|
2937
3094
|
async ({ id, include }) => {
|
|
2938
3095
|
try {
|
|
2939
3096
|
return text3(formatLinearIssuePayload(await getLinearIssue(id), include));
|
|
@@ -2946,9 +3103,9 @@ function registerLinearTools(server) {
|
|
|
2946
3103
|
"linear_download_attachment",
|
|
2947
3104
|
"Download a Linear file attachment into `.context/attachments/` using Account Linear auth. Pass the attachment uuid or uploads.linear.app URL from linear_get_issue. Returns the worktree-relative path \u2014 Read that file; do not dump bytes into chat.",
|
|
2948
3105
|
{
|
|
2949
|
-
id:
|
|
2950
|
-
name:
|
|
2951
|
-
repoPath:
|
|
3106
|
+
id: z4.string().describe("Attachment uuid or upload URL from linear_get_issue."),
|
|
3107
|
+
name: z4.string().optional().describe("Optional filename override."),
|
|
3108
|
+
repoPath: z4.string().optional().describe("Workspace / worktree path. Omit on a worktree turn (uses cwd).")
|
|
2952
3109
|
},
|
|
2953
3110
|
async ({ id, name, repoPath }) => {
|
|
2954
3111
|
try {
|
|
@@ -2962,13 +3119,13 @@ function registerLinearTools(server) {
|
|
|
2962
3119
|
"linear_create_issue",
|
|
2963
3120
|
'Create a Linear issue. Call linear_list_teams first. team is id/key/name; state is name/type/id; assignee is "me" or a user id. Pass parent (ENG-123 or uuid) to nest a spin-off under the current ticket.',
|
|
2964
3121
|
{
|
|
2965
|
-
team:
|
|
2966
|
-
title:
|
|
2967
|
-
description:
|
|
2968
|
-
state:
|
|
2969
|
-
assignee:
|
|
3122
|
+
team: z4.string(),
|
|
3123
|
+
title: z4.string(),
|
|
3124
|
+
description: z4.string().optional(),
|
|
3125
|
+
state: z4.string().optional(),
|
|
3126
|
+
assignee: z4.string().nullable().optional(),
|
|
2970
3127
|
priority: prioritySchema,
|
|
2971
|
-
parent:
|
|
3128
|
+
parent: z4.string().optional().describe("Parent issue uuid or identifier (ENG-123) for a spin-off / sub-issue.")
|
|
2972
3129
|
},
|
|
2973
3130
|
async (args) => {
|
|
2974
3131
|
try {
|
|
@@ -2978,26 +3135,26 @@ function registerLinearTools(server) {
|
|
|
2978
3135
|
}
|
|
2979
3136
|
}
|
|
2980
3137
|
);
|
|
2981
|
-
const relationInputSchema =
|
|
2982
|
-
type:
|
|
2983
|
-
issue:
|
|
3138
|
+
const relationInputSchema = z4.object({
|
|
3139
|
+
type: z4.string().describe("blocks, blockedBy, related, duplicate, or duplicateOf (this issue's view)."),
|
|
3140
|
+
issue: z4.string().describe("Related issue uuid or identifier (ENG-123).")
|
|
2984
3141
|
});
|
|
2985
3142
|
server.tool(
|
|
2986
3143
|
"linear_update_issue",
|
|
2987
3144
|
'Update a Linear issue (uuid or ENG-123). Pass title, description, state, assignee, priority, cycle, labels, project, parent, and/or relations. Cycle/project/parent: name or id, or "none" to clear. labels replaces all names (pass [] to clear; get the issue first to keep existing). relations adds; removeRelations removes by type + issue. When reviewing a PR or ticket, wait until they type a next step before changing the ticket \u2014 do not ask_user after the review. The author is notified.',
|
|
2988
3145
|
{
|
|
2989
|
-
id:
|
|
2990
|
-
title:
|
|
2991
|
-
description:
|
|
2992
|
-
state:
|
|
2993
|
-
assignee:
|
|
3146
|
+
id: z4.string(),
|
|
3147
|
+
title: z4.string().optional(),
|
|
3148
|
+
description: z4.string().optional(),
|
|
3149
|
+
state: z4.string().optional(),
|
|
3150
|
+
assignee: z4.string().nullable().optional(),
|
|
2994
3151
|
priority: prioritySchema,
|
|
2995
|
-
cycle:
|
|
2996
|
-
labels:
|
|
2997
|
-
project:
|
|
2998
|
-
parent:
|
|
2999
|
-
relations:
|
|
3000
|
-
removeRelations:
|
|
3152
|
+
cycle: z4.string().nullable().optional().describe('Cycle name, number, "current"/"active", or "none" to unschedule.'),
|
|
3153
|
+
labels: z4.array(z4.string()).optional().describe("Replace all labels (names or ids). Empty array clears. Call linear_get_issue first to keep existing."),
|
|
3154
|
+
project: z4.string().nullable().optional().describe('Project name, id, slug, or "none" to unassign.'),
|
|
3155
|
+
parent: z4.string().nullable().optional().describe('Parent issue uuid or identifier (ENG-123), or "none" to unset.'),
|
|
3156
|
+
relations: z4.array(relationInputSchema).optional().describe("Add relations from this issue's view (blocks, blockedBy, related, duplicate, duplicateOf)."),
|
|
3157
|
+
removeRelations: z4.array(relationInputSchema).optional().describe("Remove relations by type + related issue (uuid or ENG-123).")
|
|
3001
3158
|
},
|
|
3002
3159
|
async (args) => {
|
|
3003
3160
|
try {
|
|
@@ -3011,8 +3168,8 @@ function registerLinearTools(server) {
|
|
|
3011
3168
|
"linear_comment",
|
|
3012
3169
|
"Add a markdown comment on a Linear issue (uuid or ENG-123). When reviewing a PR or ticket, show the draft in chat and wait until they type a next step \u2014 do not ask_user after the review. The author sees this immediately.",
|
|
3013
3170
|
{
|
|
3014
|
-
id:
|
|
3015
|
-
body:
|
|
3171
|
+
id: z4.string(),
|
|
3172
|
+
body: z4.string()
|
|
3016
3173
|
},
|
|
3017
3174
|
async (args) => {
|
|
3018
3175
|
try {
|
|
@@ -3031,104 +3188,6 @@ function registerConnectedIssueVendorTools(server, settings = loadAppSettings())
|
|
|
3031
3188
|
if (isAbleTimeConnected(settings)) registerAbleTimeTools(server);
|
|
3032
3189
|
}
|
|
3033
3190
|
|
|
3034
|
-
// src/mcp/viewer-context-tools.ts
|
|
3035
|
-
import { z as z6 } from "zod";
|
|
3036
|
-
function text4(payload, isError = false) {
|
|
3037
|
-
return mcpJson(payload, isError);
|
|
3038
|
-
}
|
|
3039
|
-
function matchRegisteredWorkspace(repoPath) {
|
|
3040
|
-
const hint = (repoPath ?? "").trim().replace(/\/+$/, "");
|
|
3041
|
-
if (!hint) return null;
|
|
3042
|
-
let best = null;
|
|
3043
|
-
for (const w of listWorkspaces()) {
|
|
3044
|
-
const stored = w.path.replace(/\/+$/, "");
|
|
3045
|
-
if (stored === hint || hint.startsWith(`${stored}/`)) {
|
|
3046
|
-
if (!best || stored.length > best.length) best = stored;
|
|
3047
|
-
}
|
|
3048
|
-
}
|
|
3049
|
-
return best;
|
|
3050
|
-
}
|
|
3051
|
-
async function resolveProjectRepoPath(repoPath) {
|
|
3052
|
-
const hint = repoPath?.trim() || process.cwd();
|
|
3053
|
-
let root = hint;
|
|
3054
|
-
try {
|
|
3055
|
-
root = await resolveRepoRoot(hint);
|
|
3056
|
-
} catch {
|
|
3057
|
-
root = hint.replace(/\/+$/, "");
|
|
3058
|
-
}
|
|
3059
|
-
return matchRegisteredWorkspace(root) ?? matchRegisteredWorkspace(hint);
|
|
3060
|
-
}
|
|
3061
|
-
var PROJECT_PATH_HINT = "Pass repoPath from list_projects. Orchestration cwd is synthetic and empty \u2014 do not omit repoPath for project context.";
|
|
3062
|
-
function registerViewerContextTools(server) {
|
|
3063
|
-
server.tool(
|
|
3064
|
-
"get_viewer_context",
|
|
3065
|
-
"Read account and project user context (Settings \u2192 Agents / Projects): roles, tickets, and review queues as freeform text. Orchestration must pass repoPath from list_projects for project context. Worktree turns may omit repoPath (uses cwd).",
|
|
3066
|
-
{
|
|
3067
|
-
repoPath: z6.string().optional().describe(
|
|
3068
|
-
"Registered workspace or worktree path. Required from orchestration for a project. Omit on a worktree turn (uses cwd)."
|
|
3069
|
-
)
|
|
3070
|
-
},
|
|
3071
|
-
async ({ repoPath }) => {
|
|
3072
|
-
try {
|
|
3073
|
-
const path = await resolveProjectRepoPath(repoPath);
|
|
3074
|
-
return text4(readViewerContext(path ?? void 0));
|
|
3075
|
-
} catch (err) {
|
|
3076
|
-
return text4(
|
|
3077
|
-
{ error: err instanceof Error ? err.message : String(err) },
|
|
3078
|
-
true
|
|
3079
|
-
);
|
|
3080
|
-
}
|
|
3081
|
-
}
|
|
3082
|
-
);
|
|
3083
|
-
server.tool(
|
|
3084
|
-
"update_viewer_context",
|
|
3085
|
-
"Replace account or project user context (Settings \u2192 Agents / Projects). Never call until the user confirmed. Show the proposed text, ask_user (Save this context / Do not save), wait, then pass confirmed=true. Empty context clears that field.",
|
|
3086
|
-
{
|
|
3087
|
-
scope: z6.enum(["account", "project"]).describe("account = Settings \u2192 Agents; project = Settings \u2192 Projects for one repo"),
|
|
3088
|
-
context: z6.string().describe("Full replacement text (roles, tickets, review queues). Empty clears."),
|
|
3089
|
-
confirmed: z6.boolean().optional().describe("Must be true after the user confirmed via ask_user. False/omit is rejected."),
|
|
3090
|
-
repoPath: z6.string().optional().describe(
|
|
3091
|
-
"Required for scope=project from orchestration (path from list_projects). Worktree turns may omit it (cwd)."
|
|
3092
|
-
)
|
|
3093
|
-
},
|
|
3094
|
-
async ({ scope, context, confirmed, repoPath }) => {
|
|
3095
|
-
try {
|
|
3096
|
-
if (scope === "project") {
|
|
3097
|
-
const path = await resolveProjectRepoPath(repoPath);
|
|
3098
|
-
if (!path) {
|
|
3099
|
-
return text4(
|
|
3100
|
-
{
|
|
3101
|
-
ok: false,
|
|
3102
|
-
error: "repoPath required",
|
|
3103
|
-
message: PROJECT_PATH_HINT
|
|
3104
|
-
},
|
|
3105
|
-
true
|
|
3106
|
-
);
|
|
3107
|
-
}
|
|
3108
|
-
const result2 = writeViewerContext({
|
|
3109
|
-
scope,
|
|
3110
|
-
context,
|
|
3111
|
-
repoPath: path,
|
|
3112
|
-
confirmed: confirmed === true
|
|
3113
|
-
});
|
|
3114
|
-
return text4(result2, !result2.ok);
|
|
3115
|
-
}
|
|
3116
|
-
const result = writeViewerContext({
|
|
3117
|
-
scope,
|
|
3118
|
-
context,
|
|
3119
|
-
confirmed: confirmed === true
|
|
3120
|
-
});
|
|
3121
|
-
return text4(result, !result.ok);
|
|
3122
|
-
} catch (err) {
|
|
3123
|
-
return text4(
|
|
3124
|
-
{ error: err instanceof Error ? err.message : String(err) },
|
|
3125
|
-
true
|
|
3126
|
-
);
|
|
3127
|
-
}
|
|
3128
|
-
}
|
|
3129
|
-
);
|
|
3130
|
-
}
|
|
3131
|
-
|
|
3132
3191
|
// src/mcp/pr-list.ts
|
|
3133
3192
|
function compactPrRow(pr) {
|
|
3134
3193
|
return {
|
|
@@ -3164,12 +3223,12 @@ function formatMcpPrList(input) {
|
|
|
3164
3223
|
}
|
|
3165
3224
|
|
|
3166
3225
|
// src/mcp/schedule-tools.ts
|
|
3167
|
-
import { z as
|
|
3168
|
-
function
|
|
3226
|
+
import { z as z5 } from "zod";
|
|
3227
|
+
function text4(payload, isError = false) {
|
|
3169
3228
|
return mcpJson(payload, isError);
|
|
3170
3229
|
}
|
|
3171
3230
|
function fail4(err) {
|
|
3172
|
-
return
|
|
3231
|
+
return text4(
|
|
3173
3232
|
{ error: err instanceof Error ? err.message : String(err) },
|
|
3174
3233
|
true
|
|
3175
3234
|
);
|
|
@@ -3194,7 +3253,7 @@ function registerScheduleTools(server) {
|
|
|
3194
3253
|
{},
|
|
3195
3254
|
async () => {
|
|
3196
3255
|
try {
|
|
3197
|
-
return
|
|
3256
|
+
return text4({ schedules: listSchedules() });
|
|
3198
3257
|
} catch (err) {
|
|
3199
3258
|
return fail4(err);
|
|
3200
3259
|
}
|
|
@@ -3204,18 +3263,18 @@ function registerScheduleTools(server) {
|
|
|
3204
3263
|
"create_schedule",
|
|
3205
3264
|
"Create a local schedule that, when due, sends a prompt to an existing chat (chatId) or starts a new Global orchestration chat (omit chatId). Pass chatId=self to continue this chat. If that chat is missing or archived when the job fires, Sideboard starts a new Global chat (recurring jobs then continue that chat). Exactly one of at (ISO datetime), every (15m/1h/6h/1d), or cron (5-field). Recurring jobs without chatId open a new Global chat each run. Creating or enabling a future job turns on Settings \u2192 Advanced \u2192 Caffeinate while schedules are enabled so the Mac stays awake until due. Sideboard.app must be running for the job to fire.",
|
|
3206
3265
|
{
|
|
3207
|
-
prompt:
|
|
3208
|
-
name:
|
|
3209
|
-
at:
|
|
3210
|
-
every:
|
|
3211
|
-
cron:
|
|
3212
|
-
tz:
|
|
3213
|
-
chatId:
|
|
3266
|
+
prompt: z5.string().describe("User message / goal queued when the schedule fires"),
|
|
3267
|
+
name: z5.string().optional(),
|
|
3268
|
+
at: z5.string().optional().describe("ISO datetime for a one-shot"),
|
|
3269
|
+
every: z5.string().optional().describe("Interval such as 15m, 1h, 6h, 1d"),
|
|
3270
|
+
cron: z5.string().optional().describe("5-field cron expression"),
|
|
3271
|
+
tz: z5.string().optional().describe("IANA timezone for cron (default: system)"),
|
|
3272
|
+
chatId: z5.string().optional().describe(
|
|
3214
3273
|
'Existing chat id (orchestration or workspace), or "self" for this chat. Omit to create a new Global chat on fire.'
|
|
3215
3274
|
),
|
|
3216
|
-
threadId:
|
|
3217
|
-
agent:
|
|
3218
|
-
model:
|
|
3275
|
+
threadId: z5.string().optional().describe("Legacy alias for chatId."),
|
|
3276
|
+
agent: z5.enum(["claude", "cursor", "codex", "opencode"]).optional().describe("Agent for a new Global chat (omit for Account default)"),
|
|
3277
|
+
model: z5.string().optional()
|
|
3219
3278
|
},
|
|
3220
3279
|
async (args) => {
|
|
3221
3280
|
try {
|
|
@@ -3238,7 +3297,7 @@ function registerScheduleTools(server) {
|
|
|
3238
3297
|
model: args.model,
|
|
3239
3298
|
createdBy: "mcp"
|
|
3240
3299
|
});
|
|
3241
|
-
return
|
|
3300
|
+
return text4({
|
|
3242
3301
|
schedule,
|
|
3243
3302
|
hint: scheduleNeedsCaffeinate(schedule) ? "Fires while Sideboard.app is running. Recurring jobs without chatId create a new orchestration chat each run. Caffeinate while schedules are enabled is now on so the Mac can stay awake until this job is due." : "Fires while Sideboard.app is running. Recurring jobs without chatId create a new orchestration chat each run."
|
|
3244
3303
|
});
|
|
@@ -3251,20 +3310,20 @@ function registerScheduleTools(server) {
|
|
|
3251
3310
|
"update_schedule",
|
|
3252
3311
|
"Update a local schedule (prompt, cadence, target chat, enabled). Pass id from list_schedules.",
|
|
3253
3312
|
{
|
|
3254
|
-
id:
|
|
3255
|
-
prompt:
|
|
3256
|
-
name:
|
|
3257
|
-
at:
|
|
3258
|
-
every:
|
|
3259
|
-
cron:
|
|
3260
|
-
tz:
|
|
3261
|
-
chatId:
|
|
3313
|
+
id: z5.string(),
|
|
3314
|
+
prompt: z5.string().optional(),
|
|
3315
|
+
name: z5.string().optional(),
|
|
3316
|
+
at: z5.string().optional(),
|
|
3317
|
+
every: z5.string().optional(),
|
|
3318
|
+
cron: z5.string().optional(),
|
|
3319
|
+
tz: z5.string().optional(),
|
|
3320
|
+
chatId: z5.string().optional().describe(
|
|
3262
3321
|
'Existing chat (orchestration or workspace), "self", or empty string to create a new Global chat each run'
|
|
3263
3322
|
),
|
|
3264
|
-
threadId:
|
|
3265
|
-
agent:
|
|
3266
|
-
model:
|
|
3267
|
-
enabled:
|
|
3323
|
+
threadId: z5.string().optional().describe("Legacy alias for chatId."),
|
|
3324
|
+
agent: z5.enum(["claude", "cursor", "codex", "opencode"]).optional(),
|
|
3325
|
+
model: z5.string().optional(),
|
|
3326
|
+
enabled: z5.boolean().optional()
|
|
3268
3327
|
},
|
|
3269
3328
|
async (args) => {
|
|
3270
3329
|
try {
|
|
@@ -3296,7 +3355,7 @@ function registerScheduleTools(server) {
|
|
|
3296
3355
|
enabled: args.enabled,
|
|
3297
3356
|
model: args.model
|
|
3298
3357
|
});
|
|
3299
|
-
return
|
|
3358
|
+
return text4({ schedule });
|
|
3300
3359
|
} catch (err) {
|
|
3301
3360
|
return fail4(err);
|
|
3302
3361
|
}
|
|
@@ -3305,11 +3364,11 @@ function registerScheduleTools(server) {
|
|
|
3305
3364
|
server.tool(
|
|
3306
3365
|
"delete_schedule",
|
|
3307
3366
|
"Delete a local schedule. Pass id from list_schedules.",
|
|
3308
|
-
{ id:
|
|
3367
|
+
{ id: z5.string() },
|
|
3309
3368
|
async ({ id }) => {
|
|
3310
3369
|
try {
|
|
3311
3370
|
deleteSchedule(id);
|
|
3312
|
-
return
|
|
3371
|
+
return text4({ ok: true, id });
|
|
3313
3372
|
} catch (err) {
|
|
3314
3373
|
return fail4(err);
|
|
3315
3374
|
}
|
|
@@ -3318,14 +3377,14 @@ function registerScheduleTools(server) {
|
|
|
3318
3377
|
server.tool(
|
|
3319
3378
|
"run_schedule",
|
|
3320
3379
|
"Fire a schedule now (does not wait for nextRunAt). Queues the prompt on the target chat or starts a new Global chat. If Sideboard.app is running, the desktop drains the turn.",
|
|
3321
|
-
{ id:
|
|
3380
|
+
{ id: z5.string() },
|
|
3322
3381
|
async ({ id }) => {
|
|
3323
3382
|
try {
|
|
3324
3383
|
if (!getSchedule(id)) {
|
|
3325
3384
|
return fail4(new Error(`Schedule not found: ${id}`));
|
|
3326
3385
|
}
|
|
3327
3386
|
const schedule = await fireSchedule(id);
|
|
3328
|
-
return
|
|
3387
|
+
return text4({ schedule });
|
|
3329
3388
|
} catch (err) {
|
|
3330
3389
|
return fail4(err);
|
|
3331
3390
|
}
|
|
@@ -3590,9 +3649,9 @@ async function getGitHubStatus() {
|
|
|
3590
3649
|
};
|
|
3591
3650
|
}
|
|
3592
3651
|
const status = await run("gh", ["auth", "status"], { reject: false });
|
|
3593
|
-
const
|
|
3652
|
+
const text5 = `${status.stdout}
|
|
3594
3653
|
${status.stderr}`;
|
|
3595
|
-
const loginMatch =
|
|
3654
|
+
const loginMatch = text5.match(/Logged in to ([^\s]+) account (\S+)/i) ?? text5.match(/Logged in to ([^\s]+) as (\S+)/i);
|
|
3596
3655
|
if (loginMatch) {
|
|
3597
3656
|
return {
|
|
3598
3657
|
connected: true,
|
|
@@ -3609,154 +3668,7 @@ ${status.stderr}`;
|
|
|
3609
3668
|
};
|
|
3610
3669
|
}
|
|
3611
3670
|
|
|
3612
|
-
// src/mcp/create-thread-agent.ts
|
|
3613
|
-
function resolveOrchCreateThreadOptions(input) {
|
|
3614
|
-
const { resolveNewThreadOptions } = input;
|
|
3615
|
-
const account = resolveNewThreadOptions({});
|
|
3616
|
-
const requested = input.requestedAgent;
|
|
3617
|
-
const parent = input.parentAgent ?? void 0;
|
|
3618
|
-
const cursorReady = input.cursorReady ?? isCursorApiKeyConfigured();
|
|
3619
|
-
let agent = account.agent;
|
|
3620
|
-
let ignoredAgent;
|
|
3621
|
-
let honorRequestedModel = false;
|
|
3622
|
-
if (requested) {
|
|
3623
|
-
const parentEcho = Boolean(parent) && requested === parent && requested !== account.agent;
|
|
3624
|
-
const cursorAutofill = requested === "cursor" && account.agent !== "cursor";
|
|
3625
|
-
if (parentEcho || cursorAutofill) {
|
|
3626
|
-
ignoredAgent = requested;
|
|
3627
|
-
} else {
|
|
3628
|
-
agent = requested;
|
|
3629
|
-
honorRequestedModel = input.requestedModel !== void 0;
|
|
3630
|
-
}
|
|
3631
|
-
}
|
|
3632
|
-
let coercedFrom;
|
|
3633
|
-
if (agent === "codex" && parent === "codex" && cursorReady) {
|
|
3634
|
-
coercedFrom = requested ?? "codex";
|
|
3635
|
-
agent = "cursor";
|
|
3636
|
-
}
|
|
3637
|
-
const opts = resolveNewThreadOptions({
|
|
3638
|
-
agent,
|
|
3639
|
-
model: honorRequestedModel ? input.requestedModel : void 0
|
|
3640
|
-
});
|
|
3641
|
-
return {
|
|
3642
|
-
...opts,
|
|
3643
|
-
...ignoredAgent ? { ignoredAgent } : {},
|
|
3644
|
-
...coercedFrom ? { coercedFrom } : {}
|
|
3645
|
-
};
|
|
3646
|
-
}
|
|
3647
|
-
function orchCreateThreadAgentNote(input) {
|
|
3648
|
-
if (input.coercedFrom) {
|
|
3649
|
-
return `Avoid nested Codex under a Codex orchestrator \u2014 used agent=${input.agent}`;
|
|
3650
|
-
}
|
|
3651
|
-
if (input.ignoredAgent) {
|
|
3652
|
-
return `Ignored agent=${input.ignoredAgent} \u2014 used Account default agent=${input.agent}`;
|
|
3653
|
-
}
|
|
3654
|
-
return void 0;
|
|
3655
|
-
}
|
|
3656
|
-
|
|
3657
3671
|
// src/mcp/server.ts
|
|
3658
|
-
var MAX_ORCH_THREADS = 5;
|
|
3659
|
-
var CREATE_THREAD_TIMEOUT_MS = 9e4;
|
|
3660
|
-
function withTimeout(promise, ms, label) {
|
|
3661
|
-
return new Promise((resolve, reject) => {
|
|
3662
|
-
const timer = setTimeout(() => {
|
|
3663
|
-
reject(new Error(`${label} timed out after ${Math.round(ms / 1e3)}s`));
|
|
3664
|
-
}, ms);
|
|
3665
|
-
promise.then(
|
|
3666
|
-
(value) => {
|
|
3667
|
-
clearTimeout(timer);
|
|
3668
|
-
resolve(value);
|
|
3669
|
-
},
|
|
3670
|
-
(err) => {
|
|
3671
|
-
clearTimeout(timer);
|
|
3672
|
-
reject(err);
|
|
3673
|
-
}
|
|
3674
|
-
);
|
|
3675
|
-
});
|
|
3676
|
-
}
|
|
3677
|
-
async function createOrchChildThread(orch, args, resolveNewThreadOptions) {
|
|
3678
|
-
const envParentId = process.env.SIDEBOARD_ORCHESTRATOR_THREAD_ID?.trim() || "";
|
|
3679
|
-
let parentId = args.parentChatId?.trim() || args.parentThreadId?.trim() || "";
|
|
3680
|
-
let parent = parentId ? orch.getThread(parentId) : null;
|
|
3681
|
-
let parentCorrectedFrom;
|
|
3682
|
-
if (envParentId) {
|
|
3683
|
-
const envParent = orch.getThread(envParentId);
|
|
3684
|
-
if (envParent) {
|
|
3685
|
-
if (parentId && parentId !== envParentId) parentCorrectedFrom = parentId;
|
|
3686
|
-
else if (!parentId) parentCorrectedFrom = void 0;
|
|
3687
|
-
parentId = envParentId;
|
|
3688
|
-
parent = envParent;
|
|
3689
|
-
}
|
|
3690
|
-
}
|
|
3691
|
-
if (parentId && !parent) {
|
|
3692
|
-
parentCorrectedFrom = parentId;
|
|
3693
|
-
parentId = "";
|
|
3694
|
-
parent = null;
|
|
3695
|
-
}
|
|
3696
|
-
if (parentId) {
|
|
3697
|
-
const children = orch.getThreads(false).filter((t) => t.parentThreadId === parentId);
|
|
3698
|
-
if (children.length >= MAX_ORCH_THREADS) {
|
|
3699
|
-
return {
|
|
3700
|
-
ok: false,
|
|
3701
|
-
text: `Thread-creation cap (${MAX_ORCH_THREADS}) reached for this orchestration session`
|
|
3702
|
-
};
|
|
3703
|
-
}
|
|
3704
|
-
}
|
|
3705
|
-
const opts = resolveOrchCreateThreadOptions({
|
|
3706
|
-
requestedAgent: args.agent,
|
|
3707
|
-
requestedModel: args.model,
|
|
3708
|
-
parentAgent: parent?.agent,
|
|
3709
|
-
resolveNewThreadOptions
|
|
3710
|
-
});
|
|
3711
|
-
const agentNote = orchCreateThreadAgentNote(opts);
|
|
3712
|
-
try {
|
|
3713
|
-
const priorIds = new Set(orch.getThreads(false).map((t) => t.id));
|
|
3714
|
-
const thread = await withTimeout(
|
|
3715
|
-
orch.createThread({
|
|
3716
|
-
sourceType: args.sourceType,
|
|
3717
|
-
sourceRef: args.sourceRef,
|
|
3718
|
-
agent: opts.agent,
|
|
3719
|
-
model: opts.model,
|
|
3720
|
-
effort: opts.effort,
|
|
3721
|
-
fast: opts.fast,
|
|
3722
|
-
repoPath: args.repoPath,
|
|
3723
|
-
title: args.title,
|
|
3724
|
-
parentThreadId: parentId || null,
|
|
3725
|
-
cowboy: args.cowboy || void 0,
|
|
3726
|
-
attachments: args.attachments
|
|
3727
|
-
}),
|
|
3728
|
-
CREATE_THREAD_TIMEOUT_MS,
|
|
3729
|
-
"create_workspace"
|
|
3730
|
-
);
|
|
3731
|
-
const alreadyStarted = priorIds.has(thread.id);
|
|
3732
|
-
const parentNote = parentCorrectedFrom ? parentId ? `Ignored unknown/stale parentThreadId ${parentCorrectedFrom}; nested under ${parentId}` : `Ignored unknown/stale parentThreadId ${parentCorrectedFrom}; created without parent` : void 0;
|
|
3733
|
-
return {
|
|
3734
|
-
ok: true,
|
|
3735
|
-
text: JSON.stringify({
|
|
3736
|
-
id: thread.id,
|
|
3737
|
-
title: thread.title,
|
|
3738
|
-
branchName: thread.branchName,
|
|
3739
|
-
worktreePath: thread.worktreePath,
|
|
3740
|
-
agent: thread.agent,
|
|
3741
|
-
model: thread.model,
|
|
3742
|
-
status: thread.status,
|
|
3743
|
-
cowboy: Boolean(thread.cowboy),
|
|
3744
|
-
link: `sideboard://chat/${thread.id}`,
|
|
3745
|
-
parentChatId: thread.parentThreadId,
|
|
3746
|
-
parentThreadId: thread.parentThreadId,
|
|
3747
|
-
chats: workspaceAgentChatSummaries(thread.worktreePath),
|
|
3748
|
-
...alreadyStarted ? { alreadyStarted: true } : {},
|
|
3749
|
-
...opts.coercedFrom ? { agentCoercedFrom: opts.coercedFrom } : {},
|
|
3750
|
-
...opts.ignoredAgent ? { agentIgnored: opts.ignoredAgent } : {},
|
|
3751
|
-
...agentNote ? { note: agentNote } : {},
|
|
3752
|
-
...parentCorrectedFrom ? { parentCorrectedFrom, parentNote } : {}
|
|
3753
|
-
})
|
|
3754
|
-
};
|
|
3755
|
-
} catch (err) {
|
|
3756
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
3757
|
-
return { ok: false, text: `create_workspace failed: ${message}` };
|
|
3758
|
-
}
|
|
3759
|
-
}
|
|
3760
3672
|
function previewHint(preview) {
|
|
3761
3673
|
return preview === "window" ? "Electron: the native window is the app. url is the renderer/HMR origin only \u2014 do not open it in a browser (no preload/IPC)." : "Open url in a browser, curl, or Playwright to use the app.";
|
|
3762
3674
|
}
|
|
@@ -3854,16 +3766,16 @@ async function startMcpServer() {
|
|
|
3854
3766
|
"list_board",
|
|
3855
3767
|
"Home Kanban of workspaces (New, Draft, Review, Merged) \u2014 one card per checkout; sibling chats on that card are separate agents. Same cards as desktop Home. Path to merge: no PR \u2192 draft PR \u2192 open PR \u2192 merged. Archive removes the card to Settings \u2192 History. Queued/running are activity on the card, not columns. Orchestration chats are not on the board. Filters: query, repoPath, kind (ticket/PR/branch source), ownership (mine = your PRs / WIP; reviewing = someone else's PR), column, limit (default 40). create_workspace adds a workspace (and a Home card), or returns the live checkout if that ticket/PR/named branch is already checked out. That reuse is one checkout, not one agent \u2014 fork_chat to add another.",
|
|
3856
3768
|
{
|
|
3857
|
-
query:
|
|
3858
|
-
repoPath:
|
|
3859
|
-
kind:
|
|
3860
|
-
ownership:
|
|
3769
|
+
query: z6.string().optional().describe("Case-insensitive token search across title, id, labels, workspace tags, repo"),
|
|
3770
|
+
repoPath: z6.string().optional().describe("Limit to one project path from list_projects"),
|
|
3771
|
+
kind: z6.enum(["all", "tickets", "prs", "branches", "threads"]).optional().describe("Filter by worktree source (default all)"),
|
|
3772
|
+
ownership: z6.enum(["all", "mine", "reviewing"]).optional().describe(
|
|
3861
3773
|
"Mine = PRs you authored or local WIP; reviewing = someone else's PR. Uses the gh login."
|
|
3862
3774
|
),
|
|
3863
|
-
column:
|
|
3775
|
+
column: z6.enum(["new", "draft", "review", "done", "needs_you"]).optional().describe(
|
|
3864
3776
|
"Return cards for this column only (totals still include the rest). needs_you is a legacy alias for new."
|
|
3865
3777
|
),
|
|
3866
|
-
limit:
|
|
3778
|
+
limit: z6.number().int().positive().optional().describe("Max cards per column (default 40). hidden counts the remainder.")
|
|
3867
3779
|
},
|
|
3868
3780
|
async ({ query, repoPath, kind, ownership, column, limit }) => {
|
|
3869
3781
|
const workspaces = orch.listWorkspaces();
|
|
@@ -3893,7 +3805,7 @@ async function startMcpServer() {
|
|
|
3893
3805
|
server.tool(
|
|
3894
3806
|
"get_chat",
|
|
3895
3807
|
"Get a compact chat summary by id/ref. Includes last message preview, parentChatId, and child workspace chats (status + lastText). While running, includes progress (last tool/thinking) and lastActivityAt. Includes usage (billed token + costUsd totals when providers reported cost) and lastTurnUsage. blockedReason is why that agent is waiting on a person (question, plan approval, or a reported block).",
|
|
3896
|
-
{ ref:
|
|
3808
|
+
{ ref: z6.string() },
|
|
3897
3809
|
async ({ ref }) => {
|
|
3898
3810
|
const t = orch.getThread(ref);
|
|
3899
3811
|
if (!t) {
|
|
@@ -3915,6 +3827,7 @@ async function startMcpServer() {
|
|
|
3915
3827
|
sessionId: t.sessionId,
|
|
3916
3828
|
parentChatId: t.parentThreadId,
|
|
3917
3829
|
parentThreadId: t.parentThreadId,
|
|
3830
|
+
tags: t.tags ?? [],
|
|
3918
3831
|
children: childThreadRefs(t.id, orch.getThreads(false)),
|
|
3919
3832
|
queueLength: t.queue.length,
|
|
3920
3833
|
messageCount: t.messages.length,
|
|
@@ -3939,17 +3852,17 @@ async function startMcpServer() {
|
|
|
3939
3852
|
"present_artifact",
|
|
3940
3853
|
"Show a document or live log in Sideboard\u2019s side column. For html/svg/markdown/react, pass the FULL document and do not also fence that same body in chat. For type=log, pass only NEW lines (same artifact_id appends). Prefer type=log for long-running job output \u2014 do not resend HTML. type=react is a single default-export component (JSX/TSX); only react/react-dom imports.",
|
|
3941
3854
|
{
|
|
3942
|
-
title:
|
|
3943
|
-
type:
|
|
3855
|
+
title: z6.string().describe("Short title shown in the artifact pane header"),
|
|
3856
|
+
type: z6.enum(["html", "svg", "markdown", "react", "log"]).describe(
|
|
3944
3857
|
"html/svg/markdown/react replace the pane. log appends content to the same artifact_id (new lines only)."
|
|
3945
3858
|
),
|
|
3946
|
-
content:
|
|
3859
|
+
content: z6.string().describe(
|
|
3947
3860
|
"html/svg/markdown/react: full document. log: only the new lines since the last call (empty is ok for a status-only update)."
|
|
3948
3861
|
),
|
|
3949
|
-
artifact_id:
|
|
3950
|
-
status:
|
|
3951
|
-
phase:
|
|
3952
|
-
mode:
|
|
3862
|
+
artifact_id: z6.string().optional().describe("Stable id. Required for type=log so later calls append to the same pane."),
|
|
3863
|
+
status: z6.enum(["running", "ok", "failed", "idle"]).optional().describe("Log header pill: running (working), ok (done), failed, idle"),
|
|
3864
|
+
phase: z6.string().optional().describe("Log subtitle (Signing, Notarizing, \u2026)"),
|
|
3865
|
+
mode: z6.enum(["append", "replace"]).optional().describe("log only: append (default) or replace the buffer")
|
|
3953
3866
|
},
|
|
3954
3867
|
async ({ title, type, artifact_id, status, phase, mode }) => {
|
|
3955
3868
|
return mcpJson(
|
|
@@ -3962,15 +3875,15 @@ async function startMcpServer() {
|
|
|
3962
3875
|
"ask_user",
|
|
3963
3876
|
"Ask the user a clarifying multiple-choice question in Sideboard\u2019s composer. Call only when work is blocked on choosing among a few concrete options (approach fork, which API, auth vs cookies). Do not call for greetings, check-ins, \u201Chello\u201D, open-ended how-can-I-help, or to invent a menu of possible next tasks \u2014 reply in chat instead. If one option is the obvious default, proceed without asking. Before calling, write a short chat message explaining the decision and what each option means. Include a description on every option. After calling, stop and wait for answers. Not for \u201Cis the plan ready?\u201D.",
|
|
3964
3877
|
{
|
|
3965
|
-
questions:
|
|
3966
|
-
|
|
3967
|
-
question:
|
|
3968
|
-
header:
|
|
3969
|
-
multiSelect:
|
|
3970
|
-
options:
|
|
3971
|
-
|
|
3972
|
-
label:
|
|
3973
|
-
description:
|
|
3878
|
+
questions: z6.array(
|
|
3879
|
+
z6.object({
|
|
3880
|
+
question: z6.string().describe("Full question text ending with ?"),
|
|
3881
|
+
header: z6.string().max(24).optional().describe("Short label shown above the question"),
|
|
3882
|
+
multiSelect: z6.boolean().optional().describe("Allow selecting multiple options"),
|
|
3883
|
+
options: z6.array(
|
|
3884
|
+
z6.object({
|
|
3885
|
+
label: z6.string(),
|
|
3886
|
+
description: z6.string().optional().describe("What this option means / when to choose it (strongly preferred)")
|
|
3974
3887
|
})
|
|
3975
3888
|
).min(2).max(6).describe("2\u20136 choices (Sideboard also offers Other)")
|
|
3976
3889
|
})
|
|
@@ -3996,9 +3909,9 @@ async function startMcpServer() {
|
|
|
3996
3909
|
"present_plan",
|
|
3997
3910
|
PRESENT_PLAN_TOOL_DESCRIPTION,
|
|
3998
3911
|
{
|
|
3999
|
-
title:
|
|
4000
|
-
content:
|
|
4001
|
-
thread_id:
|
|
3912
|
+
title: z6.string().optional().describe("Short plan title (defaults to Plan)"),
|
|
3913
|
+
content: z6.string().min(1).describe("Full plan markdown (headings, steps, risks, open questions)"),
|
|
3914
|
+
thread_id: z6.string().optional().describe("Sideboard thread id when cwd is not the worktree")
|
|
4002
3915
|
},
|
|
4003
3916
|
async ({ title, content, thread_id }) => {
|
|
4004
3917
|
const { writePlanFile } = await import("../plan-file-VAHV5WBB.js");
|
|
@@ -4027,15 +3940,15 @@ async function startMcpServer() {
|
|
|
4027
3940
|
"present_schema",
|
|
4028
3941
|
"Open Sideboard\u2019s schema-driven side column (filterable table and/or form) when the user needs to filter, edit, publish, or persist records. Do not call this just to re-display rows you already wrote as a markdown table. If the user asks for an editable / interactive table, call this even if chat already showed those rows. 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.",
|
|
4029
3942
|
{
|
|
4030
|
-
title:
|
|
4031
|
-
mode:
|
|
4032
|
-
datasource:
|
|
4033
|
-
resource_id:
|
|
4034
|
-
record_id:
|
|
4035
|
-
resource:
|
|
4036
|
-
record:
|
|
4037
|
-
records:
|
|
4038
|
-
pane_id:
|
|
3943
|
+
title: z6.string().describe("Pane title"),
|
|
3944
|
+
mode: z6.enum(["table", "form"]).optional().describe("table = list/filter records; form = edit one record"),
|
|
3945
|
+
datasource: z6.enum(["brightsy", "inline"]).optional().describe("brightsy resolves via login; inline uses embedded resource/records"),
|
|
3946
|
+
resource_id: z6.string().optional().describe("Brightsy record type UUID (or generic resource id)"),
|
|
3947
|
+
record_id: z6.string().optional().describe("Record id when opening form mode"),
|
|
3948
|
+
resource: z6.record(z6.unknown()).optional().describe("Inline { id, title, schema, schemaUi?, slug? }"),
|
|
3949
|
+
record: z6.record(z6.unknown()).optional().describe("Inline record { id, data, published_at? }"),
|
|
3950
|
+
records: z6.array(z6.record(z6.unknown())).optional().describe("Inline records for table mode"),
|
|
3951
|
+
pane_id: z6.string().optional().describe("Stable pane id across updates")
|
|
4039
3952
|
},
|
|
4040
3953
|
async (args) => {
|
|
4041
3954
|
const id = args.pane_id?.trim() || `schema_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 8)}`;
|
|
@@ -4056,10 +3969,10 @@ async function startMcpServer() {
|
|
|
4056
3969
|
"present_files",
|
|
4057
3970
|
"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.",
|
|
4058
3971
|
{
|
|
4059
|
-
title:
|
|
4060
|
-
datasource:
|
|
4061
|
-
path:
|
|
4062
|
-
pane_id:
|
|
3972
|
+
title: z6.string().optional().describe("Pane title (default: Files)"),
|
|
3973
|
+
datasource: z6.enum(["brightsy", "memory"]).optional().describe("brightsy = account storage via login; memory = session demo store"),
|
|
3974
|
+
path: z6.string().optional().describe("Initial folder path (e.g. public)"),
|
|
3975
|
+
pane_id: z6.string().optional().describe("Stable pane id across updates")
|
|
4063
3976
|
},
|
|
4064
3977
|
async (args) => {
|
|
4065
3978
|
const id = args.pane_id?.trim() || `files_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 8)}`;
|
|
@@ -4079,8 +3992,8 @@ async function startMcpServer() {
|
|
|
4079
3992
|
"wait_for_job",
|
|
4080
3993
|
"Wait on a detached long job (tests, pack, deploy, connector CLI) started with detached-job.cjs. MCP clients kill tools around 60s, so this returns within 45s. Sideboard opens a type=log pane from this result (artifact_id = job id, content = delta). stillRunning is the source of truth \u2014 if true, call wait_for_job again. If the job is hanging, producing no useful output, or doing the wrong thing, call stop_job instead of looping forever. Do not end the turn or tell the user you will let them know later. If false, ok/failed is the result.",
|
|
4081
3994
|
{
|
|
4082
|
-
id:
|
|
4083
|
-
timeoutMs:
|
|
3995
|
+
id: z6.string().describe("Detached job id (same kebab-case id passed to detached-job.cjs start)"),
|
|
3996
|
+
timeoutMs: z6.number().optional()
|
|
4084
3997
|
},
|
|
4085
3998
|
async ({ id, timeoutMs }) => {
|
|
4086
3999
|
const result = await waitForDetachedJob(process.cwd(), id, {
|
|
@@ -4093,8 +4006,8 @@ async function startMcpServer() {
|
|
|
4093
4006
|
"stop_job",
|
|
4094
4007
|
"Stop a detached job you started with detached-job.cjs when it is hanging, producing no useful output, buffering forever, or doing the wrong thing (wrong project, infinite watch, huge dump). Do not stop a pack/test/deploy that is clearly making progress. The type=log pane updates to status=failed with the last delta. Then decide the next step.",
|
|
4095
4008
|
{
|
|
4096
|
-
id:
|
|
4097
|
-
reason:
|
|
4009
|
+
id: z6.string().describe("Detached job id (same kebab-case id passed to detached-job.cjs start)"),
|
|
4010
|
+
reason: z6.string().optional().describe("Why you are stopping (hanging, no progress, wrong output, already have the answer)")
|
|
4098
4011
|
},
|
|
4099
4012
|
async ({ id, reason }) => {
|
|
4100
4013
|
const result = await stopDetachedJob(process.cwd(), id, { reason });
|
|
@@ -4106,7 +4019,7 @@ async function startMcpServer() {
|
|
|
4106
4019
|
"list_run_scripts",
|
|
4107
4020
|
"List .sideboard/.conductor run scripts for this thread (same menu as the desktop Dev button) and which are active. Each script and active run includes preview: url (open http://localhost:<SIDEBOARD_PORT>) or window (Electron: native window is the app; url is renderer/HMR only). Never guess :3000. Worktree turns may omit ref (uses cwd).",
|
|
4108
4021
|
{
|
|
4109
|
-
ref:
|
|
4022
|
+
ref: z6.string().optional().describe("Thread id. Omit on a worktree turn (uses cwd).")
|
|
4110
4023
|
},
|
|
4111
4024
|
async ({ ref }) => {
|
|
4112
4025
|
try {
|
|
@@ -4136,8 +4049,8 @@ async function startMcpServer() {
|
|
|
4136
4049
|
"run_dev_script",
|
|
4137
4050
|
"Start a .sideboard/.conductor run script (same as the desktop Dev / play button). Several named scripts can run at once; each gets its own port, and starting one does not stop the others. Logs and Stop appear in the thread UI \u2014 do not detached-job or shell-spawn the same command. Returns {port, ports, url, preview}. preview=url \u2192 open url (browser, curl, Playwright). preview=window \u2192 Electron: the native window is the app; url is renderer/HMR only (no preload/IPC in a browser). The port is allocated SIDEBOARD_PORT, not 3000. Worktree turns may omit ref (uses cwd). Omit name for the default script.",
|
|
4138
4051
|
{
|
|
4139
|
-
ref:
|
|
4140
|
-
name:
|
|
4052
|
+
ref: z6.string().optional().describe("Thread id. Omit on a worktree turn (uses cwd)."),
|
|
4053
|
+
name: z6.string().optional().describe("Script name from list_run_scripts (default script if omitted).")
|
|
4141
4054
|
},
|
|
4142
4055
|
async ({ ref, name }) => {
|
|
4143
4056
|
try {
|
|
@@ -4165,8 +4078,8 @@ async function startMcpServer() {
|
|
|
4165
4078
|
"stop_dev_script",
|
|
4166
4079
|
"Stop a run script started with run_dev_script or the desktop Dev button (same as UI Stop). Omit name to stop all scripts on this worktree. Worktree turns may omit ref (uses cwd).",
|
|
4167
4080
|
{
|
|
4168
|
-
ref:
|
|
4169
|
-
name:
|
|
4081
|
+
ref: z6.string().optional().describe("Thread id. Omit on a worktree turn (uses cwd)."),
|
|
4082
|
+
name: z6.string().optional().describe("Script name to stop; omit to stop all active scripts.")
|
|
4170
4083
|
},
|
|
4171
4084
|
async ({ ref, name }) => {
|
|
4172
4085
|
try {
|
|
@@ -4183,9 +4096,9 @@ async function startMcpServer() {
|
|
|
4183
4096
|
"get_run_log",
|
|
4184
4097
|
"Read the Run-tab terminal for a worktree script (same stdout/stderr as the desktop Dev pane). Default is a tail \u2014 raise `tail` only if the error is missing. Worktree turns may omit ref (uses cwd). Omit name for the default script.",
|
|
4185
4098
|
{
|
|
4186
|
-
ref:
|
|
4187
|
-
name:
|
|
4188
|
-
tail:
|
|
4099
|
+
ref: z6.string().optional().describe("Thread id. Omit on a worktree turn (uses cwd)."),
|
|
4100
|
+
name: z6.string().optional().describe("Script name from list_run_scripts (default script if omitted)."),
|
|
4101
|
+
tail: z6.number().int().min(1).max(MAX_RUN_LOG_CHARS).optional().describe(
|
|
4189
4102
|
`Max characters to return from the end (default ${DEFAULT_RUN_LOG_TAIL_CHARS}). Do not request the full buffer unless the tail is truncated and you still need more.`
|
|
4190
4103
|
)
|
|
4191
4104
|
},
|
|
@@ -4223,8 +4136,8 @@ async function startMcpServer() {
|
|
|
4223
4136
|
"notify_orchestrator",
|
|
4224
4137
|
"Wake the parent Global orchestrator with a short status so it does not have to poll wait_for_turn. Use when you are blocked and the parent may have moved on (missing access, waiting on something the coordinator must handle). The message is shown on the sidebar and board until the next user message in this chat. ask_user already notifies for input-required \u2014 do not call this for the same question. Information only: never a git command. Do not use this to steer other worktrees.",
|
|
4225
4138
|
{
|
|
4226
|
-
message:
|
|
4227
|
-
reason:
|
|
4139
|
+
message: z6.string().min(1).max(1500).describe("Why you are blocked / what the parent should know (short)."),
|
|
4140
|
+
reason: z6.enum(["input-required", "blocked"]).optional().describe(
|
|
4228
4141
|
"input-required = waiting on the user in this chat; blocked = need the coordinator (default)."
|
|
4229
4142
|
)
|
|
4230
4143
|
},
|
|
@@ -4250,13 +4163,45 @@ async function startMcpServer() {
|
|
|
4250
4163
|
...result.deduped ? { deduped: true } : {}
|
|
4251
4164
|
});
|
|
4252
4165
|
} catch (err) {
|
|
4253
|
-
const
|
|
4254
|
-
return { content: [{ type: "text", text:
|
|
4166
|
+
const text5 = err instanceof Error ? err.message : String(err);
|
|
4167
|
+
return { content: [{ type: "text", text: text5 }], isError: true };
|
|
4255
4168
|
}
|
|
4256
4169
|
}
|
|
4257
4170
|
);
|
|
4258
4171
|
}
|
|
4259
4172
|
registerScheduleTools(server);
|
|
4173
|
+
server.tool(
|
|
4174
|
+
"set_workspace_tags",
|
|
4175
|
+
worktreeProfile ? "Update tags on this workspace (the checkout you are in). Omit ref to use this chat. mode=replace (default), add, or remove. Short labels \u2014 the same ones the orchestrator used for this task. You cannot retag a different checkout." : "Label a project workspace so related checkouts stay recognizable in the sidebar and on the phone. Pass any chat id on that workspace. Use the same tags on every workspace from one task. mode=replace is the default; add and remove change the set. create_workspace tags= sets them at create time. Not for orchestration chats.",
|
|
4176
|
+
{
|
|
4177
|
+
ref: z6.string().optional().describe(
|
|
4178
|
+
worktreeProfile ? "Omit for this chat. Another chat id only if it is on this same checkout." : "Chat id on the workspace to label."
|
|
4179
|
+
),
|
|
4180
|
+
tags: z6.union([z6.string(), z6.array(z6.string())]).describe("Labels to set. A string may be comma-separated."),
|
|
4181
|
+
mode: z6.enum(["replace", "add", "remove"]).optional()
|
|
4182
|
+
},
|
|
4183
|
+
async ({ ref, tags, mode }) => {
|
|
4184
|
+
try {
|
|
4185
|
+
const callerId = process.env[SIDEBOARD_THREAD_ID_ENV]?.trim() || "";
|
|
4186
|
+
const target = workspaceTagTargetRef({
|
|
4187
|
+
ownWorktreeOnly: worktreeProfile,
|
|
4188
|
+
caller: callerId ? orch.getThread(callerId) : null,
|
|
4189
|
+
requestedRef: ref,
|
|
4190
|
+
find: (id) => orch.getThread(id)
|
|
4191
|
+
});
|
|
4192
|
+
const thread = applyWorkspaceTags(target, tags, mode ?? "replace");
|
|
4193
|
+
return mcpJson({
|
|
4194
|
+
id: thread.id,
|
|
4195
|
+
worktreePath: thread.worktreePath,
|
|
4196
|
+
tags: thread.tags ?? [],
|
|
4197
|
+
chats: workspaceAgentChatSummaries(thread.worktreePath)
|
|
4198
|
+
});
|
|
4199
|
+
} catch (err) {
|
|
4200
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
4201
|
+
return mcpJson({ ok: false, message }, true);
|
|
4202
|
+
}
|
|
4203
|
+
}
|
|
4204
|
+
);
|
|
4260
4205
|
if (!worktreeProfile) {
|
|
4261
4206
|
registerConnectedIssueVendorTools(server);
|
|
4262
4207
|
const { getCaffeinateHold, setCaffeinateHold } = await import("../caffeinate-hold-7JM5H5P3.js");
|
|
@@ -4267,7 +4212,7 @@ async function startMcpServer() {
|
|
|
4267
4212
|
"set_caffeinate",
|
|
4268
4213
|
"Keep this Mac awake with caffeinate (like Claude Code) across turns \u2014 independent of Settings toggles. Turn ON when the user will be away from the keyboard, is driving work from Slack, or asks you to keep the machine awake. Turn OFF when they say they are done, wrapping up, going to sleep, or no longer need the Mac awake. Closing or archiving this orchestration chat also releases the hold. macOS only.",
|
|
4269
4214
|
{
|
|
4270
|
-
enabled:
|
|
4215
|
+
enabled: z6.boolean().describe("true = hold caffeinate on; false = release and let the Mac sleep")
|
|
4271
4216
|
},
|
|
4272
4217
|
async ({ enabled }) => {
|
|
4273
4218
|
const threadId = process.env.SIDEBOARD_ORCHESTRATOR_THREAD_ID?.trim() || null;
|
|
@@ -4291,25 +4236,28 @@ async function startMcpServer() {
|
|
|
4291
4236
|
);
|
|
4292
4237
|
server.tool(
|
|
4293
4238
|
"create_workspace",
|
|
4294
|
-
`Create a workspace (isolated git checkout + first chat) from branch, pr, or ticket. A ticket, PR, or named branch has one checkout \u2014 if one already matches, returns it (alreadyStarted=true) instead of a second checkout. That is not one agent: chats lists every agent on that checkout. Add another with fork_chat; send_to_chat only the chat that should do this job. Creating from the default branch still opens a new isolated checkout. Pass repoPath from list_projects. cowboy=true uses the project folder on the default branch (no isolated worktree; land is commit+push). From an orchestration chat, omit parentChatId (Sideboard binds the child to this chat) or pass the exact id from the turn reminder \u2014 never invent a uuid. Omit agent/model \u2014 Sideboard applies ${accountDefaultsHint}. Do not pass your own agent or agent=cursor; those are ignored. Setup (settings.toml, .cursor/worktrees.json, or script/setup) runs in the background in parallel with the first turn (skipped for cowboy).`,
|
|
4239
|
+
`Create a workspace (isolated git checkout + first chat) from branch, pr, or ticket. A ticket, PR, or named branch has one checkout \u2014 if one already matches, returns it (alreadyStarted=true) instead of a second checkout. That is not one agent: chats lists every agent on that checkout. Add another with fork_chat; send_to_chat only the chat that should do this job. Creating from the default branch still opens a new isolated checkout. Pass repoPath from list_projects. cowboy=true uses the project folder on the default branch (no isolated worktree; land is commit+push). From an orchestration chat, omit parentChatId (Sideboard binds the child to this chat) or pass the exact id from the turn reminder \u2014 never invent a uuid. Omit agent/model \u2014 Sideboard applies ${accountDefaultsHint}. Do not pass your own agent or agent=cursor; those are ignored. Setup (settings.toml, .cursor/worktrees.json, or script/setup) runs in the background in parallel with the first turn (skipped for cowboy). Pass the same tags on every workspace you open for one task (short labels such as "phone-sync") so they still match in the sidebar the next day. Reusing a checkout adds those tags.`,
|
|
4295
4240
|
{
|
|
4296
|
-
sourceType:
|
|
4297
|
-
sourceRef:
|
|
4298
|
-
agent:
|
|
4241
|
+
sourceType: z6.enum(["branch", "pr", "ticket"]),
|
|
4242
|
+
sourceRef: z6.string(),
|
|
4243
|
+
agent: z6.enum(["claude", "codex", "opencode", "brightsy", "cursor"]).optional().describe(
|
|
4299
4244
|
`Usually omit. Sideboard uses Account default agent (${accountDefaults.agent}). Passing your own agent or cursor is ignored.`
|
|
4300
4245
|
),
|
|
4301
|
-
model:
|
|
4246
|
+
model: z6.string().nullable().optional().describe(
|
|
4302
4247
|
`Usually omit to use Account default model (${accountDefaults.model?.trim() || "Auto"}). Pass null only to force Auto / agent-default.`
|
|
4303
4248
|
),
|
|
4304
|
-
repoPath:
|
|
4305
|
-
title:
|
|
4306
|
-
cowboy:
|
|
4249
|
+
repoPath: z6.string(),
|
|
4250
|
+
title: z6.string().optional(),
|
|
4251
|
+
cowboy: z6.boolean().optional().describe(
|
|
4307
4252
|
"If true, work in the project folder on the default branch (no thread/* worktree). Requires Settings \u2192 Advanced \u2192 Cowboy mode. Land is commit+push to that branch. Archive does not delete the folder."
|
|
4308
4253
|
),
|
|
4309
|
-
parentChatId:
|
|
4254
|
+
parentChatId: z6.string().optional().describe(
|
|
4310
4255
|
"Orchestration: omit (preferred) or pass YOUR chat id from the turn reminder / AGENTS.md. Do not invent uuids."
|
|
4311
4256
|
),
|
|
4312
|
-
parentThreadId:
|
|
4257
|
+
parentThreadId: z6.string().optional().describe("Legacy alias for parentChatId."),
|
|
4258
|
+
tags: z6.union([z6.string(), z6.array(z6.string())]).optional().describe(
|
|
4259
|
+
"Short labels for this workspace (or a comma-separated string). Use the same tags on every checkout for one task. Added when the checkout already exists."
|
|
4260
|
+
)
|
|
4313
4261
|
},
|
|
4314
4262
|
async (args) => {
|
|
4315
4263
|
const result = await createOrchChildThread(orch, args, resolveNewThreadOptions);
|
|
@@ -4323,12 +4271,15 @@ async function startMcpServer() {
|
|
|
4323
4271
|
"start_board_card",
|
|
4324
4272
|
"Same as create_workspace for a ticket, PR, or named branch (attaches issue text when Sideboard can resolve it). Does not create a second checkout when one already matches \u2014 returns that workspace (alreadyStarted) and its chats. A workspace can have many agents. fork_chat to add one; send_to_chat only the chat that should do this job.",
|
|
4325
4273
|
{
|
|
4326
|
-
kind:
|
|
4327
|
-
ref:
|
|
4328
|
-
repoPath:
|
|
4329
|
-
title:
|
|
4274
|
+
kind: z6.enum(["ticket", "pr", "branch"]),
|
|
4275
|
+
ref: z6.string().describe("Ticket identifier (ENG-12), PR number (44), or branch name from list_board"),
|
|
4276
|
+
repoPath: z6.string().describe("Project path from list_projects / list_board"),
|
|
4277
|
+
title: z6.string().optional(),
|
|
4278
|
+
tags: z6.union([z6.string(), z6.array(z6.string())]).optional().describe(
|
|
4279
|
+
"Short labels for this workspace. Same tags on every checkout for one task. Added when the checkout already exists."
|
|
4280
|
+
)
|
|
4330
4281
|
},
|
|
4331
|
-
async ({ kind, ref, repoPath, title }) => {
|
|
4282
|
+
async ({ kind, ref, repoPath, title, tags }) => {
|
|
4332
4283
|
const root = await resolveRepoRoot(repoPath);
|
|
4333
4284
|
const existing = findLiveThreadForCreate(
|
|
4334
4285
|
{
|
|
@@ -4343,13 +4294,15 @@ async function startMcpServer() {
|
|
|
4343
4294
|
}))
|
|
4344
4295
|
);
|
|
4345
4296
|
if (existing) {
|
|
4297
|
+
const tagged = tags === void 0 ? orch.getThread(existing.id) : applyWorkspaceTags(existing.id, tags, "add");
|
|
4346
4298
|
return mcpJson({
|
|
4347
4299
|
alreadyStarted: true,
|
|
4348
4300
|
id: existing.id,
|
|
4349
4301
|
title: existing.title,
|
|
4350
4302
|
status: existing.status,
|
|
4303
|
+
tags: tagged?.tags ?? [],
|
|
4351
4304
|
link: `sideboard://chat/${existing.id}`,
|
|
4352
|
-
chats: workspaceAgentChatSummaries(
|
|
4305
|
+
chats: workspaceAgentChatSummaries(tagged?.worktreePath)
|
|
4353
4306
|
});
|
|
4354
4307
|
}
|
|
4355
4308
|
const workspaces = orch.listWorkspaces();
|
|
@@ -4363,7 +4316,8 @@ async function startMcpServer() {
|
|
|
4363
4316
|
sourceType: "branch",
|
|
4364
4317
|
sourceRef,
|
|
4365
4318
|
repoPath: root,
|
|
4366
|
-
title: title?.trim() || pin?.title || (sourceRef === "default" ? void 0 : sourceRef)
|
|
4319
|
+
title: title?.trim() || pin?.title || (sourceRef === "default" ? void 0 : sourceRef),
|
|
4320
|
+
tags
|
|
4367
4321
|
},
|
|
4368
4322
|
resolveNewThreadOptions
|
|
4369
4323
|
);
|
|
@@ -4393,7 +4347,8 @@ async function startMcpServer() {
|
|
|
4393
4347
|
sourceRef: ident,
|
|
4394
4348
|
repoPath: root,
|
|
4395
4349
|
title: title?.trim() || issue?.title,
|
|
4396
|
-
attachments
|
|
4350
|
+
attachments,
|
|
4351
|
+
tags
|
|
4397
4352
|
},
|
|
4398
4353
|
resolveNewThreadOptions
|
|
4399
4354
|
);
|
|
@@ -4410,7 +4365,8 @@ async function startMcpServer() {
|
|
|
4410
4365
|
sourceType: "pr",
|
|
4411
4366
|
sourceRef: number,
|
|
4412
4367
|
repoPath: root,
|
|
4413
|
-
title: title?.trim() || pr?.title
|
|
4368
|
+
title: title?.trim() || pr?.title,
|
|
4369
|
+
tags
|
|
4414
4370
|
},
|
|
4415
4371
|
resolveNewThreadOptions
|
|
4416
4372
|
);
|
|
@@ -4422,11 +4378,11 @@ async function startMcpServer() {
|
|
|
4422
4378
|
);
|
|
4423
4379
|
server.tool(
|
|
4424
4380
|
"send_to_chat",
|
|
4425
|
-
'Steer a prompt on a workspace chat (Settings \u2192 Follow-up, default steer:
|
|
4381
|
+
'Steer a prompt on a workspace chat (Settings \u2192 Follow-up, default steer: interrupt the in-flight turn and start now, same as the workspace composer). Use after create_workspace to start or continue a conversation. For commit/push/PR, prefer ask_git (canonical desktop-button phrases). Send "Merge PR." / ask_git merge only when the user explicitly asked to merge. force_stop=true kills the in-flight turn and clears the queue before this prompt \u2014 only when the current request is wrong and must be replaced. Do not send_to_chat to check in, resume after a halt notice, or because wait_for_turn returned stillRunning; that interrupts the child mid-thought. Call wait_for_turn again instead.',
|
|
4426
4382
|
{
|
|
4427
|
-
ref:
|
|
4428
|
-
prompt:
|
|
4429
|
-
force_stop:
|
|
4383
|
+
ref: z6.string(),
|
|
4384
|
+
prompt: z6.string(),
|
|
4385
|
+
force_stop: z6.boolean().optional()
|
|
4430
4386
|
},
|
|
4431
4387
|
async ({ ref, prompt, force_stop }) => {
|
|
4432
4388
|
if (force_stop) {
|
|
@@ -4451,8 +4407,8 @@ async function startMcpServer() {
|
|
|
4451
4407
|
"wait_for_turn",
|
|
4452
4408
|
"Wait until the chat finishes its current/queued turn, or return early with a live progress snapshot. MCP clients often kill tools around 60s, so this returns within 45s even while the child is still working. taskState is the A2A-style lifecycle: submitted (queued, not started), working, input-required (ask_user), completed, failed, canceled. stillRunning is true only for submitted/working. blockedReason is why the child is waiting on a person (the question, plan approval, or a reported block) \u2014 read it and do not send_to_chat a check-in. If stillRunning, text and usage are empty (they would be the previous turn) \u2014 read progress and call wait_for_turn again. Do not send_to_chat a check-in (that steers / interrupts). On failed, lastError/text is the failure. On canceled, the child did not finish \u2014 resume with send_to_chat or tell the user. On input-required, wait for the user in that chat. When finished, text is this turn\u2019s assistant reply and usage is that turn\u2019s tokens + costUsd (this turn, not the Claude session total; sessionCostUsd is the provider session total when present).",
|
|
4453
4409
|
{
|
|
4454
|
-
ref:
|
|
4455
|
-
timeoutMs:
|
|
4410
|
+
ref: z6.string(),
|
|
4411
|
+
timeoutMs: z6.number().optional()
|
|
4456
4412
|
},
|
|
4457
4413
|
async ({ ref, timeoutMs }) => {
|
|
4458
4414
|
const thread = await orch.waitForTurn(ref, mcpWaitForTurnTimeoutMs(timeoutMs), {
|
|
@@ -4480,7 +4436,7 @@ async function startMcpServer() {
|
|
|
4480
4436
|
server.tool(
|
|
4481
4437
|
"get_turn_result",
|
|
4482
4438
|
"Assistant message and last-turn usage when the turn finished. While stillRunning (submitted/working), text and usage are empty so a previous reply cannot be mistaken for this turn \u2014 use progress for tools/thinking. Not the full transcript. Includes taskState (A2A lifecycle) and blockedReason (why the agent is waiting on a person).",
|
|
4483
|
-
{ ref:
|
|
4439
|
+
{ ref: z6.string() },
|
|
4484
4440
|
async ({ ref }) => {
|
|
4485
4441
|
const result = orch.getTurnResult(ref);
|
|
4486
4442
|
const hint = mcpWaitTaskHint(result.taskState, result.status, result.blockedReason);
|
|
@@ -4494,12 +4450,13 @@ async function startMcpServer() {
|
|
|
4494
4450
|
);
|
|
4495
4451
|
server.tool(
|
|
4496
4452
|
"stop_chat",
|
|
4497
|
-
"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.",
|
|
4453
|
+
"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. Pass reason \u2014 a short note on why this chat is stopping. It is stored on the thread and kept when the turn unwinds. If you omit it, Sideboard still records that the queue was cleared.",
|
|
4498
4454
|
{
|
|
4499
|
-
ref:
|
|
4500
|
-
force:
|
|
4455
|
+
ref: z6.string(),
|
|
4456
|
+
force: z6.boolean().optional(),
|
|
4457
|
+
reason: z6.string().optional().describe("Why this chat is stopping. Stored on the thread for the next session.")
|
|
4501
4458
|
},
|
|
4502
|
-
async ({ ref, force }) => {
|
|
4459
|
+
async ({ ref, force, reason }) => {
|
|
4503
4460
|
const t = orch.getThread(ref);
|
|
4504
4461
|
if (!t) {
|
|
4505
4462
|
return {
|
|
@@ -4509,18 +4466,19 @@ async function startMcpServer() {
|
|
|
4509
4466
|
}
|
|
4510
4467
|
const clearQueue = force !== false;
|
|
4511
4468
|
const hadQueued = t.queue.length > 0;
|
|
4512
|
-
const stopped = orch.stop(ref, { clearQueue, notifyParent: false });
|
|
4469
|
+
const stopped = orch.stop(ref, { clearQueue, notifyParent: false, reason });
|
|
4513
4470
|
return mcpJson({
|
|
4514
4471
|
id: stopped.id,
|
|
4515
4472
|
status: stopped.status,
|
|
4516
|
-
clearedQueue: clearQueue && hadQueued
|
|
4473
|
+
clearedQueue: clearQueue && hadQueued,
|
|
4474
|
+
lastError: stopped.lastError ?? null
|
|
4517
4475
|
});
|
|
4518
4476
|
}
|
|
4519
4477
|
);
|
|
4520
4478
|
server.tool(
|
|
4521
4479
|
"archive_chat",
|
|
4522
4480
|
"Archive a thread (stops agent/dev, runs archive script, removes worktree when last chat tab). Coordinators commit, push, and open PRs by asking the worktree agent (ask_git). Merge only when the user explicitly asked.",
|
|
4523
|
-
{ ref:
|
|
4481
|
+
{ ref: z6.string() },
|
|
4524
4482
|
async ({ ref }) => {
|
|
4525
4483
|
const t = orch.getThread(ref);
|
|
4526
4484
|
if (!t) {
|
|
@@ -4546,7 +4504,7 @@ async function startMcpServer() {
|
|
|
4546
4504
|
server.tool(
|
|
4547
4505
|
"restore_chat",
|
|
4548
4506
|
"Restore an archived thread (recreates worktree from branch when needed)",
|
|
4549
|
-
{ ref:
|
|
4507
|
+
{ ref: z6.string() },
|
|
4550
4508
|
async ({ ref }) => {
|
|
4551
4509
|
try {
|
|
4552
4510
|
const restored = await orch.restore(ref);
|
|
@@ -4565,8 +4523,8 @@ async function startMcpServer() {
|
|
|
4565
4523
|
"get_diff",
|
|
4566
4524
|
"Compact diff summary (capped hunks, paginated)",
|
|
4567
4525
|
{
|
|
4568
|
-
ref:
|
|
4569
|
-
maxFiles:
|
|
4526
|
+
ref: z6.string(),
|
|
4527
|
+
maxFiles: z6.number().optional()
|
|
4570
4528
|
},
|
|
4571
4529
|
async ({ ref, maxFiles }) => {
|
|
4572
4530
|
const summary = await orch.diffSummary(ref);
|
|
@@ -4579,7 +4537,7 @@ async function startMcpServer() {
|
|
|
4579
4537
|
server.tool(
|
|
4580
4538
|
"get_pr_checks",
|
|
4581
4539
|
"Snapshot of GitHub PR checks for a worktree thread (`gh pr checks` plus merge/review gates). null = no PR. If the user gave a goal (Greptile 5/5, CI green), the worktree agent watches with `gh pr checks --watch` via /long-running \u2014 this tool is a snapshot, not a waiter. Coordinators: do not run gh from the orchestration cwd.",
|
|
4582
|
-
{ ref:
|
|
4540
|
+
{ ref: z6.string().describe("Worktree thread id/ref") },
|
|
4583
4541
|
async ({ ref }) => {
|
|
4584
4542
|
try {
|
|
4585
4543
|
const checks = await orch.getPrChecks(ref);
|
|
@@ -4593,7 +4551,7 @@ async function startMcpServer() {
|
|
|
4593
4551
|
server.tool(
|
|
4594
4552
|
"request_review",
|
|
4595
4553
|
'Start a merge-readiness Review on a worktree agent thread (same as the desktop Review button). If that worktree has a single unused idle agent (no messages), runs the review there; otherwise opens a new Review chat tab. Attaches .claude/skills/review/SKILL.md when present (else copies .sideboard/review.md into .context/review.md, or seeds that file from the stock template), and sends "Review changes in this workspace." Expect Approve / Approve with nits / Request changes / Needs more information in that chat. The review stays there so the user can read it and type next steps \u2014 do not comment on or update the PR or ticket, and do not ask_user after the review, until they ask. Pass a worktree thread ref \u2014 not the orchestrator. Then wait_for_turn (loop while stillRunning) / get_turn_result on the returned review thread id.',
|
|
4596
|
-
{ ref:
|
|
4554
|
+
{ ref: z6.string().describe("Worktree thread id/ref to review") },
|
|
4597
4555
|
async ({ ref }) => {
|
|
4598
4556
|
try {
|
|
4599
4557
|
const tab = await orch.requestReview(ref);
|
|
@@ -4615,8 +4573,8 @@ async function startMcpServer() {
|
|
|
4615
4573
|
"ask_git",
|
|
4616
4574
|
"Commit & push, open a draft PR, mark ready for review, resolve conflicts, or merge \u2014 same prompts as the desktop git buttons (including Resolve). Always steers the worktree agent; does not push or open the PR locally. Then wait_for_turn (loop while stillRunning). Do not start a checks loop on a plain push \u2014 only if the user gave a goal (Greptile 5/5, CI green). Pass a worktree thread ref (not the orchestrator). action=ready-for-review and action=merge only when the user explicitly asked (or clicked the desktop button). Do not run git or gh from the orchestration cwd.",
|
|
4617
4575
|
{
|
|
4618
|
-
ref:
|
|
4619
|
-
action:
|
|
4576
|
+
ref: z6.string().describe("Worktree thread id/ref"),
|
|
4577
|
+
action: z6.enum(AGENT_GIT_ACTIONS).describe(
|
|
4620
4578
|
"commit-push | create-draft | create-web | resolve-conflicts | ready-for-review | merge"
|
|
4621
4579
|
)
|
|
4622
4580
|
},
|
|
@@ -4645,7 +4603,7 @@ async function startMcpServer() {
|
|
|
4645
4603
|
}
|
|
4646
4604
|
}
|
|
4647
4605
|
);
|
|
4648
|
-
const agentEnum =
|
|
4606
|
+
const agentEnum = z6.enum(["claude", "codex", "opencode", "brightsy", "cursor"]);
|
|
4649
4607
|
server.tool(
|
|
4650
4608
|
"list_models",
|
|
4651
4609
|
"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.",
|
|
@@ -4666,11 +4624,11 @@ async function startMcpServer() {
|
|
|
4666
4624
|
"fork_workspace",
|
|
4667
4625
|
"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_chat / wait_for_turn (loop while stillRunning) on the returned id.",
|
|
4668
4626
|
{
|
|
4669
|
-
ref:
|
|
4670
|
-
through_index:
|
|
4627
|
+
ref: z6.string().describe("Worktree thread id/ref to fork"),
|
|
4628
|
+
through_index: z6.number().optional().describe("Inclusive message index to include in the transcript (default: all)"),
|
|
4671
4629
|
agent: agentEnum.optional().describe("Agent for the forked chat (default: same as source)"),
|
|
4672
|
-
model:
|
|
4673
|
-
title:
|
|
4630
|
+
model: z6.string().nullable().optional().describe("Usually omit (Auto). Only set from list_models when you need a specific model"),
|
|
4631
|
+
title: z6.string().optional()
|
|
4674
4632
|
},
|
|
4675
4633
|
async ({ ref, through_index, agent, model, title }) => {
|
|
4676
4634
|
try {
|
|
@@ -4704,11 +4662,11 @@ async function startMcpServer() {
|
|
|
4704
4662
|
"fork_chat",
|
|
4705
4663
|
"Add another agent chat on the SAME workspace (same files, Run, and git). Also forks a Global orchestration chat into a new orchestration tab. Pass agent when the new chat should use a different harness. Leave model unset for Auto unless you have a reason. Orchestration forks require an MCP-capable agent (claude, cursor, codex, opencode \u2014 not brightsy). Use this for a second agent on a checkout (review, another harness, parallel work on the same branch). Do not create_workspace again for that \u2014 it returns the existing checkout. Then send_to_chat / wait_for_turn (loop while stillRunning) on the returned id. Use fork_workspace only when the work needs its own git checkout.",
|
|
4706
4664
|
{
|
|
4707
|
-
ref:
|
|
4708
|
-
through_index:
|
|
4665
|
+
ref: z6.string().describe("Thread id/ref to fork (worktree agent or orchestration chat)"),
|
|
4666
|
+
through_index: z6.number().optional().describe("Inclusive message index to include in the transcript (default: all)"),
|
|
4709
4667
|
agent: agentEnum.optional().describe("Agent for the forked chat (default: same as source)"),
|
|
4710
|
-
model:
|
|
4711
|
-
title:
|
|
4668
|
+
model: z6.string().nullable().optional().describe("Usually omit (Auto). Only set from list_models when you need a specific model"),
|
|
4669
|
+
title: z6.string().optional()
|
|
4712
4670
|
},
|
|
4713
4671
|
async ({ ref, through_index, agent, model, title }) => {
|
|
4714
4672
|
try {
|
|
@@ -4746,7 +4704,7 @@ async function startMcpServer() {
|
|
|
4746
4704
|
server.tool(
|
|
4747
4705
|
"run_setup",
|
|
4748
4706
|
"Re-run workspace setup (Sideboard/Conductor settings, .cursor/worktrees.json, or script/setup). New worktrees already run this automatically.",
|
|
4749
|
-
{ ref:
|
|
4707
|
+
{ ref: z6.string() },
|
|
4750
4708
|
async ({ ref }) => {
|
|
4751
4709
|
const result = await orch.runSetup(ref);
|
|
4752
4710
|
return mcpJson(result);
|
|
@@ -4755,7 +4713,7 @@ async function startMcpServer() {
|
|
|
4755
4713
|
server.tool(
|
|
4756
4714
|
"add_project",
|
|
4757
4715
|
"Register a git repo as a Sideboard workspace",
|
|
4758
|
-
{ repoPath:
|
|
4716
|
+
{ repoPath: z6.string() },
|
|
4759
4717
|
async ({ repoPath }) => {
|
|
4760
4718
|
const ws = await orch.addWorkspace(repoPath);
|
|
4761
4719
|
return mcpJson(ws);
|
|
@@ -4763,10 +4721,10 @@ async function startMcpServer() {
|
|
|
4763
4721
|
);
|
|
4764
4722
|
server.tool(
|
|
4765
4723
|
"remove_project",
|
|
4766
|
-
"Unregister a Sideboard workspace
|
|
4767
|
-
{ repoPath:
|
|
4724
|
+
"Unregister a Sideboard workspace and archive its chats",
|
|
4725
|
+
{ repoPath: z6.string() },
|
|
4768
4726
|
async ({ repoPath }) => {
|
|
4769
|
-
orch.removeWorkspace(repoPath);
|
|
4727
|
+
await orch.removeWorkspace(repoPath);
|
|
4770
4728
|
return { content: [{ type: "text", text: "ok" }] };
|
|
4771
4729
|
}
|
|
4772
4730
|
);
|
|
@@ -4774,12 +4732,12 @@ async function startMcpServer() {
|
|
|
4774
4732
|
"fanout",
|
|
4775
4733
|
"Best-of-n: create one thread per agent with the same prompt (parallel attempts)",
|
|
4776
4734
|
{
|
|
4777
|
-
prompt:
|
|
4778
|
-
agents:
|
|
4779
|
-
repoPath:
|
|
4780
|
-
sourceType:
|
|
4781
|
-
sourceRef:
|
|
4782
|
-
title:
|
|
4735
|
+
prompt: z6.string(),
|
|
4736
|
+
agents: z6.array(z6.enum(["claude", "codex", "opencode", "brightsy", "cursor"])),
|
|
4737
|
+
repoPath: z6.string(),
|
|
4738
|
+
sourceType: z6.enum(["branch", "pr", "ticket"]).optional(),
|
|
4739
|
+
sourceRef: z6.string().optional(),
|
|
4740
|
+
title: z6.string().optional()
|
|
4783
4741
|
},
|
|
4784
4742
|
async (args) => {
|
|
4785
4743
|
const threads = await orch.bestOfN(args);
|
|
@@ -4797,8 +4755,8 @@ async function startMcpServer() {
|
|
|
4797
4755
|
"list_branches",
|
|
4798
4756
|
"List git branches in a registered workspace. Pass repoPath from list_projects (unmerged into the default branch by default \u2014 for create_workspace sourceType=branch).",
|
|
4799
4757
|
{
|
|
4800
|
-
repoPath:
|
|
4801
|
-
unmergedOnly:
|
|
4758
|
+
repoPath: z6.string(),
|
|
4759
|
+
unmergedOnly: z6.boolean().optional()
|
|
4802
4760
|
},
|
|
4803
4761
|
async ({ repoPath, unmergedOnly }) => {
|
|
4804
4762
|
const root = await resolveRepoRoot(repoPath);
|
|
@@ -4819,19 +4777,19 @@ async function startMcpServer() {
|
|
|
4819
4777
|
"list_prs",
|
|
4820
4778
|
`List GitHub PRs for a registered workspace (the review surface for assigned ticket work \u2014 not the tickets). Pass repoPath from list_projects. "Get me N tickets to review" \u2192 queue=review and limit=N: open non-draft PRs labeled with this project's ready-for-review GitHub label (Settings \u2192 Projects; default eng-review) with no individual user reviewer. Prefer teams that match Settings \u2192 Agents / Projects roles for this repo. A team like engineering-team is not a claim (you are on that team). Also: state (open|closed|merged|all|review), label, reviewer (me|unassigned|login), query, limit (default 40, max 250). Then create_workspace with sourceType=pr.`,
|
|
4821
4779
|
{
|
|
4822
|
-
repoPath:
|
|
4823
|
-
query:
|
|
4824
|
-
queue:
|
|
4780
|
+
repoPath: z6.string(),
|
|
4781
|
+
query: z6.string().optional().describe("GitHub search tokens (title, draft:true, \u2026)"),
|
|
4782
|
+
queue: z6.enum(["review", "mine", "approved", "changes"]).optional().describe(
|
|
4825
4783
|
"review = unclaimed ready-for-review inbox (Settings \u2192 Projects label, default eng-review). mine = review-requested:@me. approved / changes = eng-approved / eng-requested-changes."
|
|
4826
4784
|
),
|
|
4827
|
-
state:
|
|
4828
|
-
label:
|
|
4785
|
+
state: z6.enum(["open", "closed", "merged", "all", "review"]).optional().describe("GitHub PR state (default open). review is an alias for queue=review."),
|
|
4786
|
+
label: z6.string().optional().describe(
|
|
4829
4787
|
"GitHub label / workflow tag. Comma-separated AND. Examples: eng-review, eng-approved, eng-requested-changes"
|
|
4830
4788
|
),
|
|
4831
|
-
reviewer:
|
|
4789
|
+
reviewer: z6.string().optional().describe(
|
|
4832
4790
|
"me (review requested of you), unassigned (no individual reviewer; team queues like engineering-team still count), all, or a GitHub login"
|
|
4833
4791
|
),
|
|
4834
|
-
limit:
|
|
4792
|
+
limit: z6.number().int().positive().max(250).optional().describe("Page size (default 40, max 250). Raise when truncated is true.")
|
|
4835
4793
|
},
|
|
4836
4794
|
async ({ repoPath, query, queue, state, label, reviewer, limit }) => {
|
|
4837
4795
|
const root = await resolveRepoRoot(repoPath);
|
|
@@ -4864,7 +4822,7 @@ async function startMcpServer() {
|
|
|
4864
4822
|
server.tool(
|
|
4865
4823
|
"get_pr_stack",
|
|
4866
4824
|
"Load the GitHub PR stack for a thread worktree (`gh stack view --json`). Returns null JSON when the branch is not stacked. Prefer this before ask_git merge on stacked PRs (and only merge when the user explicitly asked).",
|
|
4867
|
-
{ ref:
|
|
4825
|
+
{ ref: z6.string() },
|
|
4868
4826
|
async ({ ref }) => {
|
|
4869
4827
|
const stack = await orch.getPrStack(ref);
|
|
4870
4828
|
return mcpJson(stack);
|
|
@@ -4874,8 +4832,8 @@ async function startMcpServer() {
|
|
|
4874
4832
|
"open_pr_stack_layers",
|
|
4875
4833
|
"Materialize one worktree+thread per stack layer (or a single 1-based layer). Pass a thread ref already on the stack.",
|
|
4876
4834
|
{
|
|
4877
|
-
ref:
|
|
4878
|
-
layer:
|
|
4835
|
+
ref: z6.string(),
|
|
4836
|
+
layer: z6.number().int().positive().optional()
|
|
4879
4837
|
},
|
|
4880
4838
|
async ({ ref, layer }) => {
|
|
4881
4839
|
const result = await orch.openPrStackLayers(ref, { layer });
|
|
@@ -4898,9 +4856,9 @@ async function startMcpServer() {
|
|
|
4898
4856
|
"add_stack_layer",
|
|
4899
4857
|
"Add a branch on top of the current stack (`gh stack add`) and open a worktree+thread for it.",
|
|
4900
4858
|
{
|
|
4901
|
-
ref:
|
|
4902
|
-
branchName:
|
|
4903
|
-
title:
|
|
4859
|
+
ref: z6.string(),
|
|
4860
|
+
branchName: z6.string(),
|
|
4861
|
+
title: z6.string().optional()
|
|
4904
4862
|
},
|
|
4905
4863
|
async ({ ref, branchName, title }) => {
|
|
4906
4864
|
const result = await orch.addStackLayer(ref, branchName, { title });
|
|
@@ -4918,11 +4876,11 @@ async function startMcpServer() {
|
|
|
4918
4876
|
"create_pr_stack",
|
|
4919
4877
|
"Create a new GitHub PR stack with one Sideboard worktree per layer (bottom\u2192top branch names). Requires `gh extension install github/gh-stack`.",
|
|
4920
4878
|
{
|
|
4921
|
-
repoPath:
|
|
4922
|
-
branches:
|
|
4923
|
-
agent:
|
|
4924
|
-
base:
|
|
4925
|
-
title:
|
|
4879
|
+
repoPath: z6.string(),
|
|
4880
|
+
branches: z6.array(z6.string()).min(1),
|
|
4881
|
+
agent: z6.enum(["claude", "codex", "opencode", "brightsy", "cursor"]),
|
|
4882
|
+
base: z6.string().optional(),
|
|
4883
|
+
title: z6.string().optional()
|
|
4926
4884
|
},
|
|
4927
4885
|
async (args) => {
|
|
4928
4886
|
const result = await orch.createPrStack({
|
|
@@ -4950,10 +4908,10 @@ async function startMcpServer() {
|
|
|
4950
4908
|
"list_issues",
|
|
4951
4909
|
"List or search issues (Linear, AbleTime, or GitHub; falls back to GitHub). Use Settings \u2192 Agents / Projects notes and roles to pick tickets relevant to the viewer (query + assignee=me or unassigned as the notes say). Default 40; pass query and/or limit (max 250) when truncated. assignee: me (Linear default), unassigned, all, or a user id. Pass updatedSince for new/updated tickets and new comments (\u201Cany updates since yesterday?\u201D). Then create_workspace with sourceType=ticket.",
|
|
4952
4910
|
{
|
|
4953
|
-
repoPath:
|
|
4954
|
-
query:
|
|
4955
|
-
assignee:
|
|
4956
|
-
limit:
|
|
4911
|
+
repoPath: z6.string(),
|
|
4912
|
+
query: z6.string().optional().describe("Search title, identifier, or description"),
|
|
4913
|
+
assignee: z6.string().optional().describe("me (Linear default), unassigned, all, a user id, or a GitHub login"),
|
|
4914
|
+
limit: z6.number().int().positive().max(250).optional().describe("Page size (default 40, max 250). Raise when truncated is true."),
|
|
4957
4915
|
updatedSince: mcpIssueUpdatedSinceSchema
|
|
4958
4916
|
},
|
|
4959
4917
|
async ({ repoPath, query, assignee, limit, updatedSince }) => {
|