@sideboard-ai/core 0.1.73 → 0.1.77
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/agents/cursor-runner.cjs +1 -1
- package/dist/agents/cursor-runner.js +1 -1
- package/dist/{agents-77GE7VRW.js → agents-LKUHPPEQ.js} +3 -3
- package/dist/{agents-GUFUYXKP.js → agents-XJV6J3K2.js} +4 -4
- package/dist/{caffeinate-hold-KLC6SABD.js → caffeinate-hold-EAZNWJBE.js} +5 -1
- package/dist/caffeinate-hold-OHAUWOA6.js +20 -0
- package/dist/{chunk-AFW3M6LU.js → chunk-2FJI5JXX.js} +17 -11
- package/dist/{chunk-CXT2PLO7.js → chunk-2ZIPJRJE.js} +1 -1
- package/dist/{chunk-CYKPUTXN.js → chunk-75U65MBE.js} +1 -1
- package/dist/{chunk-V4ACEJX2.js → chunk-EEKSZTMU.js} +14 -12
- package/dist/{chunk-POW7JCB5.js → chunk-GWTLKXXP.js} +1 -1
- package/dist/chunk-MFF2RE3I.js +168 -0
- package/dist/{chunk-WSFZPOPH.js → chunk-QD37IILI.js} +17 -11
- package/dist/chunk-RV6DBEDQ.js +170 -0
- package/dist/{chunk-3WJAUKIL.js → chunk-SEOICVGB.js} +1 -1
- package/dist/{chunk-NF6Y4GTE.js → chunk-VERLUKN2.js} +1 -1
- package/dist/{chunk-6RFPKZNC.js → chunk-XJNCWYFR.js} +14 -12
- package/dist/{coordinator-prompt-4U2QNHGT.js → coordinator-prompt-LLFJUO2R.js} +1 -1
- package/dist/{coordinator-prompt-ZHBDHMZB.js → coordinator-prompt-MRCMMRSF.js} +1 -1
- package/dist/{global-workspace-VF56FTPY.js → global-workspace-TWHMYLTZ.js} +2 -2
- package/dist/{global-workspace-S3B6ESZS.js → global-workspace-YNMNO4CL.js} +2 -2
- package/dist/index.cjs +1203 -468
- package/dist/index.d.cts +177 -23
- package/dist/index.d.ts +177 -23
- package/dist/index.js +1049 -401
- package/dist/mcp/run-stdio.cjs +848 -306
- package/dist/mcp/run-stdio.js +656 -182
- package/dist/{workspaces-ZJ45O4CD.js → workspaces-GQ4XKBD3.js} +3 -3
- package/dist/{workspaces-R66324S7.js → workspaces-O3U5BENH.js} +3 -3
- package/package.json +1 -1
- package/dist/caffeinate-hold-BEJIYPJ7.js +0 -107
- package/dist/chunk-JAWEDVVA.js +0 -106
package/dist/index.js
CHANGED
|
@@ -4,7 +4,7 @@ import {
|
|
|
4
4
|
listWorkspaces,
|
|
5
5
|
removeWorkspace,
|
|
6
6
|
syncWorkspacesFromThreads
|
|
7
|
-
} from "./chunk-
|
|
7
|
+
} from "./chunk-75U65MBE.js";
|
|
8
8
|
import {
|
|
9
9
|
BRIGHTSY_MCP_ALLOWED_TOOLS,
|
|
10
10
|
CLAUDE_MODEL_CATALOG,
|
|
@@ -58,7 +58,7 @@ import {
|
|
|
58
58
|
sideboardMcpProfile,
|
|
59
59
|
threadRequestsBrightsyMcp,
|
|
60
60
|
writeInjectedMcpConfig
|
|
61
|
-
} from "./chunk-
|
|
61
|
+
} from "./chunk-QD37IILI.js";
|
|
62
62
|
import {
|
|
63
63
|
CLOUD_COORDINATOR_BUSY_REPLY,
|
|
64
64
|
CLOUD_COORDINATOR_STOPPED_REPLY,
|
|
@@ -85,7 +85,7 @@ import {
|
|
|
85
85
|
parseForceStopMessage,
|
|
86
86
|
slackCoordinatorSourceRef,
|
|
87
87
|
takenTeamSlugsForOrchestration
|
|
88
|
-
} from "./chunk-
|
|
88
|
+
} from "./chunk-VERLUKN2.js";
|
|
89
89
|
import {
|
|
90
90
|
COORDINATOR_TOOL_PLAYBOOK,
|
|
91
91
|
SLACK_REPLY_FORMATTING,
|
|
@@ -94,7 +94,7 @@ import {
|
|
|
94
94
|
enrichWorkspacesWithGithub,
|
|
95
95
|
ensureGlobalCoordinatorCwd,
|
|
96
96
|
formatWorkspaceInventory
|
|
97
|
-
} from "./chunk-
|
|
97
|
+
} from "./chunk-EEKSZTMU.js";
|
|
98
98
|
import {
|
|
99
99
|
brightsyConfigPath,
|
|
100
100
|
brightsyMcpServerName,
|
|
@@ -128,7 +128,7 @@ import {
|
|
|
128
128
|
parseCursorRunnerLine,
|
|
129
129
|
pushTurnStderr,
|
|
130
130
|
summarizeTurnStderr
|
|
131
|
-
} from "./chunk-
|
|
131
|
+
} from "./chunk-SEOICVGB.js";
|
|
132
132
|
import {
|
|
133
133
|
HARNESS_ENV_KEYS,
|
|
134
134
|
appSettingsPath,
|
|
@@ -192,8 +192,10 @@ import {
|
|
|
192
192
|
import {
|
|
193
193
|
caffeinateHoldPath,
|
|
194
194
|
getCaffeinateHold,
|
|
195
|
+
isThreadCaffeinated,
|
|
196
|
+
releaseCaffeinateHoldForThread,
|
|
195
197
|
setCaffeinateHold
|
|
196
|
-
} from "./chunk-
|
|
198
|
+
} from "./chunk-MFF2RE3I.js";
|
|
197
199
|
import {
|
|
198
200
|
writePrivateFile
|
|
199
201
|
} from "./chunk-JT7R45JB.js";
|
|
@@ -324,6 +326,31 @@ import {
|
|
|
324
326
|
withExportedPath
|
|
325
327
|
} from "./chunk-D4DPQ552.js";
|
|
326
328
|
|
|
329
|
+
// src/git/agent-git-actions.ts
|
|
330
|
+
var AGENT_GIT_ACTIONS = [
|
|
331
|
+
"commit-push",
|
|
332
|
+
"create-draft",
|
|
333
|
+
"create-web",
|
|
334
|
+
"resolve-conflicts",
|
|
335
|
+
"merge"
|
|
336
|
+
];
|
|
337
|
+
function agentGitPrompt(action, opts) {
|
|
338
|
+
switch (action) {
|
|
339
|
+
case "commit-push":
|
|
340
|
+
return "Commit and push.";
|
|
341
|
+
case "create-draft":
|
|
342
|
+
return "Commit, push, and open a draft PR.";
|
|
343
|
+
case "create-web":
|
|
344
|
+
return "Commit, push, and open a PR in the browser.";
|
|
345
|
+
case "resolve-conflicts": {
|
|
346
|
+
const base = opts?.prBase?.trim().replace(/^refs\/heads\//, "");
|
|
347
|
+
return base ? `Merge origin/${base} into this branch. Then push.` : "Fix merge conflicts.";
|
|
348
|
+
}
|
|
349
|
+
case "merge":
|
|
350
|
+
return "Merge PR.";
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
|
|
327
354
|
// src/integrations/github.ts
|
|
328
355
|
async function getGitHubStatus() {
|
|
329
356
|
const which = await run("which", ["gh"], { reject: false });
|
|
@@ -349,9 +376,9 @@ async function getGitHubStatus() {
|
|
|
349
376
|
};
|
|
350
377
|
}
|
|
351
378
|
const status = await run("gh", ["auth", "status"], { reject: false });
|
|
352
|
-
const
|
|
379
|
+
const text3 = `${status.stdout}
|
|
353
380
|
${status.stderr}`;
|
|
354
|
-
const loginMatch =
|
|
381
|
+
const loginMatch = text3.match(/Logged in to ([^\s]+) account (\S+)/i) ?? text3.match(/Logged in to ([^\s]+) as (\S+)/i);
|
|
355
382
|
if (loginMatch) {
|
|
356
383
|
return {
|
|
357
384
|
connected: true,
|
|
@@ -371,13 +398,39 @@ async function refreshGitHubAuth() {
|
|
|
371
398
|
return getGitHubStatus();
|
|
372
399
|
}
|
|
373
400
|
|
|
401
|
+
// src/http/fetch.ts
|
|
402
|
+
var injected = null;
|
|
403
|
+
function setHttpFetchImpl(fetchImpl) {
|
|
404
|
+
injected = fetchImpl;
|
|
405
|
+
}
|
|
406
|
+
function formatFetchError(err, url) {
|
|
407
|
+
if (!(err instanceof Error)) return `${String(err)} (${url})`;
|
|
408
|
+
const cause = err.cause;
|
|
409
|
+
let detail = "";
|
|
410
|
+
if (cause instanceof Error) {
|
|
411
|
+
const code = typeof cause.code === "string" ? cause.code : void 0;
|
|
412
|
+
detail = code ? ` [${code}: ${cause.message}]` : ` [${cause.message}]`;
|
|
413
|
+
} else if (cause != null) {
|
|
414
|
+
detail = ` [${String(cause)}]`;
|
|
415
|
+
}
|
|
416
|
+
return `${err.message}${detail} (${url})`;
|
|
417
|
+
}
|
|
418
|
+
async function httpFetch(input, init) {
|
|
419
|
+
const url = typeof input === "string" ? input : input.href;
|
|
420
|
+
const fn = injected ?? globalThis.fetch.bind(globalThis);
|
|
421
|
+
try {
|
|
422
|
+
return await fn(url, init);
|
|
423
|
+
} catch (err) {
|
|
424
|
+
throw new Error(formatFetchError(err, url));
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
|
|
374
428
|
// src/integrations/linear-oauth.ts
|
|
375
429
|
import { createHash, randomBytes } from "crypto";
|
|
376
430
|
import { createServer } from "http";
|
|
377
431
|
|
|
378
432
|
// src/integrations/linear-app.ts
|
|
379
433
|
var BAKED_LINEAR_CLIENT_ID = "39a15abc7ea5bea17c47f47f85ff5fd0";
|
|
380
|
-
var BAKED_LINEAR_CLIENT_SECRET = "";
|
|
381
434
|
function hasBakedLinearOAuth() {
|
|
382
435
|
return Boolean(BAKED_LINEAR_CLIENT_ID.trim());
|
|
383
436
|
}
|
|
@@ -389,12 +442,12 @@ var LINEAR_AUTHORIZE = "https://linear.app/oauth/authorize";
|
|
|
389
442
|
var LINEAR_TOKEN = "https://api.linear.app/oauth/token";
|
|
390
443
|
var LINEAR_REVOKE = "https://api.linear.app/oauth/revoke";
|
|
391
444
|
var LINEAR_GRAPHQL = "https://api.linear.app/graphql";
|
|
392
|
-
var LINEAR_OAUTH_SCOPES = "read";
|
|
445
|
+
var LINEAR_OAUTH_SCOPES = "read,write";
|
|
393
446
|
var REFRESH_SKEW_MS = 5 * 6e4;
|
|
394
447
|
function linearOAuthCredentials() {
|
|
395
448
|
const settings = loadAppSettings();
|
|
396
449
|
const clientId = process.env.SIDEBOARD_LINEAR_CLIENT_ID?.trim() || settings.integrations.linearClientId?.trim() || BAKED_LINEAR_CLIENT_ID.trim();
|
|
397
|
-
const clientSecret = process.env.SIDEBOARD_LINEAR_CLIENT_SECRET?.trim() || settings.integrations.linearClientSecret?.trim() ||
|
|
450
|
+
const clientSecret = process.env.SIDEBOARD_LINEAR_CLIENT_SECRET?.trim() || settings.integrations.linearClientSecret?.trim() || "";
|
|
398
451
|
if (!clientId) {
|
|
399
452
|
throw new Error(
|
|
400
453
|
`Linear browser sign-in needs a Linear OAuth app Client ID. Create one at linear.app/settings/api/applications/new with callback ${LINEAR_OAUTH_REDIRECT}, then set SIDEBOARD_LINEAR_CLIENT_ID (PKCE does not require a secret). You can still paste a personal API key.`
|
|
@@ -434,7 +487,7 @@ function htmlPage(title, body) {
|
|
|
434
487
|
h1{font-size:1.25rem}p{line-height:1.5;color:#444}</style></head><body>${body}</body></html>`;
|
|
435
488
|
}
|
|
436
489
|
async function exchangeLinearToken(body) {
|
|
437
|
-
const res = await
|
|
490
|
+
const res = await httpFetch(LINEAR_TOKEN, {
|
|
438
491
|
method: "POST",
|
|
439
492
|
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
440
493
|
body
|
|
@@ -456,7 +509,7 @@ async function persistLinearTokens(data, viewer) {
|
|
|
456
509
|
});
|
|
457
510
|
}
|
|
458
511
|
async function fetchLinearViewer(accessToken) {
|
|
459
|
-
const res = await
|
|
512
|
+
const res = await httpFetch(LINEAR_GRAPHQL, {
|
|
460
513
|
method: "POST",
|
|
461
514
|
headers: {
|
|
462
515
|
"Content-Type": "application/json",
|
|
@@ -627,7 +680,7 @@ async function revokeLinearToken(token, tokenTypeHint) {
|
|
|
627
680
|
token,
|
|
628
681
|
token_type_hint: tokenTypeHint
|
|
629
682
|
});
|
|
630
|
-
await
|
|
683
|
+
await httpFetch(LINEAR_REVOKE, {
|
|
631
684
|
method: "POST",
|
|
632
685
|
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
633
686
|
body
|
|
@@ -644,6 +697,18 @@ async function disconnectLinear() {
|
|
|
644
697
|
|
|
645
698
|
// src/integrations/linear.ts
|
|
646
699
|
var LINEAR_GRAPHQL2 = "https://api.linear.app/graphql";
|
|
700
|
+
var ISSUE_FIELDS = `
|
|
701
|
+
id
|
|
702
|
+
identifier
|
|
703
|
+
title
|
|
704
|
+
description
|
|
705
|
+
url
|
|
706
|
+
priority
|
|
707
|
+
state { id name type }
|
|
708
|
+
assignee { id name }
|
|
709
|
+
team { id key name states { nodes { id name type } } }
|
|
710
|
+
labels { nodes { name } }
|
|
711
|
+
`;
|
|
647
712
|
var ASSIGNED_ISSUES_QUERY = `
|
|
648
713
|
query SideboardAssignedIssues($first: Int!) {
|
|
649
714
|
viewer {
|
|
@@ -652,53 +717,280 @@ query SideboardAssignedIssues($first: Int!) {
|
|
|
652
717
|
orderBy: updatedAt
|
|
653
718
|
filter: { state: { type: { nin: ["completed", "canceled"] } } }
|
|
654
719
|
) {
|
|
655
|
-
nodes {
|
|
656
|
-
id
|
|
657
|
-
identifier
|
|
658
|
-
title
|
|
659
|
-
url
|
|
660
|
-
labels { nodes { name } }
|
|
661
|
-
}
|
|
720
|
+
nodes { ${ISSUE_FIELDS} }
|
|
662
721
|
}
|
|
663
722
|
}
|
|
664
723
|
}
|
|
665
724
|
`;
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
725
|
+
var TEAMS_QUERY = `
|
|
726
|
+
query SideboardTeams {
|
|
727
|
+
viewer { id name }
|
|
728
|
+
teams(first: 50) {
|
|
729
|
+
nodes {
|
|
730
|
+
id
|
|
731
|
+
key
|
|
732
|
+
name
|
|
733
|
+
states { nodes { id name type } }
|
|
734
|
+
}
|
|
735
|
+
}
|
|
736
|
+
}
|
|
737
|
+
`;
|
|
738
|
+
var ISSUE_QUERY = `
|
|
739
|
+
query SideboardIssue($id: String!) {
|
|
740
|
+
issue(id: $id) { ${ISSUE_FIELDS} }
|
|
741
|
+
}
|
|
742
|
+
`;
|
|
743
|
+
var ISSUE_CREATE = `
|
|
744
|
+
mutation SideboardIssueCreate($input: IssueCreateInput!) {
|
|
745
|
+
issueCreate(input: $input) {
|
|
746
|
+
success
|
|
747
|
+
issue { ${ISSUE_FIELDS} }
|
|
748
|
+
}
|
|
749
|
+
}
|
|
750
|
+
`;
|
|
751
|
+
var ISSUE_UPDATE = `
|
|
752
|
+
mutation SideboardIssueUpdate($id: String!, $input: IssueUpdateInput!) {
|
|
753
|
+
issueUpdate(id: $id, input: $input) {
|
|
754
|
+
success
|
|
755
|
+
issue { ${ISSUE_FIELDS} }
|
|
756
|
+
}
|
|
757
|
+
}
|
|
758
|
+
`;
|
|
759
|
+
var COMMENT_CREATE = `
|
|
760
|
+
mutation SideboardCommentCreate($input: CommentCreateInput!) {
|
|
761
|
+
commentCreate(input: $input) {
|
|
762
|
+
success
|
|
763
|
+
comment { id body url }
|
|
764
|
+
}
|
|
765
|
+
}
|
|
766
|
+
`;
|
|
767
|
+
function rewriteLinearError(message) {
|
|
768
|
+
if (/scope|permission|not (authorized|allowed)|unauthorized|insufficient/i.test(message)) {
|
|
769
|
+
return `${message} \u2014 Disconnect and Connect Linear in Account settings to grant write access.`;
|
|
770
|
+
}
|
|
771
|
+
return message;
|
|
772
|
+
}
|
|
773
|
+
async function requireLinearToken(apiKey) {
|
|
774
|
+
const token = (apiKey ?? await getLinearAuthToken())?.trim();
|
|
775
|
+
if (!token) {
|
|
669
776
|
throw new Error("Linear is not connected \u2014 sign in from Account settings");
|
|
670
777
|
}
|
|
671
|
-
|
|
672
|
-
|
|
778
|
+
return token;
|
|
779
|
+
}
|
|
780
|
+
async function linearGraphql(query, variables, opts) {
|
|
781
|
+
const apiKey = await requireLinearToken(opts?.apiKey);
|
|
782
|
+
const res = await httpFetch(LINEAR_GRAPHQL2, {
|
|
673
783
|
method: "POST",
|
|
674
784
|
headers: {
|
|
675
785
|
"Content-Type": "application/json",
|
|
676
786
|
Authorization: linearAuthorizationHeader(apiKey)
|
|
677
787
|
},
|
|
678
|
-
body: JSON.stringify({
|
|
679
|
-
query: ASSIGNED_ISSUES_QUERY,
|
|
680
|
-
variables: { first }
|
|
681
|
-
})
|
|
788
|
+
body: JSON.stringify({ query, variables })
|
|
682
789
|
});
|
|
683
790
|
if (!res.ok) {
|
|
684
791
|
const body = await res.text().catch(() => "");
|
|
685
792
|
throw new Error(
|
|
686
|
-
|
|
793
|
+
rewriteLinearError(
|
|
794
|
+
`Linear API error ${res.status}${body ? `: ${body.slice(0, 200)}` : ""}`
|
|
795
|
+
)
|
|
687
796
|
);
|
|
688
797
|
}
|
|
689
798
|
const json = await res.json();
|
|
690
799
|
if (json.errors?.length) {
|
|
691
|
-
throw new Error(
|
|
800
|
+
throw new Error(
|
|
801
|
+
rewriteLinearError(json.errors.map((e) => e.message ?? "Linear error").join("; "))
|
|
802
|
+
);
|
|
692
803
|
}
|
|
693
|
-
|
|
694
|
-
|
|
804
|
+
if (json.data == null) {
|
|
805
|
+
throw new Error("Linear API returned no data");
|
|
806
|
+
}
|
|
807
|
+
return json.data;
|
|
808
|
+
}
|
|
809
|
+
function mapState(node) {
|
|
810
|
+
if (!node?.id) return void 0;
|
|
811
|
+
return {
|
|
812
|
+
id: String(node.id),
|
|
813
|
+
name: String(node.name ?? ""),
|
|
814
|
+
type: String(node.type ?? "")
|
|
815
|
+
};
|
|
816
|
+
}
|
|
817
|
+
function mapIssue(node) {
|
|
818
|
+
const team = node.team;
|
|
819
|
+
return {
|
|
695
820
|
id: String(node.id ?? node.identifier ?? ""),
|
|
696
821
|
identifier: String(node.identifier ?? node.id ?? ""),
|
|
697
822
|
title: String(node.title ?? ""),
|
|
698
823
|
url: String(node.url ?? ""),
|
|
699
|
-
|
|
824
|
+
description: node.description?.trim() || void 0,
|
|
825
|
+
priority: typeof node.priority === "number" ? node.priority : void 0,
|
|
826
|
+
state: mapState(node.state),
|
|
827
|
+
assignee: node.assignee?.id ? { id: String(node.assignee.id), name: String(node.assignee.name ?? "") } : void 0,
|
|
828
|
+
team: team?.id ? {
|
|
829
|
+
id: String(team.id),
|
|
830
|
+
key: String(team.key ?? ""),
|
|
831
|
+
name: String(team.name ?? ""),
|
|
832
|
+
states: (team.states?.nodes ?? []).map((s) => mapState(s)).filter((s) => Boolean(s))
|
|
833
|
+
} : void 0,
|
|
834
|
+
labels: (node.labels?.nodes ?? []).map((l) => l.name).filter((n) => Boolean(n))
|
|
835
|
+
};
|
|
836
|
+
}
|
|
837
|
+
function toIssueInfo(issue) {
|
|
838
|
+
return {
|
|
839
|
+
id: issue.id,
|
|
840
|
+
identifier: issue.identifier,
|
|
841
|
+
title: issue.title,
|
|
842
|
+
url: issue.url,
|
|
843
|
+
labels: issue.labels,
|
|
700
844
|
provider: "linear"
|
|
701
|
-
}
|
|
845
|
+
};
|
|
846
|
+
}
|
|
847
|
+
function mapTeam(node) {
|
|
848
|
+
return {
|
|
849
|
+
id: String(node.id ?? ""),
|
|
850
|
+
key: String(node.key ?? ""),
|
|
851
|
+
name: String(node.name ?? ""),
|
|
852
|
+
states: (node.states?.nodes ?? []).map((s) => mapState(s)).filter((s) => Boolean(s))
|
|
853
|
+
};
|
|
854
|
+
}
|
|
855
|
+
function resolveLinearTeam(teams, team) {
|
|
856
|
+
const t = team.trim();
|
|
857
|
+
if (!t) throw new Error("Linear team is required (id, key, or name from linear_list_teams)");
|
|
858
|
+
const lower = t.toLowerCase();
|
|
859
|
+
const found = teams.find(
|
|
860
|
+
(x) => x.id === t || x.key.toLowerCase() === lower || x.name.toLowerCase() === lower
|
|
861
|
+
);
|
|
862
|
+
if (!found) {
|
|
863
|
+
const keys = teams.map((x) => x.key).filter(Boolean).join(", ") || "(none)";
|
|
864
|
+
throw new Error(`Linear team not found: ${team}. Known keys: ${keys}`);
|
|
865
|
+
}
|
|
866
|
+
return found;
|
|
867
|
+
}
|
|
868
|
+
function resolveLinearState(team, state) {
|
|
869
|
+
const s = state.trim();
|
|
870
|
+
if (!s) throw new Error("Linear state is empty");
|
|
871
|
+
const lower = s.toLowerCase();
|
|
872
|
+
const found = team.states.find((x) => x.id === s) || team.states.find((x) => x.name.toLowerCase() === lower) || team.states.find((x) => x.type.toLowerCase() === lower);
|
|
873
|
+
if (!found) {
|
|
874
|
+
const available = team.states.map((x) => `${x.name} (${x.type})`).join(", ") || "(none)";
|
|
875
|
+
throw new Error(`Linear state not found on ${team.key}: ${state}. Available: ${available}`);
|
|
876
|
+
}
|
|
877
|
+
return found;
|
|
878
|
+
}
|
|
879
|
+
function normalizePriority(priority) {
|
|
880
|
+
if (priority == null) return void 0;
|
|
881
|
+
if (!Number.isInteger(priority) || priority < 0 || priority > 4) {
|
|
882
|
+
throw new Error("Linear priority must be 0 (none), 1 (urgent), 2 (high), 3 (medium), or 4 (low)");
|
|
883
|
+
}
|
|
884
|
+
return priority;
|
|
885
|
+
}
|
|
886
|
+
async function listLinearIssuesDirect(opts) {
|
|
887
|
+
const first = Math.max(1, Math.min(100, opts?.limit ?? 50));
|
|
888
|
+
const json = await linearGraphql(ASSIGNED_ISSUES_QUERY, { first }, opts);
|
|
889
|
+
return (json.viewer?.assignedIssues?.nodes ?? []).map((node) => toIssueInfo(mapIssue(node)));
|
|
890
|
+
}
|
|
891
|
+
async function listLinearTeams(opts) {
|
|
892
|
+
const json = await linearGraphql(TEAMS_QUERY, void 0, opts);
|
|
893
|
+
return {
|
|
894
|
+
viewer: {
|
|
895
|
+
id: String(json.viewer?.id ?? ""),
|
|
896
|
+
name: String(json.viewer?.name ?? "")
|
|
897
|
+
},
|
|
898
|
+
teams: (json.teams?.nodes ?? []).map(mapTeam)
|
|
899
|
+
};
|
|
900
|
+
}
|
|
901
|
+
async function getLinearIssue(id, opts) {
|
|
902
|
+
const issueId = id.trim();
|
|
903
|
+
if (!issueId) throw new Error("Linear issue id is required (uuid or ENG-123)");
|
|
904
|
+
const json = await linearGraphql(
|
|
905
|
+
ISSUE_QUERY,
|
|
906
|
+
{ id: issueId },
|
|
907
|
+
opts
|
|
908
|
+
);
|
|
909
|
+
if (!json.issue) {
|
|
910
|
+
throw new Error(`Linear issue not found: ${issueId}`);
|
|
911
|
+
}
|
|
912
|
+
return mapIssue(json.issue);
|
|
913
|
+
}
|
|
914
|
+
async function createLinearIssue(input, opts) {
|
|
915
|
+
const title = input.title.trim();
|
|
916
|
+
if (!title) throw new Error("Linear issue title is required");
|
|
917
|
+
const { viewer, teams } = await listLinearTeams(opts);
|
|
918
|
+
const team = resolveLinearTeam(teams, input.team);
|
|
919
|
+
const mutationInput = {
|
|
920
|
+
teamId: team.id,
|
|
921
|
+
title
|
|
922
|
+
};
|
|
923
|
+
const description = input.description?.trim();
|
|
924
|
+
if (description) mutationInput.description = description;
|
|
925
|
+
if (input.state?.trim()) {
|
|
926
|
+
mutationInput.stateId = resolveLinearState(team, input.state).id;
|
|
927
|
+
}
|
|
928
|
+
const assignee = input.assignee === void 0 ? void 0 : input.assignee?.trim() || null;
|
|
929
|
+
if (assignee === "me") mutationInput.assigneeId = viewer.id;
|
|
930
|
+
else if (assignee) mutationInput.assigneeId = assignee;
|
|
931
|
+
const priority = normalizePriority(input.priority);
|
|
932
|
+
if (priority != null) mutationInput.priority = priority;
|
|
933
|
+
const json = await linearGraphql(ISSUE_CREATE, { input: mutationInput }, opts);
|
|
934
|
+
if (!json.issueCreate?.success || !json.issueCreate.issue) {
|
|
935
|
+
throw new Error("Linear issueCreate failed");
|
|
936
|
+
}
|
|
937
|
+
return mapIssue(json.issueCreate.issue);
|
|
938
|
+
}
|
|
939
|
+
async function updateLinearIssue(input, opts) {
|
|
940
|
+
const issueId = input.id.trim();
|
|
941
|
+
if (!issueId) throw new Error("Linear issue id is required (uuid or ENG-123)");
|
|
942
|
+
const mutationInput = {};
|
|
943
|
+
if (input.title !== void 0) {
|
|
944
|
+
const title = input.title.trim();
|
|
945
|
+
if (!title) throw new Error("Linear issue title cannot be empty");
|
|
946
|
+
mutationInput.title = title;
|
|
947
|
+
}
|
|
948
|
+
if (input.description !== void 0) {
|
|
949
|
+
mutationInput.description = input.description;
|
|
950
|
+
}
|
|
951
|
+
if (input.state?.trim()) {
|
|
952
|
+
const existing = await getLinearIssue(issueId, opts);
|
|
953
|
+
const team = existing.team;
|
|
954
|
+
if (!team?.states.length) {
|
|
955
|
+
throw new Error(`Linear issue ${existing.identifier} has no workflow states to resolve "${input.state}"`);
|
|
956
|
+
}
|
|
957
|
+
mutationInput.stateId = resolveLinearState(team, input.state).id;
|
|
958
|
+
}
|
|
959
|
+
if (input.assignee !== void 0) {
|
|
960
|
+
const assignee = input.assignee?.trim() || null;
|
|
961
|
+
if (assignee === "me") {
|
|
962
|
+
const { viewer } = await listLinearTeams(opts);
|
|
963
|
+
mutationInput.assigneeId = viewer.id;
|
|
964
|
+
} else {
|
|
965
|
+
mutationInput.assigneeId = assignee;
|
|
966
|
+
}
|
|
967
|
+
}
|
|
968
|
+
if (input.priority !== void 0) {
|
|
969
|
+
mutationInput.priority = normalizePriority(input.priority);
|
|
970
|
+
}
|
|
971
|
+
if (Object.keys(mutationInput).length === 0) {
|
|
972
|
+
throw new Error("linear_update_issue needs at least one of title, description, state, assignee, priority");
|
|
973
|
+
}
|
|
974
|
+
const json = await linearGraphql(ISSUE_UPDATE, { id: issueId, input: mutationInput }, opts);
|
|
975
|
+
if (!json.issueUpdate?.success || !json.issueUpdate.issue) {
|
|
976
|
+
throw new Error("Linear issueUpdate failed");
|
|
977
|
+
}
|
|
978
|
+
return mapIssue(json.issueUpdate.issue);
|
|
979
|
+
}
|
|
980
|
+
async function commentLinearIssue(input, opts) {
|
|
981
|
+
const issueId = input.id.trim();
|
|
982
|
+
const body = input.body.trim();
|
|
983
|
+
if (!issueId) throw new Error("Linear issue id is required (uuid or ENG-123)");
|
|
984
|
+
if (!body) throw new Error("Linear comment body is required");
|
|
985
|
+
const json = await linearGraphql(COMMENT_CREATE, { input: { issueId, body } }, opts);
|
|
986
|
+
if (!json.commentCreate?.success || !json.commentCreate.comment?.id) {
|
|
987
|
+
throw new Error("Linear commentCreate failed");
|
|
988
|
+
}
|
|
989
|
+
return {
|
|
990
|
+
id: String(json.commentCreate.comment.id),
|
|
991
|
+
body: String(json.commentCreate.comment.body ?? body),
|
|
992
|
+
url: json.commentCreate.comment.url?.trim() || void 0
|
|
993
|
+
};
|
|
702
994
|
}
|
|
703
995
|
async function validateLinearApiKey(apiKey) {
|
|
704
996
|
const key = apiKey.trim();
|
|
@@ -843,9 +1135,9 @@ function toolFilePath(input) {
|
|
|
843
1135
|
if (!input) return void 0;
|
|
844
1136
|
return str(input.file_path) ?? str(input.path) ?? str(input.filePath) ?? str(input.filename);
|
|
845
1137
|
}
|
|
846
|
-
function countLines(
|
|
847
|
-
if (!
|
|
848
|
-
return
|
|
1138
|
+
function countLines(text3) {
|
|
1139
|
+
if (!text3) return 0;
|
|
1140
|
+
return text3.split("\n").length;
|
|
849
1141
|
}
|
|
850
1142
|
function diffFromInput(input) {
|
|
851
1143
|
if (!input) return {};
|
|
@@ -972,9 +1264,9 @@ function applyAgentEvent(parts, event) {
|
|
|
972
1264
|
function partsToAssistantText(parts) {
|
|
973
1265
|
return parts.filter((p) => p.type === "text").map((p) => p.text).join("").trim();
|
|
974
1266
|
}
|
|
975
|
-
function stripBrightsyNdjsonNoise(
|
|
976
|
-
if (!
|
|
977
|
-
let out =
|
|
1267
|
+
function stripBrightsyNdjsonNoise(text3) {
|
|
1268
|
+
if (!text3 || !text3.includes('"type"')) return text3;
|
|
1269
|
+
let out = text3;
|
|
978
1270
|
out = out.replace(
|
|
979
1271
|
/\{"type":"(?:tool_use|tool_result|tool|thinking|usage|done|error)"[\s\S]*?\}\s*(?=\{"type":"|$|(?=[A-Za-z*#]))/g,
|
|
980
1272
|
""
|
|
@@ -1002,17 +1294,35 @@ function sumOptional(a, b) {
|
|
|
1002
1294
|
if (a == null && b == null) return void 0;
|
|
1003
1295
|
return (a ?? 0) + (b ?? 0);
|
|
1004
1296
|
}
|
|
1297
|
+
function requestOccupancy(u) {
|
|
1298
|
+
return u.inputTokens + (u.cacheReadTokens ?? 0) + (u.cacheWriteTokens ?? 0);
|
|
1299
|
+
}
|
|
1005
1300
|
function mergeUsage(a, b) {
|
|
1006
1301
|
return {
|
|
1007
1302
|
inputTokens: (a?.inputTokens ?? 0) + b.inputTokens,
|
|
1008
1303
|
outputTokens: (a?.outputTokens ?? 0) + b.outputTokens,
|
|
1009
1304
|
cacheReadTokens: sumOptional(a?.cacheReadTokens, b.cacheReadTokens),
|
|
1010
|
-
cacheWriteTokens: sumOptional(a?.cacheWriteTokens, b.cacheWriteTokens)
|
|
1305
|
+
cacheWriteTokens: sumOptional(a?.cacheWriteTokens, b.cacheWriteTokens),
|
|
1306
|
+
lastRequestTokens: b.lastRequestTokens ?? a?.lastRequestTokens
|
|
1011
1307
|
};
|
|
1012
1308
|
}
|
|
1309
|
+
function applyTurnUsage(current, incoming, scope = "request") {
|
|
1310
|
+
if (scope === "turn") {
|
|
1311
|
+
return {
|
|
1312
|
+
...incoming,
|
|
1313
|
+
lastRequestTokens: current?.lastRequestTokens ?? requestOccupancy(incoming)
|
|
1314
|
+
};
|
|
1315
|
+
}
|
|
1316
|
+
const merged = mergeUsage(current, incoming);
|
|
1317
|
+
return { ...merged, lastRequestTokens: requestOccupancy(incoming) };
|
|
1318
|
+
}
|
|
1013
1319
|
function totalTokens(u) {
|
|
1014
1320
|
return u.inputTokens + u.outputTokens + (u.cacheReadTokens ?? 0) + (u.cacheWriteTokens ?? 0);
|
|
1015
1321
|
}
|
|
1322
|
+
function contextTokens(u) {
|
|
1323
|
+
if (u.lastRequestTokens != null && u.lastRequestTokens > 0) return u.lastRequestTokens;
|
|
1324
|
+
return requestOccupancy(u);
|
|
1325
|
+
}
|
|
1016
1326
|
|
|
1017
1327
|
// src/agents/spawn.ts
|
|
1018
1328
|
async function spawnAgentTurn(thread, input, onEvent) {
|
|
@@ -1022,9 +1332,9 @@ async function spawnAgentTurn(thread, input, onEvent) {
|
|
|
1022
1332
|
`Cannot spawn ${thread.agent}: thread ${thread.id} has no worktreePath`
|
|
1023
1333
|
);
|
|
1024
1334
|
}
|
|
1025
|
-
const { isGlobalThread: isGlobalThread2 } = await import("./global-workspace-
|
|
1335
|
+
const { isGlobalThread: isGlobalThread2 } = await import("./global-workspace-YNMNO4CL.js");
|
|
1026
1336
|
if (isGlobalThread2(thread)) {
|
|
1027
|
-
const { ensureGlobalCoordinatorCwd: ensureGlobalCoordinatorCwd2 } = await import("./coordinator-prompt-
|
|
1337
|
+
const { ensureGlobalCoordinatorCwd: ensureGlobalCoordinatorCwd2 } = await import("./coordinator-prompt-LLFJUO2R.js");
|
|
1028
1338
|
ensureGlobalCoordinatorCwd2(
|
|
1029
1339
|
isOrchestratorThread(thread) ? { orchestratorThreadId: thread.id } : void 0
|
|
1030
1340
|
);
|
|
@@ -1086,7 +1396,7 @@ async function spawnAgentTurn(thread, input, onEvent) {
|
|
|
1086
1396
|
continue;
|
|
1087
1397
|
}
|
|
1088
1398
|
if (parsed.type === "usage") {
|
|
1089
|
-
usage =
|
|
1399
|
+
usage = applyTurnUsage(usage, parsed.data, parsed.scope ?? "request");
|
|
1090
1400
|
onEvent(parsed);
|
|
1091
1401
|
continue;
|
|
1092
1402
|
}
|
|
@@ -1111,8 +1421,8 @@ async function spawnAgentTurn(thread, input, onEvent) {
|
|
|
1111
1421
|
onEvent({ type: "exit", data: exitCode });
|
|
1112
1422
|
const finalized = finalizeParts(parts);
|
|
1113
1423
|
const rawText = assistantText.trim() || partsToAssistantText(finalized);
|
|
1114
|
-
const
|
|
1115
|
-
return { exitCode, sessionId, assistantText:
|
|
1424
|
+
const text3 = thread.agent === "brightsy" ? stripBrightsyNdjsonNoise(rawText) : rawText;
|
|
1425
|
+
return { exitCode, sessionId, assistantText: text3, parts: finalized, usage };
|
|
1116
1426
|
});
|
|
1117
1427
|
return {
|
|
1118
1428
|
pid: child.pid,
|
|
@@ -1141,7 +1451,7 @@ function formatRenameBranchDirective(thread, opts) {
|
|
|
1141
1451
|
"- Rename the git branch to a short kebab-case name that describes this task (what you are changing), e.g. `fix/panel-width` or `feat/dark-mode`:",
|
|
1142
1452
|
" `git branch -m <new-name>`",
|
|
1143
1453
|
"- Prefer Conventional Commits style prefixes when they fit (`fix/`, `feat/`, `chore/`, `docs/`).",
|
|
1144
|
-
"- Never push
|
|
1454
|
+
"- Never push this placeholder to main/master."
|
|
1145
1455
|
];
|
|
1146
1456
|
const custom = opts?.customPrompt?.trim();
|
|
1147
1457
|
if (custom) {
|
|
@@ -1184,7 +1494,7 @@ function formatWorktreeDirective(thread, opts) {
|
|
|
1184
1494
|
"- Prefer a concise imperative title (Conventional Commits style when it fits: feat:/fix:/chore:/docs:). Body should summarize intent, key changes, and test notes."
|
|
1185
1495
|
);
|
|
1186
1496
|
lines.push(
|
|
1187
|
-
"- Commit with messages that state the purpose of the change (same standard as the PR). Stay on this thread branch
|
|
1497
|
+
"- Commit with messages that state the purpose of the change (same standard as the PR). Stay on this thread branch. Never push directly to main/master or merge locally into the main checkout. When asked to merge the PR, use GitHub from this worktree (`gh pr merge` / `gh stack merge`)."
|
|
1188
1498
|
);
|
|
1189
1499
|
if (thread.prUrl) {
|
|
1190
1500
|
lines.push(
|
|
@@ -1216,13 +1526,13 @@ function formatWorktreeDirective(thread, opts) {
|
|
|
1216
1526
|
'- "Fix CI: <name>." \u2192 investigate that failing check, fix it, commit, and push.'
|
|
1217
1527
|
);
|
|
1218
1528
|
lines.push(
|
|
1219
|
-
'- "Update the branch." / "Fix merge conflicts." \u2192 sync with the PR base (merge or rebase), resolve conflicts carefully, commit, and push until the PR is mergeable.'
|
|
1529
|
+
'- "Update the branch." / "Fix merge conflicts." / "Merge origin/<base> into this branch. Then push." \u2192 sync with the PR base (merge or rebase), resolve conflicts carefully, commit, and push until the PR is mergeable.'
|
|
1220
1530
|
);
|
|
1221
1531
|
lines.push(
|
|
1222
1532
|
'- "Address review comments." \u2192 read PR review feedback, make the requested changes, commit, and push.'
|
|
1223
1533
|
);
|
|
1224
1534
|
lines.push(
|
|
1225
|
-
'- "Merge PR." \u2192 merge this thread\'s open pull request
|
|
1535
|
+
'- "Merge PR." \u2192 merge this thread\'s open pull request on GitHub. If `gh stack view` shows a stack, use `gh stack merge`; otherwise `gh pr merge` (respect repo defaults / squash vs merge). Do not force-push main/master or merge locally into the main checkout.'
|
|
1226
1536
|
);
|
|
1227
1537
|
return lines.join("\n");
|
|
1228
1538
|
}
|
|
@@ -1977,13 +2287,13 @@ function formatStat(files) {
|
|
|
1977
2287
|
return `${n} file${n === 1 ? "" : "s"} changed, ${additions} insertions(+), ${deletions} deletions(-)`;
|
|
1978
2288
|
}
|
|
1979
2289
|
function emptyScopeStats() {
|
|
1980
|
-
const
|
|
2290
|
+
const z4 = emptyStat();
|
|
1981
2291
|
return {
|
|
1982
|
-
commits:
|
|
1983
|
-
uncommitted:
|
|
1984
|
-
staged:
|
|
1985
|
-
unstaged:
|
|
1986
|
-
last_turn:
|
|
2292
|
+
commits: z4,
|
|
2293
|
+
uncommitted: z4,
|
|
2294
|
+
staged: z4,
|
|
2295
|
+
unstaged: z4,
|
|
2296
|
+
last_turn: z4
|
|
1987
2297
|
};
|
|
1988
2298
|
}
|
|
1989
2299
|
function filesFromDiff(nameStatus, numstat, combinedDiff, maxHunk) {
|
|
@@ -2254,10 +2564,10 @@ async function getDiff(worktreePath, repoPath, opts) {
|
|
|
2254
2564
|
listBranchCommits(worktreePath, repoPath, { base: baseLabel }),
|
|
2255
2565
|
isDirty(worktreePath),
|
|
2256
2566
|
countUnpushedCommits(worktreePath)
|
|
2257
|
-
]).then(([scopeStats, commits,
|
|
2567
|
+
]).then(([scopeStats, commits, dirty2, unpushed]) => ({
|
|
2258
2568
|
scopeStats,
|
|
2259
2569
|
commits,
|
|
2260
|
-
dirty,
|
|
2570
|
+
dirty: dirty2,
|
|
2261
2571
|
unpushed
|
|
2262
2572
|
})) : Promise.resolve({
|
|
2263
2573
|
scopeStats: emptyScopeStats(),
|
|
@@ -2334,13 +2644,14 @@ async function getDiff(worktreePath, repoPath, opts) {
|
|
|
2334
2644
|
}
|
|
2335
2645
|
}
|
|
2336
2646
|
const files = [...filesMap.values()].sort((a, b) => a.path.localeCompare(b.path));
|
|
2647
|
+
const dirty = includeMeta ? meta.dirty : scope === "commits" ? false : files.length > 0;
|
|
2337
2648
|
return {
|
|
2338
2649
|
scope,
|
|
2339
2650
|
commitSha: scope === "commits" ? commitSha : null,
|
|
2340
2651
|
base: labelBase,
|
|
2341
2652
|
files,
|
|
2342
2653
|
stat: formatStat(files),
|
|
2343
|
-
dirty
|
|
2654
|
+
dirty,
|
|
2344
2655
|
unpushed: meta.unpushed,
|
|
2345
2656
|
hasLastTurnBase,
|
|
2346
2657
|
commits: meta.commits,
|
|
@@ -2972,16 +3283,16 @@ var PASTE_ATTACH_MIN_CHARS = 1200;
|
|
|
2972
3283
|
var PASTE_ATTACH_MIN_LINES = 15;
|
|
2973
3284
|
var PASTED_NAME_RE = /^Pasted text #(\d+)\.txt$/i;
|
|
2974
3285
|
var PASTED_NAME_ALT_RE = /^pasted-(\d+)\.txt$/i;
|
|
2975
|
-
function pastedTextStats(
|
|
2976
|
-
const chars =
|
|
3286
|
+
function pastedTextStats(text3) {
|
|
3287
|
+
const chars = text3.length;
|
|
2977
3288
|
if (chars === 0) return { chars: 0, lines: 0 };
|
|
2978
|
-
const lines =
|
|
3289
|
+
const lines = text3.split(/\r\n|\r|\n/).length;
|
|
2979
3290
|
return { chars, lines };
|
|
2980
3291
|
}
|
|
2981
|
-
function shouldAttachPastedText(
|
|
2982
|
-
const trimmed =
|
|
3292
|
+
function shouldAttachPastedText(text3) {
|
|
3293
|
+
const trimmed = text3.trim();
|
|
2983
3294
|
if (!trimmed) return false;
|
|
2984
|
-
const { chars, lines } = pastedTextStats(
|
|
3295
|
+
const { chars, lines } = pastedTextStats(text3);
|
|
2985
3296
|
return chars >= PASTE_ATTACH_MIN_CHARS || lines >= PASTE_ATTACH_MIN_LINES;
|
|
2986
3297
|
}
|
|
2987
3298
|
function nextPastedTextName(existing) {
|
|
@@ -2992,13 +3303,13 @@ function nextPastedTextName(existing) {
|
|
|
2992
3303
|
}
|
|
2993
3304
|
return `Pasted text #${max + 1}.txt`;
|
|
2994
3305
|
}
|
|
2995
|
-
function buildPastedTextAttachment(
|
|
3306
|
+
function buildPastedTextAttachment(text3, opts) {
|
|
2996
3307
|
return {
|
|
2997
3308
|
id: opts?.id ?? randomUUID2(),
|
|
2998
3309
|
name: opts?.name ?? "Pasted text #1.txt",
|
|
2999
3310
|
kind: "file",
|
|
3000
3311
|
path: opts?.path,
|
|
3001
|
-
content:
|
|
3312
|
+
content: text3
|
|
3002
3313
|
};
|
|
3003
3314
|
}
|
|
3004
3315
|
|
|
@@ -3046,9 +3357,9 @@ async function tryClaudeSummary(transcript, opts) {
|
|
|
3046
3357
|
{ cwd: opts?.cwd, reject: false }
|
|
3047
3358
|
);
|
|
3048
3359
|
if (exitCode !== 0) return null;
|
|
3049
|
-
const
|
|
3050
|
-
if (
|
|
3051
|
-
return
|
|
3360
|
+
const text3 = stdout.trim();
|
|
3361
|
+
if (text3.length < 40) return null;
|
|
3362
|
+
return text3;
|
|
3052
3363
|
} catch {
|
|
3053
3364
|
return null;
|
|
3054
3365
|
}
|
|
@@ -3097,9 +3408,9 @@ function extractiveSummary(transcript) {
|
|
|
3097
3408
|
);
|
|
3098
3409
|
return parts.join("\n");
|
|
3099
3410
|
}
|
|
3100
|
-
function clipSummary(
|
|
3101
|
-
if (
|
|
3102
|
-
return `${
|
|
3411
|
+
function clipSummary(text3) {
|
|
3412
|
+
if (text3.length <= MAX_SUMMARY_CHARS) return text3;
|
|
3413
|
+
return `${text3.slice(0, MAX_SUMMARY_CHARS)}
|
|
3103
3414
|
|
|
3104
3415
|
[\u2026summary truncated\u2026]`;
|
|
3105
3416
|
}
|
|
@@ -3501,7 +3812,7 @@ async function createThread(input, _onSetupLine) {
|
|
|
3501
3812
|
return readThread(thread.id) ?? thread;
|
|
3502
3813
|
}
|
|
3503
3814
|
async function listLinearIssues(agent, repoPath) {
|
|
3504
|
-
const { getAdapter: getAdapter2 } = await import("./agents-
|
|
3815
|
+
const { getAdapter: getAdapter2 } = await import("./agents-XJV6J3K2.js");
|
|
3505
3816
|
await requireAgent(agent, { requireLinear: true });
|
|
3506
3817
|
const adapter = getAdapter2(agent);
|
|
3507
3818
|
if (!adapter.listLinearIssues) {
|
|
@@ -3999,13 +4310,13 @@ function resolveConductorCursorAgentId(workspacePath) {
|
|
|
3999
4310
|
for (const hash of hashes) {
|
|
4000
4311
|
const agentsFile = join7(CURSOR_SDK_STORE, hash, "agents.ndjson");
|
|
4001
4312
|
if (!existsSync9(agentsFile)) continue;
|
|
4002
|
-
let
|
|
4313
|
+
let text3;
|
|
4003
4314
|
try {
|
|
4004
|
-
|
|
4315
|
+
text3 = readFileSync7(agentsFile, "utf8");
|
|
4005
4316
|
} catch {
|
|
4006
4317
|
continue;
|
|
4007
4318
|
}
|
|
4008
|
-
for (const line of
|
|
4319
|
+
for (const line of text3.split("\n")) {
|
|
4009
4320
|
const trimmed = line.trim();
|
|
4010
4321
|
if (!trimmed) continue;
|
|
4011
4322
|
try {
|
|
@@ -4044,7 +4355,7 @@ async function adoptThread(input) {
|
|
|
4044
4355
|
messages: input.messages ?? []
|
|
4045
4356
|
});
|
|
4046
4357
|
writeThread(thread);
|
|
4047
|
-
const { ensureWorkspace: ensureWorkspace2 } = await import("./workspaces-
|
|
4358
|
+
const { ensureWorkspace: ensureWorkspace2 } = await import("./workspaces-O3U5BENH.js");
|
|
4048
4359
|
await ensureWorkspace2(repoPath);
|
|
4049
4360
|
return thread;
|
|
4050
4361
|
}
|
|
@@ -4495,8 +4806,8 @@ ${input.permalink}` : "";
|
|
|
4495
4806
|
|
|
4496
4807
|
${body}${link}`;
|
|
4497
4808
|
}
|
|
4498
|
-
function isSlackExternalReplyPrompt(
|
|
4499
|
-
return
|
|
4809
|
+
function isSlackExternalReplyPrompt(text3) {
|
|
4810
|
+
return text3.startsWith("Slack reply from ") && text3.includes("not a command");
|
|
4500
4811
|
}
|
|
4501
4812
|
function pendingSlackExternalReplies(messages) {
|
|
4502
4813
|
let i = messages.length - 1;
|
|
@@ -4520,9 +4831,9 @@ function formatSlackRepliesForTurn(replies) {
|
|
|
4520
4831
|
function listSlackOutboundWatches() {
|
|
4521
4832
|
return pruneWatches(readStore3());
|
|
4522
4833
|
}
|
|
4523
|
-
function formatOwnerSlackFyi(userName,
|
|
4834
|
+
function formatOwnerSlackFyi(userName, text3) {
|
|
4524
4835
|
const who = userName.trim() || "Someone";
|
|
4525
|
-
const body =
|
|
4836
|
+
const body = text3.trim() || "(no text)";
|
|
4526
4837
|
return `${who} replied in Slack:
|
|
4527
4838
|
${body}`;
|
|
4528
4839
|
}
|
|
@@ -4537,7 +4848,7 @@ async function relayExternalReply(opts) {
|
|
|
4537
4848
|
const thread = readThread(threadId);
|
|
4538
4849
|
if (!thread || thread.status === "archived") return true;
|
|
4539
4850
|
try {
|
|
4540
|
-
const
|
|
4851
|
+
const text3 = formatSlackExternalReplyPrompt({
|
|
4541
4852
|
userName: opts.reply.userName,
|
|
4542
4853
|
kind: opts.watch.kind,
|
|
4543
4854
|
toLabel: opts.watch.toLabel,
|
|
@@ -4546,7 +4857,7 @@ async function relayExternalReply(opts) {
|
|
|
4546
4857
|
});
|
|
4547
4858
|
appendMessage(threadId, {
|
|
4548
4859
|
role: "agent",
|
|
4549
|
-
text:
|
|
4860
|
+
text: text3,
|
|
4550
4861
|
ts: (/* @__PURE__ */ new Date()).toISOString()
|
|
4551
4862
|
});
|
|
4552
4863
|
} catch {
|
|
@@ -4743,7 +5054,7 @@ async function refreshSlackReplyBadges(opts) {
|
|
|
4743
5054
|
const messages = await fetchMessages(token, watch, opts?.fetchImpl);
|
|
4744
5055
|
const replies = messages.filter((m) => isHumanReply(m, watch)).sort((a, b) => Number(a.ts) - Number(b.ts));
|
|
4745
5056
|
if (replies.length === 0) continue;
|
|
4746
|
-
const
|
|
5057
|
+
const injected2 = new Set(watch.injectedReplyTs ?? []);
|
|
4747
5058
|
const collected = [...watch.replies ?? []];
|
|
4748
5059
|
let lastSeenTs = watch.lastSeenTs;
|
|
4749
5060
|
let latestUser;
|
|
@@ -4777,7 +5088,7 @@ async function refreshSlackReplyBadges(opts) {
|
|
|
4777
5088
|
latestName = replyUserName;
|
|
4778
5089
|
latestText = reply.text;
|
|
4779
5090
|
latestPermalink = permalink;
|
|
4780
|
-
if (
|
|
5091
|
+
if (injected2.has(ts)) {
|
|
4781
5092
|
lastSeenTs = ts;
|
|
4782
5093
|
continue;
|
|
4783
5094
|
}
|
|
@@ -4788,7 +5099,7 @@ async function refreshSlackReplyBadges(opts) {
|
|
|
4788
5099
|
fetchImpl: opts?.fetchImpl
|
|
4789
5100
|
});
|
|
4790
5101
|
if (!delivered) break;
|
|
4791
|
-
|
|
5102
|
+
injected2.add(ts);
|
|
4792
5103
|
lastSeenTs = ts;
|
|
4793
5104
|
}
|
|
4794
5105
|
watches[i] = {
|
|
@@ -4800,7 +5111,7 @@ async function refreshSlackReplyBadges(opts) {
|
|
|
4800
5111
|
replyTs: lastSeenTs,
|
|
4801
5112
|
replyPreview: latestText.slice(0, 140),
|
|
4802
5113
|
permalink: latestPermalink,
|
|
4803
|
-
injectedReplyTs: [...
|
|
5114
|
+
injectedReplyTs: [...injected2],
|
|
4804
5115
|
replies: collected.slice(-MAX_REPLIES_PER_WATCH)
|
|
4805
5116
|
};
|
|
4806
5117
|
changed = true;
|
|
@@ -5377,8 +5688,8 @@ function buildQuotaHandoffAttachment(from, limitText, fallbackAgent) {
|
|
|
5377
5688
|
);
|
|
5378
5689
|
const recent = from.messages.slice(-8).map((m) => {
|
|
5379
5690
|
const role = m.role === "user" ? "User" : m.role === "agent" ? "Agent" : "Summary";
|
|
5380
|
-
const
|
|
5381
|
-
return
|
|
5691
|
+
const text3 = m.text.trim().replace(/\s+/g, " ").slice(0, 280);
|
|
5692
|
+
return text3 ? `- ${role}: ${text3}` : null;
|
|
5382
5693
|
}).filter(Boolean);
|
|
5383
5694
|
const body = [
|
|
5384
5695
|
`# Orchestration handoff`,
|
|
@@ -5798,11 +6109,11 @@ var Orchestrator = class {
|
|
|
5798
6109
|
return results;
|
|
5799
6110
|
}
|
|
5800
6111
|
/** Edit the text of a not-yet-started queued message. */
|
|
5801
|
-
async editQueuedMessage(threadRef, index,
|
|
6112
|
+
async editQueuedMessage(threadRef, index, text3) {
|
|
5802
6113
|
const thread = this.requireThread(threadRef);
|
|
5803
6114
|
return withThreadLock(thread.id, async () => {
|
|
5804
6115
|
const current = this.requireThread(thread.id);
|
|
5805
|
-
const trimmed =
|
|
6116
|
+
const trimmed = text3.trim();
|
|
5806
6117
|
if (!trimmed || index < 0 || index >= current.queue.length) {
|
|
5807
6118
|
return current;
|
|
5808
6119
|
}
|
|
@@ -6562,6 +6873,14 @@ var Orchestrator = class {
|
|
|
6562
6873
|
throw new Error(`${action} is not available on the global coordinator`);
|
|
6563
6874
|
}
|
|
6564
6875
|
}
|
|
6876
|
+
/** MCP set_caffeinate is a detached hold — closing the chat must not leave the Mac awake. */
|
|
6877
|
+
releaseOrchestratorCaffeinate(thread) {
|
|
6878
|
+
if (!isOrchestratorThread(thread)) return;
|
|
6879
|
+
try {
|
|
6880
|
+
releaseCaffeinateHoldForThread(thread.id);
|
|
6881
|
+
} catch {
|
|
6882
|
+
}
|
|
6883
|
+
}
|
|
6565
6884
|
async diff(threadRef, opts) {
|
|
6566
6885
|
const thread = this.requireThread(threadRef);
|
|
6567
6886
|
this.assertNotGlobal(thread, "Diff");
|
|
@@ -6831,6 +7150,36 @@ var Orchestrator = class {
|
|
|
6831
7150
|
this.emit({ type: "status_changed", threadId: tab.id, status: tab.status });
|
|
6832
7151
|
return tab;
|
|
6833
7152
|
}
|
|
7153
|
+
/**
|
|
7154
|
+
* Queue a desktop-git-button prompt on a worktree agent (commit/push/PR/merge).
|
|
7155
|
+
* Orchestrators use this instead of running git/gh from the synthetic home.
|
|
7156
|
+
*/
|
|
7157
|
+
async askGit(threadRef, action) {
|
|
7158
|
+
if (!AGENT_GIT_ACTIONS.includes(action)) {
|
|
7159
|
+
throw new Error(`Unknown git action: ${action}`);
|
|
7160
|
+
}
|
|
7161
|
+
const thread = this.requireThread(threadRef);
|
|
7162
|
+
this.assertNotGlobal(thread, "ask_git");
|
|
7163
|
+
if (isOrchestratorThread(thread)) {
|
|
7164
|
+
throw new Error(
|
|
7165
|
+
"ask_git targets a worktree agent thread (not the orchestrator). Pass a child/worktree thread ref."
|
|
7166
|
+
);
|
|
7167
|
+
}
|
|
7168
|
+
if (action === "merge" && !thread.prUrl) {
|
|
7169
|
+
throw new Error(
|
|
7170
|
+
"No pull request linked. Ask the worktree agent to open a draft PR first (ask_git create-draft)."
|
|
7171
|
+
);
|
|
7172
|
+
}
|
|
7173
|
+
let prBase;
|
|
7174
|
+
if (action === "resolve-conflicts") {
|
|
7175
|
+
try {
|
|
7176
|
+
const details = await this.getPrDetails(threadRef);
|
|
7177
|
+
prBase = details?.baseRefName?.trim() || void 0;
|
|
7178
|
+
} catch {
|
|
7179
|
+
}
|
|
7180
|
+
}
|
|
7181
|
+
return this.send(threadRef, agentGitPrompt(action, { prBase }));
|
|
7182
|
+
}
|
|
6834
7183
|
setThreadOptions(threadRef, patch) {
|
|
6835
7184
|
const thread = this.requireThread(threadRef);
|
|
6836
7185
|
const next = {};
|
|
@@ -6901,6 +7250,7 @@ var Orchestrator = class {
|
|
|
6901
7250
|
async archive(threadRef) {
|
|
6902
7251
|
const thread = this.requireThread(threadRef);
|
|
6903
7252
|
this.stop(thread.id);
|
|
7253
|
+
this.releaseOrchestratorCaffeinate(thread);
|
|
6904
7254
|
if (isGlobalThread(thread)) {
|
|
6905
7255
|
const archived2 = setStatus(thread.id, "archived");
|
|
6906
7256
|
this.emit({ type: "status_changed", threadId: archived2.id, status: "archived" });
|
|
@@ -6925,7 +7275,7 @@ var Orchestrator = class {
|
|
|
6925
7275
|
this.emit({ type: "status_changed", threadId: archived.id, status: "archived" });
|
|
6926
7276
|
if (thread.repoPath && !isGlobalRepoPath(thread.repoPath)) {
|
|
6927
7277
|
try {
|
|
6928
|
-
const { ensureWorkspace: ensureWorkspace2 } = await import("./workspaces-
|
|
7278
|
+
const { ensureWorkspace: ensureWorkspace2 } = await import("./workspaces-O3U5BENH.js");
|
|
6929
7279
|
await ensureWorkspace2(thread.repoPath);
|
|
6930
7280
|
} catch {
|
|
6931
7281
|
}
|
|
@@ -6935,6 +7285,7 @@ var Orchestrator = class {
|
|
|
6935
7285
|
async purge(threadRef, opts) {
|
|
6936
7286
|
const thread = this.requireThread(threadRef);
|
|
6937
7287
|
this.stop(thread.id);
|
|
7288
|
+
this.releaseOrchestratorCaffeinate(thread);
|
|
6938
7289
|
if (isGlobalThread(thread)) {
|
|
6939
7290
|
deleteThreadRecord(thread.id);
|
|
6940
7291
|
return;
|
|
@@ -7198,7 +7549,7 @@ function formatPlanQuestionsForChat(questions) {
|
|
|
7198
7549
|
// src/mcp/server.ts
|
|
7199
7550
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
7200
7551
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
7201
|
-
import { z as
|
|
7552
|
+
import { z as z3 } from "zod";
|
|
7202
7553
|
import { basename as basename4 } from "path";
|
|
7203
7554
|
|
|
7204
7555
|
// src/mcp/archive-guard.ts
|
|
@@ -7426,8 +7777,8 @@ function labelGithubUrl(url) {
|
|
|
7426
7777
|
}
|
|
7427
7778
|
return { kind: "other", label: "GitHub", url: raw };
|
|
7428
7779
|
}
|
|
7429
|
-
function appendGithubLink(
|
|
7430
|
-
const body =
|
|
7780
|
+
function appendGithubLink(text3, githubUrl) {
|
|
7781
|
+
const body = text3.trimEnd();
|
|
7431
7782
|
if (!githubUrl?.trim()) return body;
|
|
7432
7783
|
const labeled = labelGithubUrl(githubUrl);
|
|
7433
7784
|
if (!labeled) {
|
|
@@ -7710,6 +8061,101 @@ function registerSlackTools(server) {
|
|
|
7710
8061
|
);
|
|
7711
8062
|
}
|
|
7712
8063
|
|
|
8064
|
+
// src/mcp/linear-tools.ts
|
|
8065
|
+
import { z as z2 } from "zod";
|
|
8066
|
+
function text2(payload, isError = false) {
|
|
8067
|
+
return {
|
|
8068
|
+
content: [{ type: "text", text: JSON.stringify(payload, null, 2) }],
|
|
8069
|
+
...isError ? { isError: true } : {}
|
|
8070
|
+
};
|
|
8071
|
+
}
|
|
8072
|
+
function fail2(err) {
|
|
8073
|
+
return text2(
|
|
8074
|
+
{ error: err instanceof Error ? err.message : String(err) },
|
|
8075
|
+
true
|
|
8076
|
+
);
|
|
8077
|
+
}
|
|
8078
|
+
var prioritySchema = z2.number().int().min(0).max(4).optional().describe("0 none, 1 urgent, 2 high, 3 medium, 4 low");
|
|
8079
|
+
function registerLinearTools(server) {
|
|
8080
|
+
server.tool(
|
|
8081
|
+
"linear_list_teams",
|
|
8082
|
+
"List Linear teams (id, key, name) and workflow states for the connected Account. Use team key (e.g. ENG) on linear_create_issue; use state name or type (started, completed) on create/update. Viewer id is for assignee=me.",
|
|
8083
|
+
{},
|
|
8084
|
+
async () => {
|
|
8085
|
+
try {
|
|
8086
|
+
return text2(await listLinearTeams());
|
|
8087
|
+
} catch (err) {
|
|
8088
|
+
return fail2(err);
|
|
8089
|
+
}
|
|
8090
|
+
}
|
|
8091
|
+
);
|
|
8092
|
+
server.tool(
|
|
8093
|
+
"linear_get_issue",
|
|
8094
|
+
"Get a Linear issue by uuid or identifier (ENG-123).",
|
|
8095
|
+
{ id: z2.string() },
|
|
8096
|
+
async ({ id }) => {
|
|
8097
|
+
try {
|
|
8098
|
+
return text2(await getLinearIssue(id));
|
|
8099
|
+
} catch (err) {
|
|
8100
|
+
return fail2(err);
|
|
8101
|
+
}
|
|
8102
|
+
}
|
|
8103
|
+
);
|
|
8104
|
+
server.tool(
|
|
8105
|
+
"linear_create_issue",
|
|
8106
|
+
'Create a Linear issue. Call linear_list_teams first. team is id, key, or name. state is name, type (unstarted/started/completed/canceled/backlog), or id. assignee is "me" or a user id.',
|
|
8107
|
+
{
|
|
8108
|
+
team: z2.string(),
|
|
8109
|
+
title: z2.string(),
|
|
8110
|
+
description: z2.string().optional(),
|
|
8111
|
+
state: z2.string().optional(),
|
|
8112
|
+
assignee: z2.string().nullable().optional(),
|
|
8113
|
+
priority: prioritySchema
|
|
8114
|
+
},
|
|
8115
|
+
async (args) => {
|
|
8116
|
+
try {
|
|
8117
|
+
return text2(await createLinearIssue(args));
|
|
8118
|
+
} catch (err) {
|
|
8119
|
+
return fail2(err);
|
|
8120
|
+
}
|
|
8121
|
+
}
|
|
8122
|
+
);
|
|
8123
|
+
server.tool(
|
|
8124
|
+
"linear_update_issue",
|
|
8125
|
+
'Update a Linear issue (uuid or ENG-123). Pass at least one of title, description, state, assignee, priority. state is name, type, or id. assignee is "me", a user id, or null to unassign.',
|
|
8126
|
+
{
|
|
8127
|
+
id: z2.string(),
|
|
8128
|
+
title: z2.string().optional(),
|
|
8129
|
+
description: z2.string().optional(),
|
|
8130
|
+
state: z2.string().optional(),
|
|
8131
|
+
assignee: z2.string().nullable().optional(),
|
|
8132
|
+
priority: prioritySchema
|
|
8133
|
+
},
|
|
8134
|
+
async (args) => {
|
|
8135
|
+
try {
|
|
8136
|
+
return text2(await updateLinearIssue(args));
|
|
8137
|
+
} catch (err) {
|
|
8138
|
+
return fail2(err);
|
|
8139
|
+
}
|
|
8140
|
+
}
|
|
8141
|
+
);
|
|
8142
|
+
server.tool(
|
|
8143
|
+
"linear_comment",
|
|
8144
|
+
"Add a markdown comment on a Linear issue (uuid or ENG-123).",
|
|
8145
|
+
{
|
|
8146
|
+
id: z2.string(),
|
|
8147
|
+
body: z2.string()
|
|
8148
|
+
},
|
|
8149
|
+
async (args) => {
|
|
8150
|
+
try {
|
|
8151
|
+
return text2(await commentLinearIssue(args));
|
|
8152
|
+
} catch (err) {
|
|
8153
|
+
return fail2(err);
|
|
8154
|
+
}
|
|
8155
|
+
}
|
|
8156
|
+
);
|
|
8157
|
+
}
|
|
8158
|
+
|
|
7713
8159
|
// src/mcp/server.ts
|
|
7714
8160
|
var MAX_ORCH_THREADS = 5;
|
|
7715
8161
|
var CREATE_THREAD_TIMEOUT_MS = 9e4;
|
|
@@ -7786,7 +8232,7 @@ async function startMcpServer() {
|
|
|
7786
8232
|
server.tool(
|
|
7787
8233
|
"get_thread",
|
|
7788
8234
|
"Get a compact thread summary by id/ref",
|
|
7789
|
-
{ ref:
|
|
8235
|
+
{ ref: z3.string() },
|
|
7790
8236
|
async ({ ref }) => {
|
|
7791
8237
|
const t = orch.getThread(ref);
|
|
7792
8238
|
if (!t) {
|
|
@@ -7815,14 +8261,14 @@ async function startMcpServer() {
|
|
|
7815
8261
|
"present_artifact",
|
|
7816
8262
|
"Show an HTML, SVG, markdown, or React document in Sideboard\u2019s Claude-style side column (desktop). Use instead of claude.ai\u2019s artifact tool \u2014 pass the full document content. Prefer type=html for interactive pages. For type=react, pass a single component module that `export default`s a component (JSX/TSX ok) \u2014 Sideboard bootstraps React/ReactDOM/Babel and renders it; only `react`/`react-dom` imports are available, no other npm packages.",
|
|
7817
8263
|
{
|
|
7818
|
-
title:
|
|
7819
|
-
type:
|
|
8264
|
+
title: z3.string().describe("Short title shown in the artifact pane header"),
|
|
8265
|
+
type: z3.enum(["html", "svg", "markdown", "react"]).describe(
|
|
7820
8266
|
"Artifact kind \u2014 html opens an iframe preview; react transpiles and renders a default-exported component in a sandboxed iframe"
|
|
7821
8267
|
),
|
|
7822
|
-
content:
|
|
8268
|
+
content: z3.string().describe(
|
|
7823
8269
|
"Full document body (complete HTML page, SVG markup, markdown, or a React component module with a default export)"
|
|
7824
8270
|
),
|
|
7825
|
-
artifact_id:
|
|
8271
|
+
artifact_id: z3.string().optional().describe("Stable id when updating the same artifact across turns")
|
|
7826
8272
|
},
|
|
7827
8273
|
async ({ title, type, artifact_id }) => {
|
|
7828
8274
|
const id = artifact_id?.trim() || `artifact_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 8)}`;
|
|
@@ -7840,15 +8286,15 @@ async function startMcpServer() {
|
|
|
7840
8286
|
"ask_user",
|
|
7841
8287
|
"Ask the user clarifying multiple-choice questions in Sideboard\u2019s composer (plan mode). Before calling, write a short chat message explaining the decision and what each option means. Include a description on every option. Use for approach forks and requirements \u2014 not for \u201Cis the plan ready?\u201D. After calling, stop and wait for their next message with answers.",
|
|
7842
8288
|
{
|
|
7843
|
-
questions:
|
|
7844
|
-
|
|
7845
|
-
question:
|
|
7846
|
-
header:
|
|
7847
|
-
multiSelect:
|
|
7848
|
-
options:
|
|
7849
|
-
|
|
7850
|
-
label:
|
|
7851
|
-
description:
|
|
8289
|
+
questions: z3.array(
|
|
8290
|
+
z3.object({
|
|
8291
|
+
question: z3.string().describe("Full question text ending with ?"),
|
|
8292
|
+
header: z3.string().max(24).optional().describe("Short label shown above the question"),
|
|
8293
|
+
multiSelect: z3.boolean().optional().describe("Allow selecting multiple options"),
|
|
8294
|
+
options: z3.array(
|
|
8295
|
+
z3.object({
|
|
8296
|
+
label: z3.string(),
|
|
8297
|
+
description: z3.string().optional().describe("What this option means / when to choose it (strongly preferred)")
|
|
7852
8298
|
})
|
|
7853
8299
|
).min(2).max(6).describe("2\u20136 choices (Sideboard also offers Other)")
|
|
7854
8300
|
})
|
|
@@ -7867,9 +8313,9 @@ async function startMcpServer() {
|
|
|
7867
8313
|
"present_plan",
|
|
7868
8314
|
"Save the implementation plan as markdown to .context/attachments/plan.md and show it in Sideboard chat for user approval (Copy / Hand off / Approve). Call this when the plan is ready \u2014 required in plan mode. Pass the full plan body in content. Then Claude should call ExitPlanMode.",
|
|
7869
8315
|
{
|
|
7870
|
-
title:
|
|
7871
|
-
content:
|
|
7872
|
-
thread_id:
|
|
8316
|
+
title: z3.string().optional().describe("Short plan title (defaults to Plan)"),
|
|
8317
|
+
content: z3.string().min(1).describe("Full plan markdown (headings, steps, risks, open questions)"),
|
|
8318
|
+
thread_id: z3.string().optional().describe("Sideboard thread id when cwd is not the worktree")
|
|
7873
8319
|
},
|
|
7874
8320
|
async ({ title, content, thread_id }) => {
|
|
7875
8321
|
const { writePlanFile: writePlanFile2 } = await import("./plan-file-6O7G4VPQ.js");
|
|
@@ -7892,15 +8338,15 @@ async function startMcpServer() {
|
|
|
7892
8338
|
"present_schema",
|
|
7893
8339
|
"Open Sideboard\u2019s schema-driven side column (filterable table and/or form). Pass JSON Schema + optional schemaUi. Prefer datasource=inline with embedded resource/records. Use datasource=brightsy with resource_id only when the user is logged into Brightsy.",
|
|
7894
8340
|
{
|
|
7895
|
-
title:
|
|
7896
|
-
mode:
|
|
7897
|
-
datasource:
|
|
7898
|
-
resource_id:
|
|
7899
|
-
record_id:
|
|
7900
|
-
resource:
|
|
7901
|
-
record:
|
|
7902
|
-
records:
|
|
7903
|
-
pane_id:
|
|
8341
|
+
title: z3.string().describe("Pane title"),
|
|
8342
|
+
mode: z3.enum(["table", "form"]).optional().describe("table = list/filter records; form = edit one record"),
|
|
8343
|
+
datasource: z3.enum(["brightsy", "inline"]).optional().describe("brightsy resolves via login; inline uses embedded resource/records"),
|
|
8344
|
+
resource_id: z3.string().optional().describe("Brightsy record type UUID (or generic resource id)"),
|
|
8345
|
+
record_id: z3.string().optional().describe("Record id when opening form mode"),
|
|
8346
|
+
resource: z3.record(z3.unknown()).optional().describe("Inline { id, title, schema, schemaUi?, slug? }"),
|
|
8347
|
+
record: z3.record(z3.unknown()).optional().describe("Inline record { id, data, published_at? }"),
|
|
8348
|
+
records: z3.array(z3.record(z3.unknown())).optional().describe("Inline records for table mode"),
|
|
8349
|
+
pane_id: z3.string().optional().describe("Stable pane id across updates")
|
|
7904
8350
|
},
|
|
7905
8351
|
async (args) => {
|
|
7906
8352
|
const id = args.pane_id?.trim() || `schema_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 8)}`;
|
|
@@ -7921,10 +8367,10 @@ async function startMcpServer() {
|
|
|
7921
8367
|
"present_files",
|
|
7922
8368
|
"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.",
|
|
7923
8369
|
{
|
|
7924
|
-
title:
|
|
7925
|
-
datasource:
|
|
7926
|
-
path:
|
|
7927
|
-
pane_id:
|
|
8370
|
+
title: z3.string().optional().describe("Pane title (default: Files)"),
|
|
8371
|
+
datasource: z3.enum(["brightsy", "memory"]).optional().describe("brightsy = account storage via login; memory = session demo store"),
|
|
8372
|
+
path: z3.string().optional().describe("Initial folder path (e.g. public)"),
|
|
8373
|
+
pane_id: z3.string().optional().describe("Stable pane id across updates")
|
|
7928
8374
|
},
|
|
7929
8375
|
async (args) => {
|
|
7930
8376
|
const id = args.pane_id?.trim() || `files_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 8)}`;
|
|
@@ -7940,19 +8386,21 @@ async function startMcpServer() {
|
|
|
7940
8386
|
}
|
|
7941
8387
|
);
|
|
7942
8388
|
registerSlackTools(server);
|
|
8389
|
+
registerLinearTools(server);
|
|
7943
8390
|
if (sideboardMcpProfile() !== "worktree") {
|
|
7944
|
-
const { getCaffeinateHold: getCaffeinateHold2, setCaffeinateHold: setCaffeinateHold2 } = await import("./caffeinate-hold-
|
|
8391
|
+
const { getCaffeinateHold: getCaffeinateHold2, setCaffeinateHold: setCaffeinateHold2 } = await import("./caffeinate-hold-EAZNWJBE.js");
|
|
7945
8392
|
const { resolveNewThreadOptions: resolveNewThreadOptions2, resolveThreadDefaults: resolveThreadDefaults2 } = await import("./app-settings-3TLA2EJ7.js");
|
|
7946
8393
|
const accountDefaults = resolveThreadDefaults2();
|
|
7947
8394
|
const accountDefaultsHint = `Account defaults: agent=${accountDefaults.agent}, model=${accountDefaults.model?.trim() || "Auto"}, effort=${accountDefaults.effort}`;
|
|
7948
8395
|
server.tool(
|
|
7949
8396
|
"set_caffeinate",
|
|
7950
|
-
"Keep this Mac awake with caffeinate (like Claude Code) across turns \u2014 independent of Settings toggles. Turn ON when the user will be away from the keyboard, is driving work from Slack, or asks you to keep the machine awake. Turn OFF when they say they are done, wrapping up, going to sleep, or no longer need the Mac awake. macOS only.",
|
|
8397
|
+
"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.",
|
|
7951
8398
|
{
|
|
7952
|
-
enabled:
|
|
8399
|
+
enabled: z3.boolean().describe("true = hold caffeinate on; false = release and let the Mac sleep")
|
|
7953
8400
|
},
|
|
7954
8401
|
async ({ enabled }) => {
|
|
7955
|
-
const
|
|
8402
|
+
const threadId = process.env.SIDEBOARD_ORCHESTRATOR_THREAD_ID?.trim() || null;
|
|
8403
|
+
const state = setCaffeinateHold2(enabled, { threadId });
|
|
7956
8404
|
if (enabled && !state.held) {
|
|
7957
8405
|
return {
|
|
7958
8406
|
content: [
|
|
@@ -7975,7 +8423,7 @@ async function startMcpServer() {
|
|
|
7975
8423
|
text: JSON.stringify({
|
|
7976
8424
|
...getCaffeinateHold2(),
|
|
7977
8425
|
ok: true,
|
|
7978
|
-
message: state.held ? "Mac will stay awake until you call set_caffeinate with enabled=false
|
|
8426
|
+
message: state.held ? "Mac will stay awake until you call set_caffeinate with enabled=false, the user says they are done, or this orchestration chat is closed." : "Caffeinate hold released. The Mac can sleep (unless Settings caffeinate toggles are on)."
|
|
7979
8427
|
})
|
|
7980
8428
|
}
|
|
7981
8429
|
]
|
|
@@ -7986,15 +8434,15 @@ async function startMcpServer() {
|
|
|
7986
8434
|
"create_thread",
|
|
7987
8435
|
`Create a new worktree thread (chat) from branch, pr, or ticket. Pass repoPath from list_workspaces. From an orchestration chat, omit parentThreadId (Sideboard binds the child to this chat) or pass the exact id from the turn reminder \u2014 never invent a uuid. Prefer omitting agent/model so Sideboard applies ${accountDefaultsHint}. Then use send_to_thread to chat.`,
|
|
7988
8436
|
{
|
|
7989
|
-
sourceType:
|
|
7990
|
-
sourceRef:
|
|
7991
|
-
agent:
|
|
7992
|
-
model:
|
|
8437
|
+
sourceType: z3.enum(["branch", "pr", "ticket"]),
|
|
8438
|
+
sourceRef: z3.string(),
|
|
8439
|
+
agent: z3.enum(["claude", "codex", "opencode", "brightsy", "cursor"]).optional().describe(`Omit to use Account default agent (${accountDefaults.agent})`),
|
|
8440
|
+
model: z3.string().nullable().optional().describe(
|
|
7993
8441
|
`Usually omit to use Account default model (${accountDefaults.model?.trim() || "Auto"}). Pass null only to force Auto / agent-default.`
|
|
7994
8442
|
),
|
|
7995
|
-
repoPath:
|
|
7996
|
-
title:
|
|
7997
|
-
parentThreadId:
|
|
8443
|
+
repoPath: z3.string(),
|
|
8444
|
+
title: z3.string().optional(),
|
|
8445
|
+
parentThreadId: z3.string().optional().describe(
|
|
7998
8446
|
"Orchestration: omit (preferred) or pass YOUR chat id from the turn reminder / AGENTS.md. Do not invent uuids."
|
|
7999
8447
|
)
|
|
8000
8448
|
},
|
|
@@ -8099,11 +8547,11 @@ async function startMcpServer() {
|
|
|
8099
8547
|
);
|
|
8100
8548
|
server.tool(
|
|
8101
8549
|
"send_to_thread",
|
|
8102
|
-
"Queue a prompt on a worktree thread chat (runs under concurrency cap). Use after create_thread to start or continue a conversation. Set force_stop=true to interrupt an in-flight/queued turn (kill + clear queue) before queueing this prompt \u2014 use when the thread is mid-turn or has stale queued prompts you need to replace.",
|
|
8550
|
+
"Queue a prompt on a worktree thread chat (runs under concurrency cap). Use after create_thread to start or continue a conversation. For commit/push/PR/merge, prefer ask_git (canonical desktop-button phrases). Set force_stop=true to interrupt an in-flight/queued turn (kill + clear queue) before queueing this prompt \u2014 use when the thread is mid-turn or has stale queued prompts you need to replace.",
|
|
8103
8551
|
{
|
|
8104
|
-
ref:
|
|
8105
|
-
prompt:
|
|
8106
|
-
force_stop:
|
|
8552
|
+
ref: z3.string(),
|
|
8553
|
+
prompt: z3.string(),
|
|
8554
|
+
force_stop: z3.boolean().optional()
|
|
8107
8555
|
},
|
|
8108
8556
|
async ({ ref, prompt, force_stop }) => {
|
|
8109
8557
|
if (force_stop) {
|
|
@@ -8130,10 +8578,10 @@ async function startMcpServer() {
|
|
|
8130
8578
|
);
|
|
8131
8579
|
server.tool(
|
|
8132
8580
|
"wait_for_turn",
|
|
8133
|
-
"Block until the thread finishes its current/queued turn (avoids polling). Use after send_to_thread to read the agent reply.",
|
|
8581
|
+
"Block until the thread finishes its current/queued turn (avoids polling). Use after send_to_thread or ask_git to read the agent reply.",
|
|
8134
8582
|
{
|
|
8135
|
-
ref:
|
|
8136
|
-
timeoutMs:
|
|
8583
|
+
ref: z3.string(),
|
|
8584
|
+
timeoutMs: z3.number().optional()
|
|
8137
8585
|
},
|
|
8138
8586
|
async ({ ref, timeoutMs }) => {
|
|
8139
8587
|
const thread = await orch.waitForTurn(ref, timeoutMs ?? 6e5);
|
|
@@ -8155,7 +8603,7 @@ async function startMcpServer() {
|
|
|
8155
8603
|
server.tool(
|
|
8156
8604
|
"get_turn_result",
|
|
8157
8605
|
"Final assistant message only (not full transcript)",
|
|
8158
|
-
{ ref:
|
|
8606
|
+
{ ref: z3.string() },
|
|
8159
8607
|
async ({ ref }) => {
|
|
8160
8608
|
const result = orch.getTurnResult(ref);
|
|
8161
8609
|
return { content: [{ type: "text", text: JSON.stringify(result) }] };
|
|
@@ -8165,8 +8613,8 @@ async function startMcpServer() {
|
|
|
8165
8613
|
"stop_thread",
|
|
8166
8614
|
"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.",
|
|
8167
8615
|
{
|
|
8168
|
-
ref:
|
|
8169
|
-
force:
|
|
8616
|
+
ref: z3.string(),
|
|
8617
|
+
force: z3.boolean().optional()
|
|
8170
8618
|
},
|
|
8171
8619
|
async ({ ref, force }) => {
|
|
8172
8620
|
const t = orch.getThread(ref);
|
|
@@ -8195,8 +8643,8 @@ async function startMcpServer() {
|
|
|
8195
8643
|
);
|
|
8196
8644
|
server.tool(
|
|
8197
8645
|
"archive_thread",
|
|
8198
|
-
"Archive a thread (stops agent/dev, runs archive script, removes worktree when last chat tab). Coordinators open PRs only by asking the worktree agent.",
|
|
8199
|
-
{ ref:
|
|
8646
|
+
"Archive a thread (stops agent/dev, runs archive script, removes worktree when last chat tab). Coordinators commit, push, open PRs, and merge only by asking the worktree agent (ask_git).",
|
|
8647
|
+
{ ref: z3.string() },
|
|
8200
8648
|
async ({ ref }) => {
|
|
8201
8649
|
const t = orch.getThread(ref);
|
|
8202
8650
|
if (!t) {
|
|
@@ -8229,7 +8677,7 @@ async function startMcpServer() {
|
|
|
8229
8677
|
server.tool(
|
|
8230
8678
|
"restore_thread",
|
|
8231
8679
|
"Restore an archived thread (recreates worktree from branch when needed)",
|
|
8232
|
-
{ ref:
|
|
8680
|
+
{ ref: z3.string() },
|
|
8233
8681
|
async ({ ref }) => {
|
|
8234
8682
|
try {
|
|
8235
8683
|
const restored = await orch.restore(ref);
|
|
@@ -8255,8 +8703,8 @@ async function startMcpServer() {
|
|
|
8255
8703
|
"get_diff",
|
|
8256
8704
|
"Compact diff summary (capped hunks, paginated)",
|
|
8257
8705
|
{
|
|
8258
|
-
ref:
|
|
8259
|
-
maxFiles:
|
|
8706
|
+
ref: z3.string(),
|
|
8707
|
+
maxFiles: z3.number().optional()
|
|
8260
8708
|
},
|
|
8261
8709
|
async ({ ref, maxFiles }) => {
|
|
8262
8710
|
const summary = await orch.diffSummary(ref);
|
|
@@ -8276,7 +8724,7 @@ async function startMcpServer() {
|
|
|
8276
8724
|
server.tool(
|
|
8277
8725
|
"request_review",
|
|
8278
8726
|
'Start a merge-readiness Review on a worktree agent thread (same as the desktop Review button). Opens a new Review chat tab, attaches .sideboard/review.md when present (else local Review request.md / stock template), and sends "Review changes in this workspace." Expect Approve / Approve with nits / Request changes / Needs more information. Pass a worktree thread ref \u2014 not the orchestrator. Then wait_for_turn / get_turn_result on the returned review tab id.',
|
|
8279
|
-
{ ref:
|
|
8727
|
+
{ ref: z3.string().describe("Worktree thread id/ref to review") },
|
|
8280
8728
|
async ({ ref }) => {
|
|
8281
8729
|
try {
|
|
8282
8730
|
const tab = await orch.requestReview(ref);
|
|
@@ -8301,7 +8749,39 @@ async function startMcpServer() {
|
|
|
8301
8749
|
}
|
|
8302
8750
|
}
|
|
8303
8751
|
);
|
|
8304
|
-
|
|
8752
|
+
server.tool(
|
|
8753
|
+
"ask_git",
|
|
8754
|
+
"Tell a worktree agent to commit & push, open a draft PR, resolve conflicts, or merge the linked PR \u2014 same short prompts as the desktop git buttons. The worktree agent runs git/gh (`gh pr merge`); this only queues the prompt. Pass a worktree thread ref (not the orchestrator). Then wait_for_turn / get_turn_result. Do not run git or gh from the orchestration cwd.",
|
|
8755
|
+
{
|
|
8756
|
+
ref: z3.string().describe("Worktree thread id/ref"),
|
|
8757
|
+
action: z3.enum(AGENT_GIT_ACTIONS).describe(
|
|
8758
|
+
"commit-push | create-draft | create-web | resolve-conflicts | merge"
|
|
8759
|
+
)
|
|
8760
|
+
},
|
|
8761
|
+
async ({ ref, action }) => {
|
|
8762
|
+
try {
|
|
8763
|
+
const thread = await orch.askGit(ref, action);
|
|
8764
|
+
return {
|
|
8765
|
+
content: [
|
|
8766
|
+
{
|
|
8767
|
+
type: "text",
|
|
8768
|
+
text: JSON.stringify({
|
|
8769
|
+
id: thread.id,
|
|
8770
|
+
status: thread.status,
|
|
8771
|
+
queueLength: thread.queue.length,
|
|
8772
|
+
action,
|
|
8773
|
+
link: `sideboard://thread/${thread.id}`
|
|
8774
|
+
})
|
|
8775
|
+
}
|
|
8776
|
+
]
|
|
8777
|
+
};
|
|
8778
|
+
} catch (err) {
|
|
8779
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
8780
|
+
return { content: [{ type: "text", text: message }], isError: true };
|
|
8781
|
+
}
|
|
8782
|
+
}
|
|
8783
|
+
);
|
|
8784
|
+
const agentEnum = z3.enum(["claude", "codex", "opencode", "brightsy", "cursor"]);
|
|
8305
8785
|
server.tool(
|
|
8306
8786
|
"list_models",
|
|
8307
8787
|
"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.",
|
|
@@ -8324,11 +8804,11 @@ async function startMcpServer() {
|
|
|
8324
8804
|
"fork_worktree",
|
|
8325
8805
|
"Fork a worktree agent chat into a NEW git worktree + chat (desktop \u201CFork to new workspace\u201D). Seeds a transcript (through through_index, default all). Optional agent override. Leave model unset for Auto (default) \u2014 only pass model when you have a reason. Not for the orchestrator. Then send_to_thread / wait_for_turn on the returned id.",
|
|
8326
8806
|
{
|
|
8327
|
-
ref:
|
|
8328
|
-
through_index:
|
|
8807
|
+
ref: z3.string().describe("Worktree thread id/ref to fork"),
|
|
8808
|
+
through_index: z3.number().optional().describe("Inclusive message index to include in the transcript (default: all)"),
|
|
8329
8809
|
agent: agentEnum.optional().describe("Agent for the forked chat (default: same as source)"),
|
|
8330
|
-
model:
|
|
8331
|
-
title:
|
|
8810
|
+
model: z3.string().nullable().optional().describe("Usually omit (Auto). Only set from list_models when you need a specific model"),
|
|
8811
|
+
title: z3.string().optional()
|
|
8332
8812
|
},
|
|
8333
8813
|
async ({ ref, through_index, agent, model, title }) => {
|
|
8334
8814
|
try {
|
|
@@ -8369,11 +8849,11 @@ async function startMcpServer() {
|
|
|
8369
8849
|
"fork_chat",
|
|
8370
8850
|
"Fork a chat into a NEW tab on the SAME workspace: worktree agent \u2192 same worktree tab; Global orchestration chat \u2192 new orchestration chat (same synthetic home). Seeds a transcript; optional agent override. Leave model unset for Auto unless you have a reason. Orchestration forks require an MCP-capable agent (claude, cursor, codex, opencode \u2014 not brightsy). Slack / Global orchestrators use this to continue an orchestration chat on another agent after session limits. Then send_to_thread / wait_for_turn on the returned id. Use fork_worktree only for worktree agents that need a new git worktree.",
|
|
8371
8851
|
{
|
|
8372
|
-
ref:
|
|
8373
|
-
through_index:
|
|
8852
|
+
ref: z3.string().describe("Thread id/ref to fork (worktree agent or orchestration chat)"),
|
|
8853
|
+
through_index: z3.number().optional().describe("Inclusive message index to include in the transcript (default: all)"),
|
|
8374
8854
|
agent: agentEnum.optional().describe("Agent for the forked chat (default: same as source)"),
|
|
8375
|
-
model:
|
|
8376
|
-
title:
|
|
8855
|
+
model: z3.string().nullable().optional().describe("Usually omit (Auto). Only set from list_models when you need a specific model"),
|
|
8856
|
+
title: z3.string().optional()
|
|
8377
8857
|
},
|
|
8378
8858
|
async ({ ref, through_index, agent, model, title }) => {
|
|
8379
8859
|
try {
|
|
@@ -8419,8 +8899,8 @@ async function startMcpServer() {
|
|
|
8419
8899
|
"run_dev_script",
|
|
8420
8900
|
"Start a .sideboard/.conductor run script for a thread (default script if name omitted); returns port",
|
|
8421
8901
|
{
|
|
8422
|
-
ref:
|
|
8423
|
-
name:
|
|
8902
|
+
ref: z3.string(),
|
|
8903
|
+
name: z3.string().optional()
|
|
8424
8904
|
},
|
|
8425
8905
|
async ({ ref, name }) => {
|
|
8426
8906
|
const result = await orch.startDev(ref, name);
|
|
@@ -8442,7 +8922,7 @@ async function startMcpServer() {
|
|
|
8442
8922
|
server.tool(
|
|
8443
8923
|
"list_run_scripts",
|
|
8444
8924
|
"List named run scripts available for a thread",
|
|
8445
|
-
{ ref:
|
|
8925
|
+
{ ref: z3.string() },
|
|
8446
8926
|
async ({ ref }) => {
|
|
8447
8927
|
const scripts = orch.listThreadRunScripts(ref);
|
|
8448
8928
|
const active = orch.getActiveRuns(ref);
|
|
@@ -8460,8 +8940,8 @@ async function startMcpServer() {
|
|
|
8460
8940
|
"stop_dev_script",
|
|
8461
8941
|
"Stop a running script for a thread (all scripts if name omitted)",
|
|
8462
8942
|
{
|
|
8463
|
-
ref:
|
|
8464
|
-
name:
|
|
8943
|
+
ref: z3.string(),
|
|
8944
|
+
name: z3.string().optional()
|
|
8465
8945
|
},
|
|
8466
8946
|
async ({ ref, name }) => {
|
|
8467
8947
|
orch.stopDev(ref, name);
|
|
@@ -8471,7 +8951,7 @@ async function startMcpServer() {
|
|
|
8471
8951
|
server.tool(
|
|
8472
8952
|
"run_setup",
|
|
8473
8953
|
"Re-run workspace setup (Sideboard/Conductor settings or .cursor/worktrees.json)",
|
|
8474
|
-
{ ref:
|
|
8954
|
+
{ ref: z3.string() },
|
|
8475
8955
|
async ({ ref }) => {
|
|
8476
8956
|
const result = await orch.runSetup(ref);
|
|
8477
8957
|
return {
|
|
@@ -8482,7 +8962,7 @@ async function startMcpServer() {
|
|
|
8482
8962
|
server.tool(
|
|
8483
8963
|
"add_workspace",
|
|
8484
8964
|
"Register a git repo as a Sideboard workspace",
|
|
8485
|
-
{ repoPath:
|
|
8965
|
+
{ repoPath: z3.string() },
|
|
8486
8966
|
async ({ repoPath }) => {
|
|
8487
8967
|
const ws = await orch.addWorkspace(repoPath);
|
|
8488
8968
|
return { content: [{ type: "text", text: JSON.stringify(ws) }] };
|
|
@@ -8491,7 +8971,7 @@ async function startMcpServer() {
|
|
|
8491
8971
|
server.tool(
|
|
8492
8972
|
"remove_workspace",
|
|
8493
8973
|
"Unregister a Sideboard workspace (does not archive threads)",
|
|
8494
|
-
{ repoPath:
|
|
8974
|
+
{ repoPath: z3.string() },
|
|
8495
8975
|
async ({ repoPath }) => {
|
|
8496
8976
|
orch.removeWorkspace(repoPath);
|
|
8497
8977
|
return { content: [{ type: "text", text: "ok" }] };
|
|
@@ -8501,12 +8981,12 @@ async function startMcpServer() {
|
|
|
8501
8981
|
"fanout",
|
|
8502
8982
|
"Best-of-n: create one thread per agent with the same prompt (parallel attempts)",
|
|
8503
8983
|
{
|
|
8504
|
-
prompt:
|
|
8505
|
-
agents:
|
|
8506
|
-
repoPath:
|
|
8507
|
-
sourceType:
|
|
8508
|
-
sourceRef:
|
|
8509
|
-
title:
|
|
8984
|
+
prompt: z3.string(),
|
|
8985
|
+
agents: z3.array(z3.enum(["claude", "codex", "opencode", "brightsy", "cursor"])),
|
|
8986
|
+
repoPath: z3.string(),
|
|
8987
|
+
sourceType: z3.enum(["branch", "pr", "ticket"]).optional(),
|
|
8988
|
+
sourceRef: z3.string().optional(),
|
|
8989
|
+
title: z3.string().optional()
|
|
8510
8990
|
},
|
|
8511
8991
|
async (args) => {
|
|
8512
8992
|
const threads = await orch.bestOfN(args);
|
|
@@ -8533,8 +9013,8 @@ async function startMcpServer() {
|
|
|
8533
9013
|
"list_branches",
|
|
8534
9014
|
"List git branches in a registered workspace. Pass repoPath from list_workspaces (unmerged into the default branch by default \u2014 for create_thread sourceType=branch).",
|
|
8535
9015
|
{
|
|
8536
|
-
repoPath:
|
|
8537
|
-
unmergedOnly:
|
|
9016
|
+
repoPath: z3.string(),
|
|
9017
|
+
unmergedOnly: z3.boolean().optional()
|
|
8538
9018
|
},
|
|
8539
9019
|
async ({ repoPath, unmergedOnly }) => {
|
|
8540
9020
|
const root = await resolveRepoRoot(repoPath);
|
|
@@ -8554,7 +9034,7 @@ async function startMcpServer() {
|
|
|
8554
9034
|
server.tool(
|
|
8555
9035
|
"list_prs",
|
|
8556
9036
|
"List open GitHub PRs for a registered workspace. Pass repoPath from list_workspaces (uses gh against that repo remote). Then create_thread with sourceType=pr.",
|
|
8557
|
-
{ repoPath:
|
|
9037
|
+
{ repoPath: z3.string() },
|
|
8558
9038
|
async ({ repoPath }) => {
|
|
8559
9039
|
const root = await resolveRepoRoot(repoPath);
|
|
8560
9040
|
const prs = await listPrs(root);
|
|
@@ -8572,8 +9052,8 @@ async function startMcpServer() {
|
|
|
8572
9052
|
);
|
|
8573
9053
|
server.tool(
|
|
8574
9054
|
"get_pr_stack",
|
|
8575
|
-
"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
|
|
8576
|
-
{ ref:
|
|
9055
|
+
"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.",
|
|
9056
|
+
{ ref: z3.string() },
|
|
8577
9057
|
async ({ ref }) => {
|
|
8578
9058
|
const stack = await orch.getPrStack(ref);
|
|
8579
9059
|
return {
|
|
@@ -8585,8 +9065,8 @@ async function startMcpServer() {
|
|
|
8585
9065
|
"open_pr_stack_layers",
|
|
8586
9066
|
"Materialize one worktree+thread per stack layer (or a single 1-based layer). Pass a thread ref already on the stack.",
|
|
8587
9067
|
{
|
|
8588
|
-
ref:
|
|
8589
|
-
layer:
|
|
9068
|
+
ref: z3.string(),
|
|
9069
|
+
layer: z3.number().int().positive().optional()
|
|
8590
9070
|
},
|
|
8591
9071
|
async ({ ref, layer }) => {
|
|
8592
9072
|
const result = await orch.openPrStackLayers(ref, { layer });
|
|
@@ -8620,9 +9100,9 @@ async function startMcpServer() {
|
|
|
8620
9100
|
"add_stack_layer",
|
|
8621
9101
|
"Add a branch on top of the current stack (`gh stack add`) and open a worktree+thread for it.",
|
|
8622
9102
|
{
|
|
8623
|
-
ref:
|
|
8624
|
-
branchName:
|
|
8625
|
-
title:
|
|
9103
|
+
ref: z3.string(),
|
|
9104
|
+
branchName: z3.string(),
|
|
9105
|
+
title: z3.string().optional()
|
|
8626
9106
|
},
|
|
8627
9107
|
async ({ ref, branchName, title }) => {
|
|
8628
9108
|
const result = await orch.addStackLayer(ref, branchName, { title });
|
|
@@ -8651,11 +9131,11 @@ async function startMcpServer() {
|
|
|
8651
9131
|
"create_pr_stack",
|
|
8652
9132
|
"Create a new GitHub PR stack with one Sideboard worktree per layer (bottom\u2192top branch names). Requires `gh extension install github/gh-stack`.",
|
|
8653
9133
|
{
|
|
8654
|
-
repoPath:
|
|
8655
|
-
branches:
|
|
8656
|
-
agent:
|
|
8657
|
-
base:
|
|
8658
|
-
title:
|
|
9134
|
+
repoPath: z3.string(),
|
|
9135
|
+
branches: z3.array(z3.string()).min(1),
|
|
9136
|
+
agent: z3.enum(["claude", "codex", "opencode", "brightsy", "cursor"]),
|
|
9137
|
+
base: z3.string().optional(),
|
|
9138
|
+
title: z3.string().optional()
|
|
8659
9139
|
},
|
|
8660
9140
|
async (args) => {
|
|
8661
9141
|
const result = await orch.createPrStack({
|
|
@@ -8693,7 +9173,7 @@ async function startMcpServer() {
|
|
|
8693
9173
|
server.tool(
|
|
8694
9174
|
"list_issues",
|
|
8695
9175
|
"List issues from Sideboard Account connections (Linear API or GitHub Issues; Linear\u2192GitHub fallback when Linear is not connected). Pass repoPath from list_workspaces \u2014 GitHub Issues are scoped to that repo. Then create_thread with sourceType=ticket.",
|
|
8696
|
-
{ repoPath:
|
|
9176
|
+
{ repoPath: z3.string() },
|
|
8697
9177
|
async ({ repoPath }) => {
|
|
8698
9178
|
const root = await resolveRepoRoot(repoPath);
|
|
8699
9179
|
const result = await listIssues(root);
|
|
@@ -8704,8 +9184,8 @@ async function startMcpServer() {
|
|
|
8704
9184
|
"list_linear_issues",
|
|
8705
9185
|
"Deprecated: prefer list_issues. Lists assigned Linear issues via the chosen agent MCP connector",
|
|
8706
9186
|
{
|
|
8707
|
-
agent:
|
|
8708
|
-
repoPath:
|
|
9187
|
+
agent: z3.enum(["claude", "codex", "opencode"]),
|
|
9188
|
+
repoPath: z3.string()
|
|
8709
9189
|
},
|
|
8710
9190
|
async ({ agent, repoPath }) => {
|
|
8711
9191
|
const issues = await listLinearIssues(agent, repoPath);
|
|
@@ -8718,18 +9198,7 @@ async function startMcpServer() {
|
|
|
8718
9198
|
}
|
|
8719
9199
|
|
|
8720
9200
|
// src/brightsy/api.ts
|
|
8721
|
-
|
|
8722
|
-
if (!(err instanceof Error)) return `${String(err)} (${url})`;
|
|
8723
|
-
const cause = err.cause;
|
|
8724
|
-
let detail = "";
|
|
8725
|
-
if (cause instanceof Error) {
|
|
8726
|
-
const code = typeof cause.code === "string" ? cause.code : void 0;
|
|
8727
|
-
detail = code ? ` [${code}: ${cause.message}]` : ` [${cause.message}]`;
|
|
8728
|
-
} else if (cause != null) {
|
|
8729
|
-
detail = ` [${String(cause)}]`;
|
|
8730
|
-
}
|
|
8731
|
-
return `${err.message}${detail} (${url})`;
|
|
8732
|
-
}
|
|
9201
|
+
var formatBrightsyFetchError = formatFetchError;
|
|
8733
9202
|
var BrightsySideboardApi = class {
|
|
8734
9203
|
cfg;
|
|
8735
9204
|
fetchImpl;
|
|
@@ -8849,8 +9318,8 @@ var BrightsySideboardApi = class {
|
|
|
8849
9318
|
};
|
|
8850
9319
|
function taskMessageText(task) {
|
|
8851
9320
|
const parts = task.message?.parts ?? [];
|
|
8852
|
-
const
|
|
8853
|
-
return
|
|
9321
|
+
const text3 = parts.find((p) => p.kind === "text")?.text ?? "";
|
|
9322
|
+
return text3.trim();
|
|
8854
9323
|
}
|
|
8855
9324
|
|
|
8856
9325
|
// src/brightsy/cloud-connect.ts
|
|
@@ -9046,63 +9515,128 @@ async function runCloudConnect(opts) {
|
|
|
9046
9515
|
|
|
9047
9516
|
// src/slack/oauth.ts
|
|
9048
9517
|
import { randomBytes as randomBytes2 } from "crypto";
|
|
9049
|
-
import { createServer as createServer3 } from "http";
|
|
9050
9518
|
|
|
9051
9519
|
// src/slack/baked-app.ts
|
|
9052
9520
|
var BAKED_SLACK_CLIENT_ID = "7592788819232.11813536272562";
|
|
9053
|
-
var
|
|
9054
|
-
var BAKED_SLACK_RELAY_URL = "wss://slack-relay.sideboard.cloud/desktop";
|
|
9521
|
+
var BAKED_SLACK_RELAY_URL = "wss://relay.sideboard.cloud/slack/desktop";
|
|
9055
9522
|
function hasBakedSlackOAuth() {
|
|
9056
|
-
return Boolean(BAKED_SLACK_CLIENT_ID.trim()
|
|
9523
|
+
return Boolean(BAKED_SLACK_CLIENT_ID.trim());
|
|
9057
9524
|
}
|
|
9058
9525
|
function slackRelayUrl() {
|
|
9059
9526
|
return process.env.SIDEBOARD_SLACK_RELAY_URL?.trim() || BAKED_SLACK_RELAY_URL.trim();
|
|
9060
9527
|
}
|
|
9061
9528
|
|
|
9062
9529
|
// src/slack/oauth-redirect.ts
|
|
9063
|
-
var
|
|
9064
|
-
var
|
|
9065
|
-
var
|
|
9066
|
-
var
|
|
9067
|
-
var
|
|
9530
|
+
var SLACK_OAUTH_CALLBACK_PATH = "/slack/callback";
|
|
9531
|
+
var SLACK_OAUTH_RESULT_PATH = "/slack/oauth/result";
|
|
9532
|
+
var SLACK_RELAY_DESKTOP_PATH = "/slack/desktop";
|
|
9533
|
+
var SLACK_OAUTH_LOCAL_PORT = 19847;
|
|
9534
|
+
var SLACK_OAUTH_LOCAL_CALLBACK = `http://127.0.0.1:${SLACK_OAUTH_LOCAL_PORT}${SLACK_OAUTH_CALLBACK_PATH}`;
|
|
9535
|
+
var SLACK_OAUTH_REDIRECT = `https://relay.sideboard.cloud${SLACK_OAUTH_CALLBACK_PATH}`;
|
|
9068
9536
|
function slackOAuthRedirectUri() {
|
|
9069
9537
|
return process.env.SIDEBOARD_SLACK_OAUTH_REDIRECT?.trim() || SLACK_OAUTH_REDIRECT;
|
|
9070
9538
|
}
|
|
9071
|
-
function slackOAuthLocalBounceTarget(params) {
|
|
9072
|
-
const dest = new URL(SLACK_OAUTH_LOCAL_CALLBACK);
|
|
9073
|
-
for (const key of BOUNCE_PARAMS) {
|
|
9074
|
-
const value = params.get(key);
|
|
9075
|
-
if (value) dest.searchParams.set(key, value);
|
|
9076
|
-
}
|
|
9077
|
-
return dest.toString();
|
|
9078
|
-
}
|
|
9079
9539
|
function escapeHtml(value) {
|
|
9080
9540
|
return value.replace(/&/g, "&").replace(/"/g, """).replace(/</g, "<").replace(/>/g, ">");
|
|
9081
9541
|
}
|
|
9082
|
-
function
|
|
9083
|
-
|
|
9084
|
-
return `<!doctype html><html><head><meta charset="utf-8"><meta http-equiv="refresh" content="0;url=${safe}"><title>Connecting Slack</title>
|
|
9542
|
+
function slackOAuthHtmlPage(title, body) {
|
|
9543
|
+
return `<!doctype html><html><head><meta charset="utf-8"><title>${escapeHtml(title)}</title>
|
|
9085
9544
|
<style>body{font-family:ui-sans-serif,system-ui,sans-serif;padding:48px 24px;max-width:36rem;margin:0 auto;color:#1a1a1a}
|
|
9086
|
-
h1{font-size:1.25rem}p{line-height:1.5;color:#444}
|
|
9087
|
-
<script>location.replace(${JSON.stringify(target)})</script>
|
|
9088
|
-
</head><body><h1>Connecting Slack</h1><p>Returning to Sideboard\u2026 <a href="${safe}">Continue</a></p></body></html>`;
|
|
9545
|
+
h1{font-size:1.25rem}p{line-height:1.5;color:#444}</style></head><body>${body}</body></html>`;
|
|
9089
9546
|
}
|
|
9090
|
-
function
|
|
9547
|
+
function parseSlackOAuthCallbackUrl(reqUrl) {
|
|
9091
9548
|
let url;
|
|
9092
9549
|
try {
|
|
9093
9550
|
url = new URL(reqUrl, SLACK_OAUTH_REDIRECT);
|
|
9094
9551
|
} catch {
|
|
9095
9552
|
return null;
|
|
9096
9553
|
}
|
|
9097
|
-
if (url.pathname !==
|
|
9098
|
-
|
|
9554
|
+
if (url.pathname !== SLACK_OAUTH_CALLBACK_PATH) return null;
|
|
9555
|
+
return url;
|
|
9556
|
+
}
|
|
9557
|
+
function parseSlackOAuthResultUrl(reqUrl) {
|
|
9558
|
+
let url;
|
|
9559
|
+
try {
|
|
9560
|
+
url = new URL(reqUrl, SLACK_OAUTH_REDIRECT);
|
|
9561
|
+
} catch {
|
|
9562
|
+
return null;
|
|
9563
|
+
}
|
|
9564
|
+
if (url.pathname !== SLACK_OAUTH_RESULT_PATH) return null;
|
|
9565
|
+
return url;
|
|
9566
|
+
}
|
|
9567
|
+
|
|
9568
|
+
// src/slack/oauth-exchange.ts
|
|
9569
|
+
var PENDING_TTL_MS = 5 * 6e4;
|
|
9570
|
+
var SlackOAuthPendingStore = class {
|
|
9571
|
+
items = /* @__PURE__ */ new Map();
|
|
9572
|
+
put(state, value) {
|
|
9573
|
+
const id = state.trim();
|
|
9574
|
+
if (!id) return;
|
|
9575
|
+
this.sweep();
|
|
9576
|
+
const existing = this.items.get(id);
|
|
9577
|
+
if (existing && existing.value.ok && !value.ok) return;
|
|
9578
|
+
this.items.set(id, { expiresAt: Date.now() + PENDING_TTL_MS, value });
|
|
9579
|
+
}
|
|
9580
|
+
/** Consume a pending result. Missing/expired → null (desktop should keep polling). */
|
|
9581
|
+
take(state) {
|
|
9582
|
+
const id = state.trim();
|
|
9583
|
+
if (!id) return null;
|
|
9584
|
+
this.sweep();
|
|
9585
|
+
const row = this.items.get(id);
|
|
9586
|
+
if (!row) return null;
|
|
9587
|
+
this.items.delete(id);
|
|
9588
|
+
return row.value;
|
|
9589
|
+
}
|
|
9590
|
+
sweep() {
|
|
9591
|
+
const now = Date.now();
|
|
9592
|
+
for (const [key, row] of this.items) {
|
|
9593
|
+
if (row.expiresAt <= now) this.items.delete(key);
|
|
9594
|
+
}
|
|
9595
|
+
}
|
|
9596
|
+
};
|
|
9597
|
+
function slackRelayHttpOrigin(relayWsUrl) {
|
|
9598
|
+
const raw = (relayWsUrl ?? slackRelayUrl()).trim();
|
|
9599
|
+
const http = raw.replace(/^ws/i, "http");
|
|
9600
|
+
const u = new URL(http);
|
|
9601
|
+
return `${u.protocol}//${u.host}`;
|
|
9602
|
+
}
|
|
9603
|
+
function slackOAuthResultUrl(state, relayWsUrl) {
|
|
9604
|
+
const u = new URL(SLACK_OAUTH_RESULT_PATH, slackRelayHttpOrigin(relayWsUrl));
|
|
9605
|
+
u.searchParams.set("state", state);
|
|
9606
|
+
return u.toString();
|
|
9607
|
+
}
|
|
9608
|
+
function slackOAuthRelayClientId() {
|
|
9609
|
+
return process.env.SIDEBOARD_SLACK_CLIENT_ID?.trim() || BAKED_SLACK_CLIENT_ID.trim();
|
|
9610
|
+
}
|
|
9611
|
+
function slackOAuthRelayRedirectUri() {
|
|
9612
|
+
return slackOAuthRedirectUri() || SLACK_OAUTH_REDIRECT;
|
|
9613
|
+
}
|
|
9614
|
+
async function exchangeSlackOAuthCode(opts) {
|
|
9615
|
+
const body = new URLSearchParams({
|
|
9616
|
+
client_id: opts.clientId,
|
|
9617
|
+
client_secret: opts.clientSecret,
|
|
9618
|
+
code: opts.code,
|
|
9619
|
+
redirect_uri: opts.redirectUri
|
|
9620
|
+
});
|
|
9621
|
+
const fetchImpl = opts.fetchImpl ?? fetch;
|
|
9622
|
+
const res = await fetchImpl("https://slack.com/api/oauth.v2.access", {
|
|
9623
|
+
method: "POST",
|
|
9624
|
+
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
9625
|
+
body
|
|
9626
|
+
});
|
|
9627
|
+
const data = await res.json();
|
|
9628
|
+
if (!data.ok) {
|
|
9629
|
+
throw new Error(`Slack OAuth exchange failed: ${data.error || res.status}`);
|
|
9630
|
+
}
|
|
9631
|
+
const teamId = data.team?.id?.trim();
|
|
9632
|
+
if (!teamId) throw new Error("Slack OAuth did not return a team id");
|
|
9099
9633
|
return {
|
|
9100
|
-
|
|
9101
|
-
|
|
9102
|
-
|
|
9103
|
-
|
|
9104
|
-
|
|
9105
|
-
|
|
9634
|
+
team_id: teamId,
|
|
9635
|
+
team_name: data.team?.name?.trim() || teamId,
|
|
9636
|
+
user_id: data.authed_user?.id?.trim() || void 0,
|
|
9637
|
+
bot_token: data.access_token?.trim() || void 0,
|
|
9638
|
+
user_token: data.authed_user?.access_token?.trim() || void 0,
|
|
9639
|
+
scopes: [data.scope, data.authed_user?.scope].filter(Boolean).join(",")
|
|
9106
9640
|
};
|
|
9107
9641
|
}
|
|
9108
9642
|
|
|
@@ -9138,13 +9672,13 @@ var SLACK_USER_SCOPES = [
|
|
|
9138
9672
|
function slackOAuthCredentials() {
|
|
9139
9673
|
const settings = loadAppSettings();
|
|
9140
9674
|
const clientId = process.env.SIDEBOARD_SLACK_CLIENT_ID?.trim() || settings.integrations.slackClientId?.trim() || BAKED_SLACK_CLIENT_ID.trim();
|
|
9141
|
-
const clientSecret = process.env.SIDEBOARD_SLACK_CLIENT_SECRET?.trim() || settings.integrations.slackClientSecret?.trim() ||
|
|
9142
|
-
if (!clientId
|
|
9675
|
+
const clientSecret = process.env.SIDEBOARD_SLACK_CLIENT_SECRET?.trim() || settings.integrations.slackClientSecret?.trim() || "";
|
|
9676
|
+
if (!clientId) {
|
|
9143
9677
|
throw new Error(
|
|
9144
|
-
"Slack browser sign-in needs a Slack app Client ID
|
|
9678
|
+
"Slack browser sign-in needs a Slack app Client ID (Account \u2192 Slack, or SIDEBOARD_SLACK_CLIENT_ID). You can still paste an xoxb- or xoxp- token."
|
|
9145
9679
|
);
|
|
9146
9680
|
}
|
|
9147
|
-
return { clientId, clientSecret };
|
|
9681
|
+
return { clientId, clientSecret: clientSecret || null };
|
|
9148
9682
|
}
|
|
9149
9683
|
var SLACK_OAUTH_CANCELLED = "Slack sign-in cancelled";
|
|
9150
9684
|
var SlackOAuthCancelledError = class extends Error {
|
|
@@ -9169,127 +9703,100 @@ function slackOAuthAuthorizeUrl(clientId, state) {
|
|
|
9169
9703
|
});
|
|
9170
9704
|
return `https://slack.com/oauth/v2/authorize?${params.toString()}`;
|
|
9171
9705
|
}
|
|
9172
|
-
function
|
|
9173
|
-
return
|
|
9174
|
-
|
|
9175
|
-
|
|
9706
|
+
function sleep(ms, signal) {
|
|
9707
|
+
return new Promise((resolve, reject) => {
|
|
9708
|
+
if (signal?.aborted) {
|
|
9709
|
+
reject(new SlackOAuthCancelledError());
|
|
9710
|
+
return;
|
|
9711
|
+
}
|
|
9712
|
+
const timer = setTimeout(() => {
|
|
9713
|
+
signal?.removeEventListener("abort", onAbort);
|
|
9714
|
+
resolve();
|
|
9715
|
+
}, ms);
|
|
9716
|
+
const onAbort = () => {
|
|
9717
|
+
clearTimeout(timer);
|
|
9718
|
+
reject(new SlackOAuthCancelledError());
|
|
9719
|
+
};
|
|
9720
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
9721
|
+
});
|
|
9722
|
+
}
|
|
9723
|
+
function upsertFromPayload(payload) {
|
|
9724
|
+
return upsertSlackWorkspace({
|
|
9725
|
+
team_id: payload.team_id,
|
|
9726
|
+
team_name: payload.team_name,
|
|
9727
|
+
user_id: payload.user_id,
|
|
9728
|
+
bot_token: payload.bot_token,
|
|
9729
|
+
user_token: payload.user_token,
|
|
9730
|
+
scopes: payload.scopes,
|
|
9731
|
+
connected_at: (/* @__PURE__ */ new Date()).toISOString()
|
|
9732
|
+
});
|
|
9176
9733
|
}
|
|
9177
9734
|
async function startSlackOAuth(opts) {
|
|
9178
9735
|
if (opts?.signal?.aborted) {
|
|
9179
9736
|
throw new SlackOAuthCancelledError();
|
|
9180
9737
|
}
|
|
9181
|
-
const { clientId
|
|
9738
|
+
const { clientId } = slackOAuthCredentials();
|
|
9182
9739
|
const state = randomBytes2(16).toString("hex");
|
|
9183
9740
|
const authorizeUrl = slackOAuthAuthorizeUrl(clientId, state);
|
|
9184
9741
|
const timeoutMs = opts?.timeoutMs ?? 5 * 6e4;
|
|
9185
|
-
const
|
|
9186
|
-
|
|
9187
|
-
|
|
9188
|
-
|
|
9189
|
-
|
|
9190
|
-
|
|
9191
|
-
|
|
9192
|
-
|
|
9193
|
-
|
|
9194
|
-
const server = createServer3((req, res2) => {
|
|
9195
|
-
try {
|
|
9196
|
-
const url = new URL(req.url || "/", `http://127.0.0.1:${SLACK_OAUTH_PORT}`);
|
|
9197
|
-
if (url.pathname !== "/callback") {
|
|
9198
|
-
res2.writeHead(404);
|
|
9199
|
-
res2.end("Not found");
|
|
9200
|
-
return;
|
|
9201
|
-
}
|
|
9202
|
-
const err = url.searchParams.get("error");
|
|
9203
|
-
const gotState = url.searchParams.get("state");
|
|
9204
|
-
const gotCode = url.searchParams.get("code");
|
|
9205
|
-
if (err) {
|
|
9206
|
-
res2.writeHead(400, { "Content-Type": "text/html" });
|
|
9207
|
-
res2.end(htmlPage2("Slack", `<h1>Authorization cancelled</h1><p>${err}</p>`));
|
|
9208
|
-
finish(new SlackOAuthCancelledError(`Slack OAuth: ${err}`));
|
|
9209
|
-
return;
|
|
9210
|
-
}
|
|
9211
|
-
if (gotState !== state || !gotCode) {
|
|
9212
|
-
res2.writeHead(400, { "Content-Type": "text/html" });
|
|
9213
|
-
res2.end(htmlPage2("Slack", "<h1>Invalid callback</h1><p>State or code missing.</p>"));
|
|
9214
|
-
finish(new Error("Slack OAuth callback was invalid"));
|
|
9215
|
-
return;
|
|
9216
|
-
}
|
|
9217
|
-
res2.writeHead(200, { "Content-Type": "text/html" });
|
|
9218
|
-
res2.end(
|
|
9219
|
-
htmlPage2(
|
|
9220
|
-
"Slack connected",
|
|
9221
|
-
"<h1>Slack workspace connected</h1><p>You can close this tab and return to Sideboard.</p>"
|
|
9222
|
-
)
|
|
9223
|
-
);
|
|
9224
|
-
finish(void 0, gotCode);
|
|
9225
|
-
} catch (e) {
|
|
9226
|
-
finish(e instanceof Error ? e : new Error(String(e)));
|
|
9227
|
-
}
|
|
9228
|
-
});
|
|
9229
|
-
const timer = setTimeout(() => {
|
|
9230
|
-
finish(new Error("Slack sign-in timed out \u2014 try again"));
|
|
9231
|
-
}, timeoutMs);
|
|
9232
|
-
const onAbort = () => finish(new SlackOAuthCancelledError());
|
|
9233
|
-
const cleanup = () => {
|
|
9234
|
-
clearTimeout(timer);
|
|
9235
|
-
opts?.signal?.removeEventListener("abort", onAbort);
|
|
9236
|
-
server.close();
|
|
9237
|
-
};
|
|
9238
|
-
opts?.signal?.addEventListener("abort", onAbort, { once: true });
|
|
9742
|
+
const pollIntervalMs = opts?.pollIntervalMs ?? 400;
|
|
9743
|
+
const fetchImpl = opts?.fetchImpl ?? fetch;
|
|
9744
|
+
const resultUrl = opts?.resultUrlForState?.(state) ?? slackOAuthResultUrl(state);
|
|
9745
|
+
await Promise.resolve(opts?.openUrl?.(authorizeUrl));
|
|
9746
|
+
if (opts?.signal?.aborted) {
|
|
9747
|
+
throw new SlackOAuthCancelledError();
|
|
9748
|
+
}
|
|
9749
|
+
const deadline = Date.now() + timeoutMs;
|
|
9750
|
+
while (Date.now() < deadline) {
|
|
9239
9751
|
if (opts?.signal?.aborted) {
|
|
9240
|
-
|
|
9241
|
-
return;
|
|
9752
|
+
throw new SlackOAuthCancelledError();
|
|
9242
9753
|
}
|
|
9243
|
-
|
|
9244
|
-
|
|
9245
|
-
|
|
9246
|
-
|
|
9247
|
-
|
|
9248
|
-
);
|
|
9249
|
-
|
|
9250
|
-
|
|
9251
|
-
|
|
9252
|
-
|
|
9754
|
+
let res;
|
|
9755
|
+
try {
|
|
9756
|
+
res = await fetchImpl(resultUrl);
|
|
9757
|
+
} catch {
|
|
9758
|
+
if (opts?.signal?.aborted) throw new SlackOAuthCancelledError();
|
|
9759
|
+
await sleep(pollIntervalMs, opts?.signal);
|
|
9760
|
+
continue;
|
|
9761
|
+
}
|
|
9762
|
+
let data;
|
|
9763
|
+
try {
|
|
9764
|
+
data = await res.json();
|
|
9765
|
+
} catch {
|
|
9766
|
+
await sleep(pollIntervalMs, opts?.signal);
|
|
9767
|
+
continue;
|
|
9768
|
+
}
|
|
9769
|
+
if (data.ok && data.team_id?.trim()) {
|
|
9770
|
+
return upsertFromPayload({
|
|
9771
|
+
team_id: data.team_id.trim(),
|
|
9772
|
+
team_name: data.team_name?.trim() || data.team_id.trim(),
|
|
9773
|
+
user_id: data.user_id?.trim() || void 0,
|
|
9774
|
+
bot_token: data.bot_token?.trim() || void 0,
|
|
9775
|
+
user_token: data.user_token?.trim() || void 0,
|
|
9776
|
+
scopes: data.scopes?.trim() || ""
|
|
9253
9777
|
});
|
|
9254
|
-
}
|
|
9255
|
-
|
|
9256
|
-
|
|
9257
|
-
|
|
9258
|
-
|
|
9259
|
-
|
|
9260
|
-
|
|
9261
|
-
|
|
9262
|
-
|
|
9263
|
-
method: "POST",
|
|
9264
|
-
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
9265
|
-
body
|
|
9266
|
-
});
|
|
9267
|
-
const data = await res.json();
|
|
9268
|
-
if (!data.ok) {
|
|
9269
|
-
throw new Error(`Slack OAuth exchange failed: ${data.error || res.status}`);
|
|
9778
|
+
}
|
|
9779
|
+
const err = data.error?.trim();
|
|
9780
|
+
if (err && err !== "pending") {
|
|
9781
|
+
if (/cancel/i.test(err) || /access_denied/i.test(err)) {
|
|
9782
|
+
throw new SlackOAuthCancelledError(`Slack OAuth: ${err}`);
|
|
9783
|
+
}
|
|
9784
|
+
throw new Error(err.startsWith("Slack") ? err : `Slack OAuth: ${err}`);
|
|
9785
|
+
}
|
|
9786
|
+
await sleep(pollIntervalMs, opts?.signal);
|
|
9270
9787
|
}
|
|
9271
|
-
|
|
9272
|
-
if (!teamId) throw new Error("Slack OAuth did not return a team id");
|
|
9273
|
-
return upsertSlackWorkspace({
|
|
9274
|
-
team_id: teamId,
|
|
9275
|
-
team_name: data.team?.name?.trim() || teamId,
|
|
9276
|
-
user_id: data.authed_user?.id,
|
|
9277
|
-
bot_token: data.access_token,
|
|
9278
|
-
user_token: data.authed_user?.access_token,
|
|
9279
|
-
scopes: [data.scope, data.authed_user?.scope].filter(Boolean).join(","),
|
|
9280
|
-
connected_at: (/* @__PURE__ */ new Date()).toISOString()
|
|
9281
|
-
});
|
|
9788
|
+
throw new Error("Slack sign-in timed out \u2014 try again");
|
|
9282
9789
|
}
|
|
9283
9790
|
|
|
9284
9791
|
// src/slack/socket-mode.ts
|
|
9285
9792
|
import { WebSocket as WsWebSocket } from "ws";
|
|
9286
9793
|
var BACKOFF_START_MS = 1e3;
|
|
9287
9794
|
var BACKOFF_MAX_MS = 3e4;
|
|
9288
|
-
function stripSlackMentions(
|
|
9289
|
-
return
|
|
9795
|
+
function stripSlackMentions(text3) {
|
|
9796
|
+
return text3.replace(/<@[^>]+>/g, "").replace(/\s+/g, " ").trim();
|
|
9290
9797
|
}
|
|
9291
|
-
function isSlackStopCommand(
|
|
9292
|
-
const t = stripSlackMentions(
|
|
9798
|
+
function isSlackStopCommand(text3) {
|
|
9799
|
+
const t = stripSlackMentions(text3).toLowerCase();
|
|
9293
9800
|
return t === "stop" || t === "sideboard_force_stop";
|
|
9294
9801
|
}
|
|
9295
9802
|
function parseSlackSocketFrame(raw) {
|
|
@@ -9311,8 +9818,8 @@ function inboundFromSocketFrame(frame) {
|
|
|
9311
9818
|
const ts = event.ts?.trim();
|
|
9312
9819
|
if (!channelId || !ts) return null;
|
|
9313
9820
|
const rawText = event.text?.trim() ?? "";
|
|
9314
|
-
const
|
|
9315
|
-
if (!
|
|
9821
|
+
const text3 = stripSlackMentions(rawText);
|
|
9822
|
+
if (!text3) return null;
|
|
9316
9823
|
const teamId = (frame.payload?.team_id || event.team || "").trim();
|
|
9317
9824
|
if (!teamId) return null;
|
|
9318
9825
|
const isDm = event.type === "message" && (event.channel_type === "im" || event.channel_type === "mpim" || !event.channel_type && channelId.startsWith("D"));
|
|
@@ -9324,7 +9831,7 @@ function inboundFromSocketFrame(frame) {
|
|
|
9324
9831
|
ts,
|
|
9325
9832
|
threadTs: event.thread_ts?.trim() || void 0,
|
|
9326
9833
|
userId: event.user?.trim(),
|
|
9327
|
-
text:
|
|
9834
|
+
text: text3,
|
|
9328
9835
|
kind: isMention ? "mention" : "dm"
|
|
9329
9836
|
};
|
|
9330
9837
|
}
|
|
@@ -9575,7 +10082,40 @@ function parseSlackRelayServerMessage(raw) {
|
|
|
9575
10082
|
}
|
|
9576
10083
|
}
|
|
9577
10084
|
|
|
10085
|
+
// src/slack/public-dns.ts
|
|
10086
|
+
import { lookup as systemLookup, Resolver } from "dns";
|
|
10087
|
+
import { request as httpRequest } from "http";
|
|
10088
|
+
import { request as httpsRequest } from "https";
|
|
10089
|
+
var PUBLIC_DNS = ["1.1.1.1", "8.8.8.8"];
|
|
10090
|
+
function finishLookup(options, callback, address, family) {
|
|
10091
|
+
if (options.all) {
|
|
10092
|
+
callback(null, [{ address, family }]);
|
|
10093
|
+
return;
|
|
10094
|
+
}
|
|
10095
|
+
callback(null, address, family);
|
|
10096
|
+
}
|
|
10097
|
+
var lookupPreferPublicDns = (hostname, options, callback) => {
|
|
10098
|
+
if (options.family === 6) {
|
|
10099
|
+
systemLookup(hostname, options, (err, address, family) => {
|
|
10100
|
+
callback(err, address, family);
|
|
10101
|
+
});
|
|
10102
|
+
return;
|
|
10103
|
+
}
|
|
10104
|
+
const resolver = new Resolver();
|
|
10105
|
+
resolver.setServers(PUBLIC_DNS);
|
|
10106
|
+
resolver.resolve4(hostname, (err, addresses) => {
|
|
10107
|
+
if (!err && addresses[0]) {
|
|
10108
|
+
finishLookup(options, callback, addresses[0], 4);
|
|
10109
|
+
return;
|
|
10110
|
+
}
|
|
10111
|
+
systemLookup(hostname, options, (sysErr, address, family) => {
|
|
10112
|
+
callback(sysErr, address, family);
|
|
10113
|
+
});
|
|
10114
|
+
});
|
|
10115
|
+
};
|
|
10116
|
+
|
|
9578
10117
|
// src/slack/relay-client.ts
|
|
10118
|
+
import { WebSocket as WsWebSocket2 } from "ws";
|
|
9579
10119
|
var BACKOFF_START_MS2 = 1e3;
|
|
9580
10120
|
var BACKOFF_MAX_MS2 = 3e4;
|
|
9581
10121
|
function wait2(ms, signal) {
|
|
@@ -9600,7 +10140,11 @@ function send(ws, msg) {
|
|
|
9600
10140
|
}
|
|
9601
10141
|
async function runSlackRelayClient(opts) {
|
|
9602
10142
|
const log = opts.onLog ?? (() => void 0);
|
|
9603
|
-
const Ws = resolveWebSocket(opts.WebSocketImpl)
|
|
10143
|
+
const Ws = opts.WebSocketImpl ? resolveWebSocket(opts.WebSocketImpl) : class {
|
|
10144
|
+
constructor(url2) {
|
|
10145
|
+
return new WsWebSocket2(url2, { lookup: lookupPreferPublicDns });
|
|
10146
|
+
}
|
|
10147
|
+
};
|
|
9604
10148
|
const url = opts.url.trim();
|
|
9605
10149
|
const deviceId = opts.deviceId.trim();
|
|
9606
10150
|
if (!url) throw new Error("Slack relay URL is empty");
|
|
@@ -9763,12 +10307,12 @@ function formatSlackInboundPrompt(msg) {
|
|
|
9763
10307
|
|
|
9764
10308
|
${msg.text}`;
|
|
9765
10309
|
}
|
|
9766
|
-
function isSlackInboundUserPrompt(
|
|
9767
|
-
return
|
|
10310
|
+
function isSlackInboundUserPrompt(text3) {
|
|
10311
|
+
return text3.startsWith("Slack DM\n") || text3.startsWith("Slack @mention\n");
|
|
9768
10312
|
}
|
|
9769
|
-
function formatSlackSignedReply(deviceLabel,
|
|
10313
|
+
function formatSlackSignedReply(deviceLabel, text3) {
|
|
9770
10314
|
const label = deviceLabel.trim();
|
|
9771
|
-
const body =
|
|
10315
|
+
const body = text3.trim();
|
|
9772
10316
|
if (!body) return body;
|
|
9773
10317
|
if (!label) return body;
|
|
9774
10318
|
const head = `${label}:`;
|
|
@@ -9777,8 +10321,8 @@ function formatSlackSignedReply(deviceLabel, text2) {
|
|
|
9777
10321
|
}
|
|
9778
10322
|
return `${label}: ${body}`;
|
|
9779
10323
|
}
|
|
9780
|
-
function signForThisMac(
|
|
9781
|
-
return formatSlackSignedReply(ensureSlackDeviceIdentity().deviceLabel,
|
|
10324
|
+
function signForThisMac(text3) {
|
|
10325
|
+
return formatSlackSignedReply(ensureSlackDeviceIdentity().deviceLabel, text3);
|
|
9782
10326
|
}
|
|
9783
10327
|
function slackReplyThreadTs(msg) {
|
|
9784
10328
|
if (msg.threadTs && msg.threadTs !== msg.ts) return msg.threadTs;
|
|
@@ -9821,7 +10365,7 @@ async function ackSlackInboundSeen(msg, opts) {
|
|
|
9821
10365
|
log(`react error: ${errMsg}${hint}`);
|
|
9822
10366
|
}
|
|
9823
10367
|
}
|
|
9824
|
-
async function postSlackText(target,
|
|
10368
|
+
async function postSlackText(target, text3, opts) {
|
|
9825
10369
|
const stub = {
|
|
9826
10370
|
teamId: target.teamId,
|
|
9827
10371
|
channelId: target.channelId,
|
|
@@ -9831,7 +10375,7 @@ async function postSlackText(target, text2, opts) {
|
|
|
9831
10375
|
kind: "dm"
|
|
9832
10376
|
};
|
|
9833
10377
|
if (opts.postReply) {
|
|
9834
|
-
await opts.postReply(stub,
|
|
10378
|
+
await opts.postReply(stub, text3);
|
|
9835
10379
|
return;
|
|
9836
10380
|
}
|
|
9837
10381
|
const token = writeTokenForTeam(target.teamId);
|
|
@@ -9840,29 +10384,29 @@ async function postSlackText(target, text2, opts) {
|
|
|
9840
10384
|
"chat.postMessage",
|
|
9841
10385
|
{
|
|
9842
10386
|
channel: target.channelId,
|
|
9843
|
-
text:
|
|
10387
|
+
text: text3,
|
|
9844
10388
|
thread_ts: target.threadTs
|
|
9845
10389
|
},
|
|
9846
10390
|
opts.fetchImpl
|
|
9847
10391
|
);
|
|
9848
10392
|
}
|
|
9849
|
-
async function postSlackReply(msg,
|
|
10393
|
+
async function postSlackReply(msg, text3, opts) {
|
|
9850
10394
|
await postSlackText(
|
|
9851
10395
|
{
|
|
9852
10396
|
teamId: msg.teamId,
|
|
9853
10397
|
channelId: msg.channelId,
|
|
9854
10398
|
threadTs: slackReplyThreadTs(msg)
|
|
9855
10399
|
},
|
|
9856
|
-
signForThisMac(
|
|
10400
|
+
signForThisMac(text3),
|
|
9857
10401
|
opts
|
|
9858
10402
|
);
|
|
9859
10403
|
}
|
|
9860
10404
|
var lastRelayed = /* @__PURE__ */ new Map();
|
|
9861
|
-
function markRelayed(threadId,
|
|
9862
|
-
lastRelayed.set(threadId, `${threadId}:${
|
|
10405
|
+
function markRelayed(threadId, text3) {
|
|
10406
|
+
lastRelayed.set(threadId, `${threadId}:${text3}`);
|
|
9863
10407
|
}
|
|
9864
|
-
function alreadyRelayed(threadId,
|
|
9865
|
-
return lastRelayed.get(threadId) === `${threadId}:${
|
|
10408
|
+
function alreadyRelayed(threadId, text3) {
|
|
10409
|
+
return lastRelayed.get(threadId) === `${threadId}:${text3}`;
|
|
9866
10410
|
}
|
|
9867
10411
|
async function relayCoordinatorReplyToSlack(threadId, opts) {
|
|
9868
10412
|
const target = getSlackReplyTarget(threadId);
|
|
@@ -9871,14 +10415,14 @@ async function relayCoordinatorReplyToSlack(threadId, opts) {
|
|
|
9871
10415
|
const lastUser = thread ? [...thread.messages].reverse().find((m) => m.role === "user") : void 0;
|
|
9872
10416
|
if (lastUser && isSlackInboundUserPrompt(lastUser.text)) return;
|
|
9873
10417
|
const result = getOrchestrator().getTurnResult(threadId);
|
|
9874
|
-
const
|
|
9875
|
-
if (!
|
|
9876
|
-
if (alreadyRelayed(threadId,
|
|
9877
|
-
markRelayed(threadId,
|
|
10418
|
+
const text3 = result.text.trim();
|
|
10419
|
+
if (!text3) return;
|
|
10420
|
+
if (alreadyRelayed(threadId, text3)) return;
|
|
10421
|
+
markRelayed(threadId, text3);
|
|
9878
10422
|
const log = opts.onLog ?? (() => void 0);
|
|
9879
10423
|
try {
|
|
9880
|
-
await postSlackText(target, signForThisMac(
|
|
9881
|
-
log(`replied ${target.threadTs ?? target.channelId} (${
|
|
10424
|
+
await postSlackText(target, signForThisMac(text3), opts);
|
|
10425
|
+
log(`replied ${target.threadTs ?? target.channelId} (${text3.length} chars)`);
|
|
9882
10426
|
} catch (err) {
|
|
9883
10427
|
lastRelayed.delete(threadId);
|
|
9884
10428
|
const errMsg = err instanceof Error ? err.message : String(err);
|
|
@@ -10042,10 +10586,10 @@ function resolveSlackListenMode(opts) {
|
|
|
10042
10586
|
}
|
|
10043
10587
|
|
|
10044
10588
|
// src/slack/relay-hub.ts
|
|
10045
|
-
function parseSlackDeviceDestination(
|
|
10046
|
-
const m =
|
|
10047
|
-
if (!m) return { label: null, rest:
|
|
10048
|
-
return { label: m[1], rest:
|
|
10589
|
+
function parseSlackDeviceDestination(text3) {
|
|
10590
|
+
const m = text3.match(/^\s*(?:to\s+)?(?:@|#)?([A-Za-z][\w-]{0,63})\s*[::]\s*/);
|
|
10591
|
+
if (!m) return { label: null, rest: text3 };
|
|
10592
|
+
return { label: m[1], rest: text3.slice(m[0].length) };
|
|
10049
10593
|
}
|
|
10050
10594
|
var SlackRelayHub = class {
|
|
10051
10595
|
sessions = /* @__PURE__ */ new Map();
|
|
@@ -10263,40 +10807,126 @@ var SlackRelayHub = class {
|
|
|
10263
10807
|
};
|
|
10264
10808
|
|
|
10265
10809
|
// src/slack/relay-server.ts
|
|
10266
|
-
import { createServer as
|
|
10810
|
+
import { createServer as createServer3 } from "http";
|
|
10267
10811
|
import { WebSocketServer } from "ws";
|
|
10812
|
+
function sendHtml(res, status, title, body) {
|
|
10813
|
+
res.writeHead(status, { "Content-Type": "text/html; charset=utf-8" });
|
|
10814
|
+
res.end(slackOAuthHtmlPage(title, body));
|
|
10815
|
+
}
|
|
10816
|
+
function sendJson(res, status, body) {
|
|
10817
|
+
res.writeHead(status, { "Content-Type": "application/json" });
|
|
10818
|
+
res.end(JSON.stringify(body));
|
|
10819
|
+
}
|
|
10268
10820
|
async function startSlackRelayServer(opts) {
|
|
10269
10821
|
const log = opts.onLog ?? console.log;
|
|
10270
10822
|
const appToken = opts.appToken.trim();
|
|
10271
10823
|
if (!appToken.startsWith("xapp-")) {
|
|
10272
10824
|
throw new Error("SIDEBOARD_SLACK_APP_TOKEN must be an xapp-\u2026 app-level token");
|
|
10273
10825
|
}
|
|
10826
|
+
const clientId = (opts.clientId ?? slackOAuthRelayClientId()).trim();
|
|
10827
|
+
const clientSecret = (opts.clientSecret ?? process.env.SIDEBOARD_SLACK_CLIENT_SECRET ?? "").trim();
|
|
10828
|
+
const redirectUri = (opts.oauthRedirectUri ?? slackOAuthRelayRedirectUri()).trim();
|
|
10829
|
+
const pending = new SlackOAuthPendingStore();
|
|
10274
10830
|
const hub = opts.hub ?? new SlackRelayHub({
|
|
10275
10831
|
fetchImpl: opts.fetchImpl,
|
|
10276
10832
|
onLog: log
|
|
10277
10833
|
});
|
|
10278
|
-
const
|
|
10279
|
-
const
|
|
10280
|
-
if (
|
|
10281
|
-
|
|
10282
|
-
|
|
10283
|
-
|
|
10834
|
+
const handleCallback = async (reqUrl, res) => {
|
|
10835
|
+
const url2 = parseSlackOAuthCallbackUrl(reqUrl);
|
|
10836
|
+
if (!url2) return false;
|
|
10837
|
+
const error = url2.searchParams.get("error")?.trim();
|
|
10838
|
+
const state = url2.searchParams.get("state")?.trim() ?? "";
|
|
10839
|
+
const code = url2.searchParams.get("code")?.trim() ?? "";
|
|
10840
|
+
if (error) {
|
|
10841
|
+
if (state) pending.put(state, { ok: false, error: `Slack OAuth: ${error}` });
|
|
10842
|
+
sendHtml(
|
|
10843
|
+
res,
|
|
10844
|
+
400,
|
|
10845
|
+
"Slack",
|
|
10846
|
+
`<h1>Authorization cancelled</h1><p>${escapeHtml(error)}</p>`
|
|
10847
|
+
);
|
|
10848
|
+
return true;
|
|
10284
10849
|
}
|
|
10285
|
-
if (
|
|
10286
|
-
res
|
|
10287
|
-
|
|
10288
|
-
|
|
10289
|
-
|
|
10290
|
-
|
|
10291
|
-
|
|
10292
|
-
|
|
10850
|
+
if (!state || !code) {
|
|
10851
|
+
sendHtml(res, 400, "Slack", "<h1>Invalid callback</h1><p>State or code missing.</p>");
|
|
10852
|
+
return true;
|
|
10853
|
+
}
|
|
10854
|
+
if (!clientSecret) {
|
|
10855
|
+
pending.put(state, {
|
|
10856
|
+
ok: false,
|
|
10857
|
+
error: "Relay is missing SIDEBOARD_SLACK_CLIENT_SECRET"
|
|
10858
|
+
});
|
|
10859
|
+
sendHtml(
|
|
10860
|
+
res,
|
|
10861
|
+
500,
|
|
10862
|
+
"Slack",
|
|
10863
|
+
"<h1>Relay misconfigured</h1><p>OAuth client secret is not set on the relay.</p>"
|
|
10293
10864
|
);
|
|
10294
|
-
return;
|
|
10865
|
+
return true;
|
|
10866
|
+
}
|
|
10867
|
+
try {
|
|
10868
|
+
const payload = await exchangeSlackOAuthCode({
|
|
10869
|
+
clientId,
|
|
10870
|
+
clientSecret,
|
|
10871
|
+
code,
|
|
10872
|
+
redirectUri,
|
|
10873
|
+
fetchImpl: opts.fetchImpl
|
|
10874
|
+
});
|
|
10875
|
+
pending.put(state, { ok: true, payload });
|
|
10876
|
+
sendHtml(
|
|
10877
|
+
res,
|
|
10878
|
+
200,
|
|
10879
|
+
"Slack connected",
|
|
10880
|
+
"<h1>Slack workspace connected</h1><p>You can close this tab and return to Sideboard.</p>"
|
|
10881
|
+
);
|
|
10882
|
+
} catch (err) {
|
|
10883
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
10884
|
+
pending.put(state, { ok: false, error: message });
|
|
10885
|
+
sendHtml(res, 400, "Slack", `<h1>Could not connect Slack</h1><p>${escapeHtml(message)}</p>`);
|
|
10886
|
+
}
|
|
10887
|
+
return true;
|
|
10888
|
+
};
|
|
10889
|
+
const handleResult = (reqUrl, res) => {
|
|
10890
|
+
const url2 = parseSlackOAuthResultUrl(reqUrl);
|
|
10891
|
+
if (!url2) return false;
|
|
10892
|
+
const state = url2.searchParams.get("state")?.trim() ?? "";
|
|
10893
|
+
const value = pending.take(state);
|
|
10894
|
+
if (!value) {
|
|
10895
|
+
sendJson(res, 404, { ok: false, error: "pending" });
|
|
10896
|
+
return true;
|
|
10897
|
+
}
|
|
10898
|
+
if (!value.ok) {
|
|
10899
|
+
sendJson(res, 400, { ok: false, error: value.error });
|
|
10900
|
+
return true;
|
|
10295
10901
|
}
|
|
10296
|
-
res.
|
|
10297
|
-
|
|
10902
|
+
sendJson(res, 200, { ok: true, ...value.payload });
|
|
10903
|
+
return true;
|
|
10904
|
+
};
|
|
10905
|
+
const httpServer = createServer3((req, res) => {
|
|
10906
|
+
void (async () => {
|
|
10907
|
+
const reqUrl = req.url || "/";
|
|
10908
|
+
if (await handleCallback(reqUrl, res)) return;
|
|
10909
|
+
if (handleResult(reqUrl, res)) return;
|
|
10910
|
+
if (req.url === "/health" || req.url === "/") {
|
|
10911
|
+
sendJson(res, 200, {
|
|
10912
|
+
ok: true,
|
|
10913
|
+
service: "sideboard-slack-relay",
|
|
10914
|
+
sessions: hub.listSessions().length,
|
|
10915
|
+
oauth: Boolean(clientSecret)
|
|
10916
|
+
});
|
|
10917
|
+
return;
|
|
10918
|
+
}
|
|
10919
|
+
res.writeHead(404);
|
|
10920
|
+
res.end("Not found");
|
|
10921
|
+
})().catch((err) => {
|
|
10922
|
+
log(`http error: ${err instanceof Error ? err.message : String(err)}`);
|
|
10923
|
+
if (!res.headersSent) {
|
|
10924
|
+
res.writeHead(500);
|
|
10925
|
+
res.end("Internal error");
|
|
10926
|
+
}
|
|
10927
|
+
});
|
|
10298
10928
|
});
|
|
10299
|
-
const wss = new WebSocketServer({ server: httpServer, path:
|
|
10929
|
+
const wss = new WebSocketServer({ server: httpServer, path: SLACK_RELAY_DESKTOP_PATH });
|
|
10300
10930
|
wss.on("connection", (ws, _req) => {
|
|
10301
10931
|
const socket = {
|
|
10302
10932
|
send: (data) => {
|
|
@@ -10334,12 +10964,12 @@ async function startSlackRelayServer(opts) {
|
|
|
10334
10964
|
});
|
|
10335
10965
|
const address = httpServer.address();
|
|
10336
10966
|
const boundPort = typeof address === "object" && address ? address.port : typeof port === "number" ? port : 0;
|
|
10337
|
-
const url = `ws://${host === "0.0.0.0" ? "127.0.0.1" : host}:${boundPort}
|
|
10967
|
+
const url = `ws://${host === "0.0.0.0" ? "127.0.0.1" : host}:${boundPort}${SLACK_RELAY_DESKTOP_PATH}`;
|
|
10338
10968
|
log(`relay listening on ${url}`);
|
|
10339
10969
|
const ac = new AbortController();
|
|
10340
10970
|
const onAbort = () => ac.abort();
|
|
10341
10971
|
opts.signal?.addEventListener("abort", onAbort, { once: true });
|
|
10342
|
-
const socketModeDone = runSlackSocketMode({
|
|
10972
|
+
const socketModeDone = opts.skipSocketMode ? Promise.resolve() : runSlackSocketMode({
|
|
10343
10973
|
appToken,
|
|
10344
10974
|
signal: ac.signal,
|
|
10345
10975
|
fetchImpl: opts.fetchImpl,
|
|
@@ -10370,6 +11000,7 @@ async function startSlackRelayServer(opts) {
|
|
|
10370
11000
|
};
|
|
10371
11001
|
}
|
|
10372
11002
|
export {
|
|
11003
|
+
AGENT_GIT_ACTIONS,
|
|
10373
11004
|
ATTACHMENTS_DIR,
|
|
10374
11005
|
BAKED_SLACK_RELAY_URL,
|
|
10375
11006
|
BRIGHTSY_MCP_ALLOWED_TOOLS,
|
|
@@ -10415,8 +11046,6 @@ export {
|
|
|
10415
11046
|
SLACK_LISTEN_STOPPED_REPLY,
|
|
10416
11047
|
SLACK_LISTEN_TIMEOUT_REPLY,
|
|
10417
11048
|
SLACK_OAUTH_CANCELLED,
|
|
10418
|
-
SLACK_OAUTH_LOCAL_CALLBACK,
|
|
10419
|
-
SLACK_OAUTH_PORT,
|
|
10420
11049
|
SLACK_OAUTH_REDIRECT,
|
|
10421
11050
|
SLACK_REPLY_FORMATTING,
|
|
10422
11051
|
SLACK_SEEN_REACTION,
|
|
@@ -10428,6 +11057,7 @@ export {
|
|
|
10428
11057
|
addStackLayerFromThread,
|
|
10429
11058
|
addWorkspace,
|
|
10430
11059
|
adoptThread,
|
|
11060
|
+
agentGitPrompt,
|
|
10431
11061
|
allAdapters,
|
|
10432
11062
|
allocatePort,
|
|
10433
11063
|
allocatePortRange,
|
|
@@ -10440,6 +11070,7 @@ export {
|
|
|
10440
11070
|
applyAppEnvironment,
|
|
10441
11071
|
applyCompaction,
|
|
10442
11072
|
applyThreadIntoMain,
|
|
11073
|
+
applyTurnUsage,
|
|
10443
11074
|
assertOrchestratorCapableAgent,
|
|
10444
11075
|
attachmentFromAbsolutePath,
|
|
10445
11076
|
attachmentsFromBuffers,
|
|
@@ -10481,12 +11112,14 @@ export {
|
|
|
10481
11112
|
codexAdapter,
|
|
10482
11113
|
coerceOrchestratorAgent,
|
|
10483
11114
|
collectTakenTeamSlugs,
|
|
11115
|
+
commentLinearIssue,
|
|
10484
11116
|
commitAll,
|
|
10485
11117
|
conductorBundledBinDir,
|
|
10486
11118
|
conductorDbPath,
|
|
10487
11119
|
confirmLand,
|
|
10488
11120
|
connectBrightsyTeam,
|
|
10489
11121
|
connectSlackToken,
|
|
11122
|
+
contextTokens,
|
|
10490
11123
|
coordinatorSystemPrompt,
|
|
10491
11124
|
coordinatorTurnReminder,
|
|
10492
11125
|
copyConfiguredFiles,
|
|
@@ -10495,6 +11128,7 @@ export {
|
|
|
10495
11128
|
createEmptyThread,
|
|
10496
11129
|
createExistingBranchWorktree,
|
|
10497
11130
|
createGlobalChat,
|
|
11131
|
+
createLinearIssue,
|
|
10498
11132
|
createLinearPkce,
|
|
10499
11133
|
createOrUpdatePr,
|
|
10500
11134
|
createPrStack,
|
|
@@ -10547,6 +11181,7 @@ export {
|
|
|
10547
11181
|
formatAgentInstructions,
|
|
10548
11182
|
formatArtifactDirective,
|
|
10549
11183
|
formatBrightsyFetchError,
|
|
11184
|
+
formatFetchError,
|
|
10550
11185
|
formatGhLandError,
|
|
10551
11186
|
formatIpcInvokeError,
|
|
10552
11187
|
formatMessagesAsTranscript,
|
|
@@ -10577,6 +11212,7 @@ export {
|
|
|
10577
11212
|
getIssueSource,
|
|
10578
11213
|
getLinearApiKey,
|
|
10579
11214
|
getLinearAuthToken,
|
|
11215
|
+
getLinearIssue,
|
|
10580
11216
|
getOrchestrator,
|
|
10581
11217
|
getPr,
|
|
10582
11218
|
getPrChecks,
|
|
@@ -10601,6 +11237,7 @@ export {
|
|
|
10601
11237
|
hasRepoHook,
|
|
10602
11238
|
hasWorkspaceHook,
|
|
10603
11239
|
healOrchestrationSoccerTitles,
|
|
11240
|
+
httpFetch,
|
|
10604
11241
|
importConductorWorkspace,
|
|
10605
11242
|
importConductorWorkspaceAsync,
|
|
10606
11243
|
initPrStack,
|
|
@@ -10634,8 +11271,10 @@ export {
|
|
|
10634
11271
|
isSlackExternalReplyPrompt,
|
|
10635
11272
|
isSlackOAuthCancelled,
|
|
10636
11273
|
isThinkingEffort,
|
|
11274
|
+
isThreadCaffeinated,
|
|
10637
11275
|
isWorkspaceScratchPath,
|
|
10638
11276
|
linearAuthorizationHeader,
|
|
11277
|
+
linearGraphql,
|
|
10639
11278
|
linearOAuthAuthorizeUrl,
|
|
10640
11279
|
linearOAuthCredentials,
|
|
10641
11280
|
listAgentSetupInfo,
|
|
@@ -10652,6 +11291,7 @@ export {
|
|
|
10652
11291
|
listIssues,
|
|
10653
11292
|
listLinearIssues,
|
|
10654
11293
|
listLinearIssuesDirect,
|
|
11294
|
+
listLinearTeams,
|
|
10655
11295
|
listModelsForAgent,
|
|
10656
11296
|
listOpencodeModels,
|
|
10657
11297
|
listPrs,
|
|
@@ -10727,9 +11367,11 @@ export {
|
|
|
10727
11367
|
recordSlackOutboundWatch,
|
|
10728
11368
|
refreshGitHubAuth,
|
|
10729
11369
|
refreshSlackReplyBadges,
|
|
11370
|
+
releaseCaffeinateHoldForThread,
|
|
10730
11371
|
removeWorkspace,
|
|
10731
11372
|
removeWorktree,
|
|
10732
11373
|
repoSlug,
|
|
11374
|
+
requestOccupancy,
|
|
10733
11375
|
requestReview,
|
|
10734
11376
|
requireAgent,
|
|
10735
11377
|
resetGhStackDetectCache,
|
|
@@ -10744,6 +11386,8 @@ export {
|
|
|
10744
11386
|
resolveFilesToCopy,
|
|
10745
11387
|
resolveGhAuthToken,
|
|
10746
11388
|
resolveGithubRepoSlug,
|
|
11389
|
+
resolveLinearState,
|
|
11390
|
+
resolveLinearTeam,
|
|
10747
11391
|
resolveLoginCommand,
|
|
10748
11392
|
resolveNewThreadOptions,
|
|
10749
11393
|
resolvePlanMarkdown,
|
|
@@ -10756,6 +11400,7 @@ export {
|
|
|
10756
11400
|
resolveThreadEffort,
|
|
10757
11401
|
resolveVaultKey,
|
|
10758
11402
|
resolveWorktreeStartPoint,
|
|
11403
|
+
rewriteLinearError,
|
|
10759
11404
|
run,
|
|
10760
11405
|
runArchiveScript,
|
|
10761
11406
|
runCloudConnect,
|
|
@@ -10769,6 +11414,7 @@ export {
|
|
|
10769
11414
|
saveLinearOAuth,
|
|
10770
11415
|
secureFileUnlocksWith,
|
|
10771
11416
|
setCaffeinateHold,
|
|
11417
|
+
setHttpFetchImpl,
|
|
10772
11418
|
setStatus,
|
|
10773
11419
|
setVaultMasterKey,
|
|
10774
11420
|
settingsSourceLabel,
|
|
@@ -10786,6 +11432,7 @@ export {
|
|
|
10786
11432
|
slackCoordinatorSourceRef,
|
|
10787
11433
|
slackListenEnabled,
|
|
10788
11434
|
slackOAuthCredentials,
|
|
11435
|
+
slackOAuthResultUrl,
|
|
10789
11436
|
slackRelayUrl,
|
|
10790
11437
|
slugify,
|
|
10791
11438
|
spawnAgentTurn,
|
|
@@ -10831,6 +11478,7 @@ export {
|
|
|
10831
11478
|
updateCodexSettings,
|
|
10832
11479
|
updateDefaultsSettings,
|
|
10833
11480
|
updateIntegrationsSettings,
|
|
11481
|
+
updateLinearIssue,
|
|
10834
11482
|
updateOpencodeSettings,
|
|
10835
11483
|
updateThread,
|
|
10836
11484
|
validateLinearApiKey,
|