@sideboard-ai/core 0.1.74 → 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-2S2JVLZ6.js → agents-LKUHPPEQ.js} +3 -3
- package/dist/{agents-H7M5CQY2.js → agents-XJV6J3K2.js} +3 -3
- package/dist/{caffeinate-hold-KLC6SABD.js → caffeinate-hold-EAZNWJBE.js} +5 -1
- package/dist/caffeinate-hold-OHAUWOA6.js +20 -0
- package/dist/{chunk-KBKPVKYZ.js → chunk-2FJI5JXX.js} +2 -2
- package/dist/{chunk-GJ2LZQJI.js → chunk-2ZIPJRJE.js} +1 -1
- package/dist/{chunk-DG3S2UXP.js → chunk-75U65MBE.js} +1 -1
- package/dist/{chunk-HCWAIEBU.js → chunk-EEKSZTMU.js} +3 -2
- package/dist/{chunk-2NKSHIRI.js → chunk-GWTLKXXP.js} +1 -1
- package/dist/chunk-MFF2RE3I.js +168 -0
- package/dist/{chunk-5KJMNNCB.js → chunk-QD37IILI.js} +2 -2
- package/dist/chunk-RV6DBEDQ.js +170 -0
- package/dist/{chunk-77W62MTX.js → chunk-VERLUKN2.js} +1 -1
- package/dist/{chunk-XA2FQJTN.js → chunk-XJNCWYFR.js} +3 -2
- package/dist/{coordinator-prompt-3XXSG7M2.js → coordinator-prompt-LLFJUO2R.js} +1 -1
- package/dist/{coordinator-prompt-RTUCCPCI.js → coordinator-prompt-MRCMMRSF.js} +1 -1
- package/dist/{global-workspace-PJU6HISJ.js → global-workspace-TWHMYLTZ.js} +2 -2
- package/dist/{global-workspace-CSIS62Z4.js → global-workspace-YNMNO4CL.js} +2 -2
- package/dist/index.cjs +1050 -438
- package/dist/index.d.cts +144 -21
- package/dist/index.d.ts +144 -21
- package/dist/index.js +926 -389
- package/dist/mcp/run-stdio.cjs +709 -276
- package/dist/mcp/run-stdio.js +543 -171
- package/dist/{workspaces-L4TXMUNM.js → workspaces-GQ4XKBD3.js} +3 -3
- package/dist/{workspaces-W72KZL4B.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,
|
|
@@ -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";
|
|
@@ -374,9 +376,9 @@ async function getGitHubStatus() {
|
|
|
374
376
|
};
|
|
375
377
|
}
|
|
376
378
|
const status = await run("gh", ["auth", "status"], { reject: false });
|
|
377
|
-
const
|
|
379
|
+
const text3 = `${status.stdout}
|
|
378
380
|
${status.stderr}`;
|
|
379
|
-
const loginMatch =
|
|
381
|
+
const loginMatch = text3.match(/Logged in to ([^\s]+) account (\S+)/i) ?? text3.match(/Logged in to ([^\s]+) as (\S+)/i);
|
|
380
382
|
if (loginMatch) {
|
|
381
383
|
return {
|
|
382
384
|
connected: true,
|
|
@@ -396,13 +398,39 @@ async function refreshGitHubAuth() {
|
|
|
396
398
|
return getGitHubStatus();
|
|
397
399
|
}
|
|
398
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
|
+
|
|
399
428
|
// src/integrations/linear-oauth.ts
|
|
400
429
|
import { createHash, randomBytes } from "crypto";
|
|
401
430
|
import { createServer } from "http";
|
|
402
431
|
|
|
403
432
|
// src/integrations/linear-app.ts
|
|
404
433
|
var BAKED_LINEAR_CLIENT_ID = "39a15abc7ea5bea17c47f47f85ff5fd0";
|
|
405
|
-
var BAKED_LINEAR_CLIENT_SECRET = "";
|
|
406
434
|
function hasBakedLinearOAuth() {
|
|
407
435
|
return Boolean(BAKED_LINEAR_CLIENT_ID.trim());
|
|
408
436
|
}
|
|
@@ -414,12 +442,12 @@ var LINEAR_AUTHORIZE = "https://linear.app/oauth/authorize";
|
|
|
414
442
|
var LINEAR_TOKEN = "https://api.linear.app/oauth/token";
|
|
415
443
|
var LINEAR_REVOKE = "https://api.linear.app/oauth/revoke";
|
|
416
444
|
var LINEAR_GRAPHQL = "https://api.linear.app/graphql";
|
|
417
|
-
var LINEAR_OAUTH_SCOPES = "read";
|
|
445
|
+
var LINEAR_OAUTH_SCOPES = "read,write";
|
|
418
446
|
var REFRESH_SKEW_MS = 5 * 6e4;
|
|
419
447
|
function linearOAuthCredentials() {
|
|
420
448
|
const settings = loadAppSettings();
|
|
421
449
|
const clientId = process.env.SIDEBOARD_LINEAR_CLIENT_ID?.trim() || settings.integrations.linearClientId?.trim() || BAKED_LINEAR_CLIENT_ID.trim();
|
|
422
|
-
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() || "";
|
|
423
451
|
if (!clientId) {
|
|
424
452
|
throw new Error(
|
|
425
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.`
|
|
@@ -459,7 +487,7 @@ function htmlPage(title, body) {
|
|
|
459
487
|
h1{font-size:1.25rem}p{line-height:1.5;color:#444}</style></head><body>${body}</body></html>`;
|
|
460
488
|
}
|
|
461
489
|
async function exchangeLinearToken(body) {
|
|
462
|
-
const res = await
|
|
490
|
+
const res = await httpFetch(LINEAR_TOKEN, {
|
|
463
491
|
method: "POST",
|
|
464
492
|
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
465
493
|
body
|
|
@@ -481,7 +509,7 @@ async function persistLinearTokens(data, viewer) {
|
|
|
481
509
|
});
|
|
482
510
|
}
|
|
483
511
|
async function fetchLinearViewer(accessToken) {
|
|
484
|
-
const res = await
|
|
512
|
+
const res = await httpFetch(LINEAR_GRAPHQL, {
|
|
485
513
|
method: "POST",
|
|
486
514
|
headers: {
|
|
487
515
|
"Content-Type": "application/json",
|
|
@@ -652,7 +680,7 @@ async function revokeLinearToken(token, tokenTypeHint) {
|
|
|
652
680
|
token,
|
|
653
681
|
token_type_hint: tokenTypeHint
|
|
654
682
|
});
|
|
655
|
-
await
|
|
683
|
+
await httpFetch(LINEAR_REVOKE, {
|
|
656
684
|
method: "POST",
|
|
657
685
|
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
658
686
|
body
|
|
@@ -669,6 +697,18 @@ async function disconnectLinear() {
|
|
|
669
697
|
|
|
670
698
|
// src/integrations/linear.ts
|
|
671
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
|
+
`;
|
|
672
712
|
var ASSIGNED_ISSUES_QUERY = `
|
|
673
713
|
query SideboardAssignedIssues($first: Int!) {
|
|
674
714
|
viewer {
|
|
@@ -677,53 +717,280 @@ query SideboardAssignedIssues($first: Int!) {
|
|
|
677
717
|
orderBy: updatedAt
|
|
678
718
|
filter: { state: { type: { nin: ["completed", "canceled"] } } }
|
|
679
719
|
) {
|
|
680
|
-
nodes {
|
|
681
|
-
id
|
|
682
|
-
identifier
|
|
683
|
-
title
|
|
684
|
-
url
|
|
685
|
-
labels { nodes { name } }
|
|
686
|
-
}
|
|
720
|
+
nodes { ${ISSUE_FIELDS} }
|
|
687
721
|
}
|
|
688
722
|
}
|
|
689
723
|
}
|
|
690
724
|
`;
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
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) {
|
|
694
776
|
throw new Error("Linear is not connected \u2014 sign in from Account settings");
|
|
695
777
|
}
|
|
696
|
-
|
|
697
|
-
|
|
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, {
|
|
698
783
|
method: "POST",
|
|
699
784
|
headers: {
|
|
700
785
|
"Content-Type": "application/json",
|
|
701
786
|
Authorization: linearAuthorizationHeader(apiKey)
|
|
702
787
|
},
|
|
703
|
-
body: JSON.stringify({
|
|
704
|
-
query: ASSIGNED_ISSUES_QUERY,
|
|
705
|
-
variables: { first }
|
|
706
|
-
})
|
|
788
|
+
body: JSON.stringify({ query, variables })
|
|
707
789
|
});
|
|
708
790
|
if (!res.ok) {
|
|
709
791
|
const body = await res.text().catch(() => "");
|
|
710
792
|
throw new Error(
|
|
711
|
-
|
|
793
|
+
rewriteLinearError(
|
|
794
|
+
`Linear API error ${res.status}${body ? `: ${body.slice(0, 200)}` : ""}`
|
|
795
|
+
)
|
|
712
796
|
);
|
|
713
797
|
}
|
|
714
798
|
const json = await res.json();
|
|
715
799
|
if (json.errors?.length) {
|
|
716
|
-
throw new Error(
|
|
800
|
+
throw new Error(
|
|
801
|
+
rewriteLinearError(json.errors.map((e) => e.message ?? "Linear error").join("; "))
|
|
802
|
+
);
|
|
803
|
+
}
|
|
804
|
+
if (json.data == null) {
|
|
805
|
+
throw new Error("Linear API returned no data");
|
|
717
806
|
}
|
|
718
|
-
|
|
719
|
-
|
|
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 {
|
|
720
820
|
id: String(node.id ?? node.identifier ?? ""),
|
|
721
821
|
identifier: String(node.identifier ?? node.id ?? ""),
|
|
722
822
|
title: String(node.title ?? ""),
|
|
723
823
|
url: String(node.url ?? ""),
|
|
724
|
-
|
|
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,
|
|
725
844
|
provider: "linear"
|
|
726
|
-
}
|
|
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
|
+
};
|
|
727
994
|
}
|
|
728
995
|
async function validateLinearApiKey(apiKey) {
|
|
729
996
|
const key = apiKey.trim();
|
|
@@ -868,9 +1135,9 @@ function toolFilePath(input) {
|
|
|
868
1135
|
if (!input) return void 0;
|
|
869
1136
|
return str(input.file_path) ?? str(input.path) ?? str(input.filePath) ?? str(input.filename);
|
|
870
1137
|
}
|
|
871
|
-
function countLines(
|
|
872
|
-
if (!
|
|
873
|
-
return
|
|
1138
|
+
function countLines(text3) {
|
|
1139
|
+
if (!text3) return 0;
|
|
1140
|
+
return text3.split("\n").length;
|
|
874
1141
|
}
|
|
875
1142
|
function diffFromInput(input) {
|
|
876
1143
|
if (!input) return {};
|
|
@@ -997,9 +1264,9 @@ function applyAgentEvent(parts, event) {
|
|
|
997
1264
|
function partsToAssistantText(parts) {
|
|
998
1265
|
return parts.filter((p) => p.type === "text").map((p) => p.text).join("").trim();
|
|
999
1266
|
}
|
|
1000
|
-
function stripBrightsyNdjsonNoise(
|
|
1001
|
-
if (!
|
|
1002
|
-
let out =
|
|
1267
|
+
function stripBrightsyNdjsonNoise(text3) {
|
|
1268
|
+
if (!text3 || !text3.includes('"type"')) return text3;
|
|
1269
|
+
let out = text3;
|
|
1003
1270
|
out = out.replace(
|
|
1004
1271
|
/\{"type":"(?:tool_use|tool_result|tool|thinking|usage|done|error)"[\s\S]*?\}\s*(?=\{"type":"|$|(?=[A-Za-z*#]))/g,
|
|
1005
1272
|
""
|
|
@@ -1065,9 +1332,9 @@ async function spawnAgentTurn(thread, input, onEvent) {
|
|
|
1065
1332
|
`Cannot spawn ${thread.agent}: thread ${thread.id} has no worktreePath`
|
|
1066
1333
|
);
|
|
1067
1334
|
}
|
|
1068
|
-
const { isGlobalThread: isGlobalThread2 } = await import("./global-workspace-
|
|
1335
|
+
const { isGlobalThread: isGlobalThread2 } = await import("./global-workspace-YNMNO4CL.js");
|
|
1069
1336
|
if (isGlobalThread2(thread)) {
|
|
1070
|
-
const { ensureGlobalCoordinatorCwd: ensureGlobalCoordinatorCwd2 } = await import("./coordinator-prompt-
|
|
1337
|
+
const { ensureGlobalCoordinatorCwd: ensureGlobalCoordinatorCwd2 } = await import("./coordinator-prompt-LLFJUO2R.js");
|
|
1071
1338
|
ensureGlobalCoordinatorCwd2(
|
|
1072
1339
|
isOrchestratorThread(thread) ? { orchestratorThreadId: thread.id } : void 0
|
|
1073
1340
|
);
|
|
@@ -1154,8 +1421,8 @@ async function spawnAgentTurn(thread, input, onEvent) {
|
|
|
1154
1421
|
onEvent({ type: "exit", data: exitCode });
|
|
1155
1422
|
const finalized = finalizeParts(parts);
|
|
1156
1423
|
const rawText = assistantText.trim() || partsToAssistantText(finalized);
|
|
1157
|
-
const
|
|
1158
|
-
return { exitCode, sessionId, assistantText:
|
|
1424
|
+
const text3 = thread.agent === "brightsy" ? stripBrightsyNdjsonNoise(rawText) : rawText;
|
|
1425
|
+
return { exitCode, sessionId, assistantText: text3, parts: finalized, usage };
|
|
1159
1426
|
});
|
|
1160
1427
|
return {
|
|
1161
1428
|
pid: child.pid,
|
|
@@ -2020,13 +2287,13 @@ function formatStat(files) {
|
|
|
2020
2287
|
return `${n} file${n === 1 ? "" : "s"} changed, ${additions} insertions(+), ${deletions} deletions(-)`;
|
|
2021
2288
|
}
|
|
2022
2289
|
function emptyScopeStats() {
|
|
2023
|
-
const
|
|
2290
|
+
const z4 = emptyStat();
|
|
2024
2291
|
return {
|
|
2025
|
-
commits:
|
|
2026
|
-
uncommitted:
|
|
2027
|
-
staged:
|
|
2028
|
-
unstaged:
|
|
2029
|
-
last_turn:
|
|
2292
|
+
commits: z4,
|
|
2293
|
+
uncommitted: z4,
|
|
2294
|
+
staged: z4,
|
|
2295
|
+
unstaged: z4,
|
|
2296
|
+
last_turn: z4
|
|
2030
2297
|
};
|
|
2031
2298
|
}
|
|
2032
2299
|
function filesFromDiff(nameStatus, numstat, combinedDiff, maxHunk) {
|
|
@@ -3016,16 +3283,16 @@ var PASTE_ATTACH_MIN_CHARS = 1200;
|
|
|
3016
3283
|
var PASTE_ATTACH_MIN_LINES = 15;
|
|
3017
3284
|
var PASTED_NAME_RE = /^Pasted text #(\d+)\.txt$/i;
|
|
3018
3285
|
var PASTED_NAME_ALT_RE = /^pasted-(\d+)\.txt$/i;
|
|
3019
|
-
function pastedTextStats(
|
|
3020
|
-
const chars =
|
|
3286
|
+
function pastedTextStats(text3) {
|
|
3287
|
+
const chars = text3.length;
|
|
3021
3288
|
if (chars === 0) return { chars: 0, lines: 0 };
|
|
3022
|
-
const lines =
|
|
3289
|
+
const lines = text3.split(/\r\n|\r|\n/).length;
|
|
3023
3290
|
return { chars, lines };
|
|
3024
3291
|
}
|
|
3025
|
-
function shouldAttachPastedText(
|
|
3026
|
-
const trimmed =
|
|
3292
|
+
function shouldAttachPastedText(text3) {
|
|
3293
|
+
const trimmed = text3.trim();
|
|
3027
3294
|
if (!trimmed) return false;
|
|
3028
|
-
const { chars, lines } = pastedTextStats(
|
|
3295
|
+
const { chars, lines } = pastedTextStats(text3);
|
|
3029
3296
|
return chars >= PASTE_ATTACH_MIN_CHARS || lines >= PASTE_ATTACH_MIN_LINES;
|
|
3030
3297
|
}
|
|
3031
3298
|
function nextPastedTextName(existing) {
|
|
@@ -3036,13 +3303,13 @@ function nextPastedTextName(existing) {
|
|
|
3036
3303
|
}
|
|
3037
3304
|
return `Pasted text #${max + 1}.txt`;
|
|
3038
3305
|
}
|
|
3039
|
-
function buildPastedTextAttachment(
|
|
3306
|
+
function buildPastedTextAttachment(text3, opts) {
|
|
3040
3307
|
return {
|
|
3041
3308
|
id: opts?.id ?? randomUUID2(),
|
|
3042
3309
|
name: opts?.name ?? "Pasted text #1.txt",
|
|
3043
3310
|
kind: "file",
|
|
3044
3311
|
path: opts?.path,
|
|
3045
|
-
content:
|
|
3312
|
+
content: text3
|
|
3046
3313
|
};
|
|
3047
3314
|
}
|
|
3048
3315
|
|
|
@@ -3090,9 +3357,9 @@ async function tryClaudeSummary(transcript, opts) {
|
|
|
3090
3357
|
{ cwd: opts?.cwd, reject: false }
|
|
3091
3358
|
);
|
|
3092
3359
|
if (exitCode !== 0) return null;
|
|
3093
|
-
const
|
|
3094
|
-
if (
|
|
3095
|
-
return
|
|
3360
|
+
const text3 = stdout.trim();
|
|
3361
|
+
if (text3.length < 40) return null;
|
|
3362
|
+
return text3;
|
|
3096
3363
|
} catch {
|
|
3097
3364
|
return null;
|
|
3098
3365
|
}
|
|
@@ -3141,9 +3408,9 @@ function extractiveSummary(transcript) {
|
|
|
3141
3408
|
);
|
|
3142
3409
|
return parts.join("\n");
|
|
3143
3410
|
}
|
|
3144
|
-
function clipSummary(
|
|
3145
|
-
if (
|
|
3146
|
-
return `${
|
|
3411
|
+
function clipSummary(text3) {
|
|
3412
|
+
if (text3.length <= MAX_SUMMARY_CHARS) return text3;
|
|
3413
|
+
return `${text3.slice(0, MAX_SUMMARY_CHARS)}
|
|
3147
3414
|
|
|
3148
3415
|
[\u2026summary truncated\u2026]`;
|
|
3149
3416
|
}
|
|
@@ -3545,7 +3812,7 @@ async function createThread(input, _onSetupLine) {
|
|
|
3545
3812
|
return readThread(thread.id) ?? thread;
|
|
3546
3813
|
}
|
|
3547
3814
|
async function listLinearIssues(agent, repoPath) {
|
|
3548
|
-
const { getAdapter: getAdapter2 } = await import("./agents-
|
|
3815
|
+
const { getAdapter: getAdapter2 } = await import("./agents-XJV6J3K2.js");
|
|
3549
3816
|
await requireAgent(agent, { requireLinear: true });
|
|
3550
3817
|
const adapter = getAdapter2(agent);
|
|
3551
3818
|
if (!adapter.listLinearIssues) {
|
|
@@ -4043,13 +4310,13 @@ function resolveConductorCursorAgentId(workspacePath) {
|
|
|
4043
4310
|
for (const hash of hashes) {
|
|
4044
4311
|
const agentsFile = join7(CURSOR_SDK_STORE, hash, "agents.ndjson");
|
|
4045
4312
|
if (!existsSync9(agentsFile)) continue;
|
|
4046
|
-
let
|
|
4313
|
+
let text3;
|
|
4047
4314
|
try {
|
|
4048
|
-
|
|
4315
|
+
text3 = readFileSync7(agentsFile, "utf8");
|
|
4049
4316
|
} catch {
|
|
4050
4317
|
continue;
|
|
4051
4318
|
}
|
|
4052
|
-
for (const line of
|
|
4319
|
+
for (const line of text3.split("\n")) {
|
|
4053
4320
|
const trimmed = line.trim();
|
|
4054
4321
|
if (!trimmed) continue;
|
|
4055
4322
|
try {
|
|
@@ -4088,7 +4355,7 @@ async function adoptThread(input) {
|
|
|
4088
4355
|
messages: input.messages ?? []
|
|
4089
4356
|
});
|
|
4090
4357
|
writeThread(thread);
|
|
4091
|
-
const { ensureWorkspace: ensureWorkspace2 } = await import("./workspaces-
|
|
4358
|
+
const { ensureWorkspace: ensureWorkspace2 } = await import("./workspaces-O3U5BENH.js");
|
|
4092
4359
|
await ensureWorkspace2(repoPath);
|
|
4093
4360
|
return thread;
|
|
4094
4361
|
}
|
|
@@ -4539,8 +4806,8 @@ ${input.permalink}` : "";
|
|
|
4539
4806
|
|
|
4540
4807
|
${body}${link}`;
|
|
4541
4808
|
}
|
|
4542
|
-
function isSlackExternalReplyPrompt(
|
|
4543
|
-
return
|
|
4809
|
+
function isSlackExternalReplyPrompt(text3) {
|
|
4810
|
+
return text3.startsWith("Slack reply from ") && text3.includes("not a command");
|
|
4544
4811
|
}
|
|
4545
4812
|
function pendingSlackExternalReplies(messages) {
|
|
4546
4813
|
let i = messages.length - 1;
|
|
@@ -4564,9 +4831,9 @@ function formatSlackRepliesForTurn(replies) {
|
|
|
4564
4831
|
function listSlackOutboundWatches() {
|
|
4565
4832
|
return pruneWatches(readStore3());
|
|
4566
4833
|
}
|
|
4567
|
-
function formatOwnerSlackFyi(userName,
|
|
4834
|
+
function formatOwnerSlackFyi(userName, text3) {
|
|
4568
4835
|
const who = userName.trim() || "Someone";
|
|
4569
|
-
const body =
|
|
4836
|
+
const body = text3.trim() || "(no text)";
|
|
4570
4837
|
return `${who} replied in Slack:
|
|
4571
4838
|
${body}`;
|
|
4572
4839
|
}
|
|
@@ -4581,7 +4848,7 @@ async function relayExternalReply(opts) {
|
|
|
4581
4848
|
const thread = readThread(threadId);
|
|
4582
4849
|
if (!thread || thread.status === "archived") return true;
|
|
4583
4850
|
try {
|
|
4584
|
-
const
|
|
4851
|
+
const text3 = formatSlackExternalReplyPrompt({
|
|
4585
4852
|
userName: opts.reply.userName,
|
|
4586
4853
|
kind: opts.watch.kind,
|
|
4587
4854
|
toLabel: opts.watch.toLabel,
|
|
@@ -4590,7 +4857,7 @@ async function relayExternalReply(opts) {
|
|
|
4590
4857
|
});
|
|
4591
4858
|
appendMessage(threadId, {
|
|
4592
4859
|
role: "agent",
|
|
4593
|
-
text:
|
|
4860
|
+
text: text3,
|
|
4594
4861
|
ts: (/* @__PURE__ */ new Date()).toISOString()
|
|
4595
4862
|
});
|
|
4596
4863
|
} catch {
|
|
@@ -4787,7 +5054,7 @@ async function refreshSlackReplyBadges(opts) {
|
|
|
4787
5054
|
const messages = await fetchMessages(token, watch, opts?.fetchImpl);
|
|
4788
5055
|
const replies = messages.filter((m) => isHumanReply(m, watch)).sort((a, b) => Number(a.ts) - Number(b.ts));
|
|
4789
5056
|
if (replies.length === 0) continue;
|
|
4790
|
-
const
|
|
5057
|
+
const injected2 = new Set(watch.injectedReplyTs ?? []);
|
|
4791
5058
|
const collected = [...watch.replies ?? []];
|
|
4792
5059
|
let lastSeenTs = watch.lastSeenTs;
|
|
4793
5060
|
let latestUser;
|
|
@@ -4821,7 +5088,7 @@ async function refreshSlackReplyBadges(opts) {
|
|
|
4821
5088
|
latestName = replyUserName;
|
|
4822
5089
|
latestText = reply.text;
|
|
4823
5090
|
latestPermalink = permalink;
|
|
4824
|
-
if (
|
|
5091
|
+
if (injected2.has(ts)) {
|
|
4825
5092
|
lastSeenTs = ts;
|
|
4826
5093
|
continue;
|
|
4827
5094
|
}
|
|
@@ -4832,7 +5099,7 @@ async function refreshSlackReplyBadges(opts) {
|
|
|
4832
5099
|
fetchImpl: opts?.fetchImpl
|
|
4833
5100
|
});
|
|
4834
5101
|
if (!delivered) break;
|
|
4835
|
-
|
|
5102
|
+
injected2.add(ts);
|
|
4836
5103
|
lastSeenTs = ts;
|
|
4837
5104
|
}
|
|
4838
5105
|
watches[i] = {
|
|
@@ -4844,7 +5111,7 @@ async function refreshSlackReplyBadges(opts) {
|
|
|
4844
5111
|
replyTs: lastSeenTs,
|
|
4845
5112
|
replyPreview: latestText.slice(0, 140),
|
|
4846
5113
|
permalink: latestPermalink,
|
|
4847
|
-
injectedReplyTs: [...
|
|
5114
|
+
injectedReplyTs: [...injected2],
|
|
4848
5115
|
replies: collected.slice(-MAX_REPLIES_PER_WATCH)
|
|
4849
5116
|
};
|
|
4850
5117
|
changed = true;
|
|
@@ -5421,8 +5688,8 @@ function buildQuotaHandoffAttachment(from, limitText, fallbackAgent) {
|
|
|
5421
5688
|
);
|
|
5422
5689
|
const recent = from.messages.slice(-8).map((m) => {
|
|
5423
5690
|
const role = m.role === "user" ? "User" : m.role === "agent" ? "Agent" : "Summary";
|
|
5424
|
-
const
|
|
5425
|
-
return
|
|
5691
|
+
const text3 = m.text.trim().replace(/\s+/g, " ").slice(0, 280);
|
|
5692
|
+
return text3 ? `- ${role}: ${text3}` : null;
|
|
5426
5693
|
}).filter(Boolean);
|
|
5427
5694
|
const body = [
|
|
5428
5695
|
`# Orchestration handoff`,
|
|
@@ -5842,11 +6109,11 @@ var Orchestrator = class {
|
|
|
5842
6109
|
return results;
|
|
5843
6110
|
}
|
|
5844
6111
|
/** Edit the text of a not-yet-started queued message. */
|
|
5845
|
-
async editQueuedMessage(threadRef, index,
|
|
6112
|
+
async editQueuedMessage(threadRef, index, text3) {
|
|
5846
6113
|
const thread = this.requireThread(threadRef);
|
|
5847
6114
|
return withThreadLock(thread.id, async () => {
|
|
5848
6115
|
const current = this.requireThread(thread.id);
|
|
5849
|
-
const trimmed =
|
|
6116
|
+
const trimmed = text3.trim();
|
|
5850
6117
|
if (!trimmed || index < 0 || index >= current.queue.length) {
|
|
5851
6118
|
return current;
|
|
5852
6119
|
}
|
|
@@ -6606,6 +6873,14 @@ var Orchestrator = class {
|
|
|
6606
6873
|
throw new Error(`${action} is not available on the global coordinator`);
|
|
6607
6874
|
}
|
|
6608
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
|
+
}
|
|
6609
6884
|
async diff(threadRef, opts) {
|
|
6610
6885
|
const thread = this.requireThread(threadRef);
|
|
6611
6886
|
this.assertNotGlobal(thread, "Diff");
|
|
@@ -6975,6 +7250,7 @@ var Orchestrator = class {
|
|
|
6975
7250
|
async archive(threadRef) {
|
|
6976
7251
|
const thread = this.requireThread(threadRef);
|
|
6977
7252
|
this.stop(thread.id);
|
|
7253
|
+
this.releaseOrchestratorCaffeinate(thread);
|
|
6978
7254
|
if (isGlobalThread(thread)) {
|
|
6979
7255
|
const archived2 = setStatus(thread.id, "archived");
|
|
6980
7256
|
this.emit({ type: "status_changed", threadId: archived2.id, status: "archived" });
|
|
@@ -6999,7 +7275,7 @@ var Orchestrator = class {
|
|
|
6999
7275
|
this.emit({ type: "status_changed", threadId: archived.id, status: "archived" });
|
|
7000
7276
|
if (thread.repoPath && !isGlobalRepoPath(thread.repoPath)) {
|
|
7001
7277
|
try {
|
|
7002
|
-
const { ensureWorkspace: ensureWorkspace2 } = await import("./workspaces-
|
|
7278
|
+
const { ensureWorkspace: ensureWorkspace2 } = await import("./workspaces-O3U5BENH.js");
|
|
7003
7279
|
await ensureWorkspace2(thread.repoPath);
|
|
7004
7280
|
} catch {
|
|
7005
7281
|
}
|
|
@@ -7009,6 +7285,7 @@ var Orchestrator = class {
|
|
|
7009
7285
|
async purge(threadRef, opts) {
|
|
7010
7286
|
const thread = this.requireThread(threadRef);
|
|
7011
7287
|
this.stop(thread.id);
|
|
7288
|
+
this.releaseOrchestratorCaffeinate(thread);
|
|
7012
7289
|
if (isGlobalThread(thread)) {
|
|
7013
7290
|
deleteThreadRecord(thread.id);
|
|
7014
7291
|
return;
|
|
@@ -7272,7 +7549,7 @@ function formatPlanQuestionsForChat(questions) {
|
|
|
7272
7549
|
// src/mcp/server.ts
|
|
7273
7550
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
7274
7551
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
7275
|
-
import { z as
|
|
7552
|
+
import { z as z3 } from "zod";
|
|
7276
7553
|
import { basename as basename4 } from "path";
|
|
7277
7554
|
|
|
7278
7555
|
// src/mcp/archive-guard.ts
|
|
@@ -7500,8 +7777,8 @@ function labelGithubUrl(url) {
|
|
|
7500
7777
|
}
|
|
7501
7778
|
return { kind: "other", label: "GitHub", url: raw };
|
|
7502
7779
|
}
|
|
7503
|
-
function appendGithubLink(
|
|
7504
|
-
const body =
|
|
7780
|
+
function appendGithubLink(text3, githubUrl) {
|
|
7781
|
+
const body = text3.trimEnd();
|
|
7505
7782
|
if (!githubUrl?.trim()) return body;
|
|
7506
7783
|
const labeled = labelGithubUrl(githubUrl);
|
|
7507
7784
|
if (!labeled) {
|
|
@@ -7784,6 +8061,101 @@ function registerSlackTools(server) {
|
|
|
7784
8061
|
);
|
|
7785
8062
|
}
|
|
7786
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
|
+
|
|
7787
8159
|
// src/mcp/server.ts
|
|
7788
8160
|
var MAX_ORCH_THREADS = 5;
|
|
7789
8161
|
var CREATE_THREAD_TIMEOUT_MS = 9e4;
|
|
@@ -7860,7 +8232,7 @@ async function startMcpServer() {
|
|
|
7860
8232
|
server.tool(
|
|
7861
8233
|
"get_thread",
|
|
7862
8234
|
"Get a compact thread summary by id/ref",
|
|
7863
|
-
{ ref:
|
|
8235
|
+
{ ref: z3.string() },
|
|
7864
8236
|
async ({ ref }) => {
|
|
7865
8237
|
const t = orch.getThread(ref);
|
|
7866
8238
|
if (!t) {
|
|
@@ -7889,14 +8261,14 @@ async function startMcpServer() {
|
|
|
7889
8261
|
"present_artifact",
|
|
7890
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.",
|
|
7891
8263
|
{
|
|
7892
|
-
title:
|
|
7893
|
-
type:
|
|
8264
|
+
title: z3.string().describe("Short title shown in the artifact pane header"),
|
|
8265
|
+
type: z3.enum(["html", "svg", "markdown", "react"]).describe(
|
|
7894
8266
|
"Artifact kind \u2014 html opens an iframe preview; react transpiles and renders a default-exported component in a sandboxed iframe"
|
|
7895
8267
|
),
|
|
7896
|
-
content:
|
|
8268
|
+
content: z3.string().describe(
|
|
7897
8269
|
"Full document body (complete HTML page, SVG markup, markdown, or a React component module with a default export)"
|
|
7898
8270
|
),
|
|
7899
|
-
artifact_id:
|
|
8271
|
+
artifact_id: z3.string().optional().describe("Stable id when updating the same artifact across turns")
|
|
7900
8272
|
},
|
|
7901
8273
|
async ({ title, type, artifact_id }) => {
|
|
7902
8274
|
const id = artifact_id?.trim() || `artifact_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 8)}`;
|
|
@@ -7914,15 +8286,15 @@ async function startMcpServer() {
|
|
|
7914
8286
|
"ask_user",
|
|
7915
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.",
|
|
7916
8288
|
{
|
|
7917
|
-
questions:
|
|
7918
|
-
|
|
7919
|
-
question:
|
|
7920
|
-
header:
|
|
7921
|
-
multiSelect:
|
|
7922
|
-
options:
|
|
7923
|
-
|
|
7924
|
-
label:
|
|
7925
|
-
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)")
|
|
7926
8298
|
})
|
|
7927
8299
|
).min(2).max(6).describe("2\u20136 choices (Sideboard also offers Other)")
|
|
7928
8300
|
})
|
|
@@ -7941,9 +8313,9 @@ async function startMcpServer() {
|
|
|
7941
8313
|
"present_plan",
|
|
7942
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.",
|
|
7943
8315
|
{
|
|
7944
|
-
title:
|
|
7945
|
-
content:
|
|
7946
|
-
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")
|
|
7947
8319
|
},
|
|
7948
8320
|
async ({ title, content, thread_id }) => {
|
|
7949
8321
|
const { writePlanFile: writePlanFile2 } = await import("./plan-file-6O7G4VPQ.js");
|
|
@@ -7966,15 +8338,15 @@ async function startMcpServer() {
|
|
|
7966
8338
|
"present_schema",
|
|
7967
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.",
|
|
7968
8340
|
{
|
|
7969
|
-
title:
|
|
7970
|
-
mode:
|
|
7971
|
-
datasource:
|
|
7972
|
-
resource_id:
|
|
7973
|
-
record_id:
|
|
7974
|
-
resource:
|
|
7975
|
-
record:
|
|
7976
|
-
records:
|
|
7977
|
-
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")
|
|
7978
8350
|
},
|
|
7979
8351
|
async (args) => {
|
|
7980
8352
|
const id = args.pane_id?.trim() || `schema_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 8)}`;
|
|
@@ -7995,10 +8367,10 @@ async function startMcpServer() {
|
|
|
7995
8367
|
"present_files",
|
|
7996
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.",
|
|
7997
8369
|
{
|
|
7998
|
-
title:
|
|
7999
|
-
datasource:
|
|
8000
|
-
path:
|
|
8001
|
-
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")
|
|
8002
8374
|
},
|
|
8003
8375
|
async (args) => {
|
|
8004
8376
|
const id = args.pane_id?.trim() || `files_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 8)}`;
|
|
@@ -8014,19 +8386,21 @@ async function startMcpServer() {
|
|
|
8014
8386
|
}
|
|
8015
8387
|
);
|
|
8016
8388
|
registerSlackTools(server);
|
|
8389
|
+
registerLinearTools(server);
|
|
8017
8390
|
if (sideboardMcpProfile() !== "worktree") {
|
|
8018
|
-
const { getCaffeinateHold: getCaffeinateHold2, setCaffeinateHold: setCaffeinateHold2 } = await import("./caffeinate-hold-
|
|
8391
|
+
const { getCaffeinateHold: getCaffeinateHold2, setCaffeinateHold: setCaffeinateHold2 } = await import("./caffeinate-hold-EAZNWJBE.js");
|
|
8019
8392
|
const { resolveNewThreadOptions: resolveNewThreadOptions2, resolveThreadDefaults: resolveThreadDefaults2 } = await import("./app-settings-3TLA2EJ7.js");
|
|
8020
8393
|
const accountDefaults = resolveThreadDefaults2();
|
|
8021
8394
|
const accountDefaultsHint = `Account defaults: agent=${accountDefaults.agent}, model=${accountDefaults.model?.trim() || "Auto"}, effort=${accountDefaults.effort}`;
|
|
8022
8395
|
server.tool(
|
|
8023
8396
|
"set_caffeinate",
|
|
8024
|
-
"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.",
|
|
8025
8398
|
{
|
|
8026
|
-
enabled:
|
|
8399
|
+
enabled: z3.boolean().describe("true = hold caffeinate on; false = release and let the Mac sleep")
|
|
8027
8400
|
},
|
|
8028
8401
|
async ({ enabled }) => {
|
|
8029
|
-
const
|
|
8402
|
+
const threadId = process.env.SIDEBOARD_ORCHESTRATOR_THREAD_ID?.trim() || null;
|
|
8403
|
+
const state = setCaffeinateHold2(enabled, { threadId });
|
|
8030
8404
|
if (enabled && !state.held) {
|
|
8031
8405
|
return {
|
|
8032
8406
|
content: [
|
|
@@ -8049,7 +8423,7 @@ async function startMcpServer() {
|
|
|
8049
8423
|
text: JSON.stringify({
|
|
8050
8424
|
...getCaffeinateHold2(),
|
|
8051
8425
|
ok: true,
|
|
8052
|
-
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)."
|
|
8053
8427
|
})
|
|
8054
8428
|
}
|
|
8055
8429
|
]
|
|
@@ -8060,15 +8434,15 @@ async function startMcpServer() {
|
|
|
8060
8434
|
"create_thread",
|
|
8061
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.`,
|
|
8062
8436
|
{
|
|
8063
|
-
sourceType:
|
|
8064
|
-
sourceRef:
|
|
8065
|
-
agent:
|
|
8066
|
-
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(
|
|
8067
8441
|
`Usually omit to use Account default model (${accountDefaults.model?.trim() || "Auto"}). Pass null only to force Auto / agent-default.`
|
|
8068
8442
|
),
|
|
8069
|
-
repoPath:
|
|
8070
|
-
title:
|
|
8071
|
-
parentThreadId:
|
|
8443
|
+
repoPath: z3.string(),
|
|
8444
|
+
title: z3.string().optional(),
|
|
8445
|
+
parentThreadId: z3.string().optional().describe(
|
|
8072
8446
|
"Orchestration: omit (preferred) or pass YOUR chat id from the turn reminder / AGENTS.md. Do not invent uuids."
|
|
8073
8447
|
)
|
|
8074
8448
|
},
|
|
@@ -8175,9 +8549,9 @@ async function startMcpServer() {
|
|
|
8175
8549
|
"send_to_thread",
|
|
8176
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.",
|
|
8177
8551
|
{
|
|
8178
|
-
ref:
|
|
8179
|
-
prompt:
|
|
8180
|
-
force_stop:
|
|
8552
|
+
ref: z3.string(),
|
|
8553
|
+
prompt: z3.string(),
|
|
8554
|
+
force_stop: z3.boolean().optional()
|
|
8181
8555
|
},
|
|
8182
8556
|
async ({ ref, prompt, force_stop }) => {
|
|
8183
8557
|
if (force_stop) {
|
|
@@ -8206,8 +8580,8 @@ async function startMcpServer() {
|
|
|
8206
8580
|
"wait_for_turn",
|
|
8207
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.",
|
|
8208
8582
|
{
|
|
8209
|
-
ref:
|
|
8210
|
-
timeoutMs:
|
|
8583
|
+
ref: z3.string(),
|
|
8584
|
+
timeoutMs: z3.number().optional()
|
|
8211
8585
|
},
|
|
8212
8586
|
async ({ ref, timeoutMs }) => {
|
|
8213
8587
|
const thread = await orch.waitForTurn(ref, timeoutMs ?? 6e5);
|
|
@@ -8229,7 +8603,7 @@ async function startMcpServer() {
|
|
|
8229
8603
|
server.tool(
|
|
8230
8604
|
"get_turn_result",
|
|
8231
8605
|
"Final assistant message only (not full transcript)",
|
|
8232
|
-
{ ref:
|
|
8606
|
+
{ ref: z3.string() },
|
|
8233
8607
|
async ({ ref }) => {
|
|
8234
8608
|
const result = orch.getTurnResult(ref);
|
|
8235
8609
|
return { content: [{ type: "text", text: JSON.stringify(result) }] };
|
|
@@ -8239,8 +8613,8 @@ async function startMcpServer() {
|
|
|
8239
8613
|
"stop_thread",
|
|
8240
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.",
|
|
8241
8615
|
{
|
|
8242
|
-
ref:
|
|
8243
|
-
force:
|
|
8616
|
+
ref: z3.string(),
|
|
8617
|
+
force: z3.boolean().optional()
|
|
8244
8618
|
},
|
|
8245
8619
|
async ({ ref, force }) => {
|
|
8246
8620
|
const t = orch.getThread(ref);
|
|
@@ -8270,7 +8644,7 @@ async function startMcpServer() {
|
|
|
8270
8644
|
server.tool(
|
|
8271
8645
|
"archive_thread",
|
|
8272
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).",
|
|
8273
|
-
{ ref:
|
|
8647
|
+
{ ref: z3.string() },
|
|
8274
8648
|
async ({ ref }) => {
|
|
8275
8649
|
const t = orch.getThread(ref);
|
|
8276
8650
|
if (!t) {
|
|
@@ -8303,7 +8677,7 @@ async function startMcpServer() {
|
|
|
8303
8677
|
server.tool(
|
|
8304
8678
|
"restore_thread",
|
|
8305
8679
|
"Restore an archived thread (recreates worktree from branch when needed)",
|
|
8306
|
-
{ ref:
|
|
8680
|
+
{ ref: z3.string() },
|
|
8307
8681
|
async ({ ref }) => {
|
|
8308
8682
|
try {
|
|
8309
8683
|
const restored = await orch.restore(ref);
|
|
@@ -8329,8 +8703,8 @@ async function startMcpServer() {
|
|
|
8329
8703
|
"get_diff",
|
|
8330
8704
|
"Compact diff summary (capped hunks, paginated)",
|
|
8331
8705
|
{
|
|
8332
|
-
ref:
|
|
8333
|
-
maxFiles:
|
|
8706
|
+
ref: z3.string(),
|
|
8707
|
+
maxFiles: z3.number().optional()
|
|
8334
8708
|
},
|
|
8335
8709
|
async ({ ref, maxFiles }) => {
|
|
8336
8710
|
const summary = await orch.diffSummary(ref);
|
|
@@ -8350,7 +8724,7 @@ async function startMcpServer() {
|
|
|
8350
8724
|
server.tool(
|
|
8351
8725
|
"request_review",
|
|
8352
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.',
|
|
8353
|
-
{ ref:
|
|
8727
|
+
{ ref: z3.string().describe("Worktree thread id/ref to review") },
|
|
8354
8728
|
async ({ ref }) => {
|
|
8355
8729
|
try {
|
|
8356
8730
|
const tab = await orch.requestReview(ref);
|
|
@@ -8379,8 +8753,8 @@ async function startMcpServer() {
|
|
|
8379
8753
|
"ask_git",
|
|
8380
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.",
|
|
8381
8755
|
{
|
|
8382
|
-
ref:
|
|
8383
|
-
action:
|
|
8756
|
+
ref: z3.string().describe("Worktree thread id/ref"),
|
|
8757
|
+
action: z3.enum(AGENT_GIT_ACTIONS).describe(
|
|
8384
8758
|
"commit-push | create-draft | create-web | resolve-conflicts | merge"
|
|
8385
8759
|
)
|
|
8386
8760
|
},
|
|
@@ -8407,7 +8781,7 @@ async function startMcpServer() {
|
|
|
8407
8781
|
}
|
|
8408
8782
|
}
|
|
8409
8783
|
);
|
|
8410
|
-
const agentEnum =
|
|
8784
|
+
const agentEnum = z3.enum(["claude", "codex", "opencode", "brightsy", "cursor"]);
|
|
8411
8785
|
server.tool(
|
|
8412
8786
|
"list_models",
|
|
8413
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.",
|
|
@@ -8430,11 +8804,11 @@ async function startMcpServer() {
|
|
|
8430
8804
|
"fork_worktree",
|
|
8431
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.",
|
|
8432
8806
|
{
|
|
8433
|
-
ref:
|
|
8434
|
-
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)"),
|
|
8435
8809
|
agent: agentEnum.optional().describe("Agent for the forked chat (default: same as source)"),
|
|
8436
|
-
model:
|
|
8437
|
-
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()
|
|
8438
8812
|
},
|
|
8439
8813
|
async ({ ref, through_index, agent, model, title }) => {
|
|
8440
8814
|
try {
|
|
@@ -8475,11 +8849,11 @@ async function startMcpServer() {
|
|
|
8475
8849
|
"fork_chat",
|
|
8476
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.",
|
|
8477
8851
|
{
|
|
8478
|
-
ref:
|
|
8479
|
-
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)"),
|
|
8480
8854
|
agent: agentEnum.optional().describe("Agent for the forked chat (default: same as source)"),
|
|
8481
|
-
model:
|
|
8482
|
-
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()
|
|
8483
8857
|
},
|
|
8484
8858
|
async ({ ref, through_index, agent, model, title }) => {
|
|
8485
8859
|
try {
|
|
@@ -8525,8 +8899,8 @@ async function startMcpServer() {
|
|
|
8525
8899
|
"run_dev_script",
|
|
8526
8900
|
"Start a .sideboard/.conductor run script for a thread (default script if name omitted); returns port",
|
|
8527
8901
|
{
|
|
8528
|
-
ref:
|
|
8529
|
-
name:
|
|
8902
|
+
ref: z3.string(),
|
|
8903
|
+
name: z3.string().optional()
|
|
8530
8904
|
},
|
|
8531
8905
|
async ({ ref, name }) => {
|
|
8532
8906
|
const result = await orch.startDev(ref, name);
|
|
@@ -8548,7 +8922,7 @@ async function startMcpServer() {
|
|
|
8548
8922
|
server.tool(
|
|
8549
8923
|
"list_run_scripts",
|
|
8550
8924
|
"List named run scripts available for a thread",
|
|
8551
|
-
{ ref:
|
|
8925
|
+
{ ref: z3.string() },
|
|
8552
8926
|
async ({ ref }) => {
|
|
8553
8927
|
const scripts = orch.listThreadRunScripts(ref);
|
|
8554
8928
|
const active = orch.getActiveRuns(ref);
|
|
@@ -8566,8 +8940,8 @@ async function startMcpServer() {
|
|
|
8566
8940
|
"stop_dev_script",
|
|
8567
8941
|
"Stop a running script for a thread (all scripts if name omitted)",
|
|
8568
8942
|
{
|
|
8569
|
-
ref:
|
|
8570
|
-
name:
|
|
8943
|
+
ref: z3.string(),
|
|
8944
|
+
name: z3.string().optional()
|
|
8571
8945
|
},
|
|
8572
8946
|
async ({ ref, name }) => {
|
|
8573
8947
|
orch.stopDev(ref, name);
|
|
@@ -8577,7 +8951,7 @@ async function startMcpServer() {
|
|
|
8577
8951
|
server.tool(
|
|
8578
8952
|
"run_setup",
|
|
8579
8953
|
"Re-run workspace setup (Sideboard/Conductor settings or .cursor/worktrees.json)",
|
|
8580
|
-
{ ref:
|
|
8954
|
+
{ ref: z3.string() },
|
|
8581
8955
|
async ({ ref }) => {
|
|
8582
8956
|
const result = await orch.runSetup(ref);
|
|
8583
8957
|
return {
|
|
@@ -8588,7 +8962,7 @@ async function startMcpServer() {
|
|
|
8588
8962
|
server.tool(
|
|
8589
8963
|
"add_workspace",
|
|
8590
8964
|
"Register a git repo as a Sideboard workspace",
|
|
8591
|
-
{ repoPath:
|
|
8965
|
+
{ repoPath: z3.string() },
|
|
8592
8966
|
async ({ repoPath }) => {
|
|
8593
8967
|
const ws = await orch.addWorkspace(repoPath);
|
|
8594
8968
|
return { content: [{ type: "text", text: JSON.stringify(ws) }] };
|
|
@@ -8597,7 +8971,7 @@ async function startMcpServer() {
|
|
|
8597
8971
|
server.tool(
|
|
8598
8972
|
"remove_workspace",
|
|
8599
8973
|
"Unregister a Sideboard workspace (does not archive threads)",
|
|
8600
|
-
{ repoPath:
|
|
8974
|
+
{ repoPath: z3.string() },
|
|
8601
8975
|
async ({ repoPath }) => {
|
|
8602
8976
|
orch.removeWorkspace(repoPath);
|
|
8603
8977
|
return { content: [{ type: "text", text: "ok" }] };
|
|
@@ -8607,12 +8981,12 @@ async function startMcpServer() {
|
|
|
8607
8981
|
"fanout",
|
|
8608
8982
|
"Best-of-n: create one thread per agent with the same prompt (parallel attempts)",
|
|
8609
8983
|
{
|
|
8610
|
-
prompt:
|
|
8611
|
-
agents:
|
|
8612
|
-
repoPath:
|
|
8613
|
-
sourceType:
|
|
8614
|
-
sourceRef:
|
|
8615
|
-
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()
|
|
8616
8990
|
},
|
|
8617
8991
|
async (args) => {
|
|
8618
8992
|
const threads = await orch.bestOfN(args);
|
|
@@ -8639,8 +9013,8 @@ async function startMcpServer() {
|
|
|
8639
9013
|
"list_branches",
|
|
8640
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).",
|
|
8641
9015
|
{
|
|
8642
|
-
repoPath:
|
|
8643
|
-
unmergedOnly:
|
|
9016
|
+
repoPath: z3.string(),
|
|
9017
|
+
unmergedOnly: z3.boolean().optional()
|
|
8644
9018
|
},
|
|
8645
9019
|
async ({ repoPath, unmergedOnly }) => {
|
|
8646
9020
|
const root = await resolveRepoRoot(repoPath);
|
|
@@ -8660,7 +9034,7 @@ async function startMcpServer() {
|
|
|
8660
9034
|
server.tool(
|
|
8661
9035
|
"list_prs",
|
|
8662
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.",
|
|
8663
|
-
{ repoPath:
|
|
9037
|
+
{ repoPath: z3.string() },
|
|
8664
9038
|
async ({ repoPath }) => {
|
|
8665
9039
|
const root = await resolveRepoRoot(repoPath);
|
|
8666
9040
|
const prs = await listPrs(root);
|
|
@@ -8679,7 +9053,7 @@ async function startMcpServer() {
|
|
|
8679
9053
|
server.tool(
|
|
8680
9054
|
"get_pr_stack",
|
|
8681
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.",
|
|
8682
|
-
{ ref:
|
|
9056
|
+
{ ref: z3.string() },
|
|
8683
9057
|
async ({ ref }) => {
|
|
8684
9058
|
const stack = await orch.getPrStack(ref);
|
|
8685
9059
|
return {
|
|
@@ -8691,8 +9065,8 @@ async function startMcpServer() {
|
|
|
8691
9065
|
"open_pr_stack_layers",
|
|
8692
9066
|
"Materialize one worktree+thread per stack layer (or a single 1-based layer). Pass a thread ref already on the stack.",
|
|
8693
9067
|
{
|
|
8694
|
-
ref:
|
|
8695
|
-
layer:
|
|
9068
|
+
ref: z3.string(),
|
|
9069
|
+
layer: z3.number().int().positive().optional()
|
|
8696
9070
|
},
|
|
8697
9071
|
async ({ ref, layer }) => {
|
|
8698
9072
|
const result = await orch.openPrStackLayers(ref, { layer });
|
|
@@ -8726,9 +9100,9 @@ async function startMcpServer() {
|
|
|
8726
9100
|
"add_stack_layer",
|
|
8727
9101
|
"Add a branch on top of the current stack (`gh stack add`) and open a worktree+thread for it.",
|
|
8728
9102
|
{
|
|
8729
|
-
ref:
|
|
8730
|
-
branchName:
|
|
8731
|
-
title:
|
|
9103
|
+
ref: z3.string(),
|
|
9104
|
+
branchName: z3.string(),
|
|
9105
|
+
title: z3.string().optional()
|
|
8732
9106
|
},
|
|
8733
9107
|
async ({ ref, branchName, title }) => {
|
|
8734
9108
|
const result = await orch.addStackLayer(ref, branchName, { title });
|
|
@@ -8757,11 +9131,11 @@ async function startMcpServer() {
|
|
|
8757
9131
|
"create_pr_stack",
|
|
8758
9132
|
"Create a new GitHub PR stack with one Sideboard worktree per layer (bottom\u2192top branch names). Requires `gh extension install github/gh-stack`.",
|
|
8759
9133
|
{
|
|
8760
|
-
repoPath:
|
|
8761
|
-
branches:
|
|
8762
|
-
agent:
|
|
8763
|
-
base:
|
|
8764
|
-
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()
|
|
8765
9139
|
},
|
|
8766
9140
|
async (args) => {
|
|
8767
9141
|
const result = await orch.createPrStack({
|
|
@@ -8799,7 +9173,7 @@ async function startMcpServer() {
|
|
|
8799
9173
|
server.tool(
|
|
8800
9174
|
"list_issues",
|
|
8801
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.",
|
|
8802
|
-
{ repoPath:
|
|
9176
|
+
{ repoPath: z3.string() },
|
|
8803
9177
|
async ({ repoPath }) => {
|
|
8804
9178
|
const root = await resolveRepoRoot(repoPath);
|
|
8805
9179
|
const result = await listIssues(root);
|
|
@@ -8810,8 +9184,8 @@ async function startMcpServer() {
|
|
|
8810
9184
|
"list_linear_issues",
|
|
8811
9185
|
"Deprecated: prefer list_issues. Lists assigned Linear issues via the chosen agent MCP connector",
|
|
8812
9186
|
{
|
|
8813
|
-
agent:
|
|
8814
|
-
repoPath:
|
|
9187
|
+
agent: z3.enum(["claude", "codex", "opencode"]),
|
|
9188
|
+
repoPath: z3.string()
|
|
8815
9189
|
},
|
|
8816
9190
|
async ({ agent, repoPath }) => {
|
|
8817
9191
|
const issues = await listLinearIssues(agent, repoPath);
|
|
@@ -8824,18 +9198,7 @@ async function startMcpServer() {
|
|
|
8824
9198
|
}
|
|
8825
9199
|
|
|
8826
9200
|
// src/brightsy/api.ts
|
|
8827
|
-
|
|
8828
|
-
if (!(err instanceof Error)) return `${String(err)} (${url})`;
|
|
8829
|
-
const cause = err.cause;
|
|
8830
|
-
let detail = "";
|
|
8831
|
-
if (cause instanceof Error) {
|
|
8832
|
-
const code = typeof cause.code === "string" ? cause.code : void 0;
|
|
8833
|
-
detail = code ? ` [${code}: ${cause.message}]` : ` [${cause.message}]`;
|
|
8834
|
-
} else if (cause != null) {
|
|
8835
|
-
detail = ` [${String(cause)}]`;
|
|
8836
|
-
}
|
|
8837
|
-
return `${err.message}${detail} (${url})`;
|
|
8838
|
-
}
|
|
9201
|
+
var formatBrightsyFetchError = formatFetchError;
|
|
8839
9202
|
var BrightsySideboardApi = class {
|
|
8840
9203
|
cfg;
|
|
8841
9204
|
fetchImpl;
|
|
@@ -8955,8 +9318,8 @@ var BrightsySideboardApi = class {
|
|
|
8955
9318
|
};
|
|
8956
9319
|
function taskMessageText(task) {
|
|
8957
9320
|
const parts = task.message?.parts ?? [];
|
|
8958
|
-
const
|
|
8959
|
-
return
|
|
9321
|
+
const text3 = parts.find((p) => p.kind === "text")?.text ?? "";
|
|
9322
|
+
return text3.trim();
|
|
8960
9323
|
}
|
|
8961
9324
|
|
|
8962
9325
|
// src/brightsy/cloud-connect.ts
|
|
@@ -9152,63 +9515,128 @@ async function runCloudConnect(opts) {
|
|
|
9152
9515
|
|
|
9153
9516
|
// src/slack/oauth.ts
|
|
9154
9517
|
import { randomBytes as randomBytes2 } from "crypto";
|
|
9155
|
-
import { createServer as createServer3 } from "http";
|
|
9156
9518
|
|
|
9157
9519
|
// src/slack/baked-app.ts
|
|
9158
9520
|
var BAKED_SLACK_CLIENT_ID = "7592788819232.11813536272562";
|
|
9159
|
-
var
|
|
9160
|
-
var BAKED_SLACK_RELAY_URL = "wss://slack-relay.sideboard.cloud/desktop";
|
|
9521
|
+
var BAKED_SLACK_RELAY_URL = "wss://relay.sideboard.cloud/slack/desktop";
|
|
9161
9522
|
function hasBakedSlackOAuth() {
|
|
9162
|
-
return Boolean(BAKED_SLACK_CLIENT_ID.trim()
|
|
9523
|
+
return Boolean(BAKED_SLACK_CLIENT_ID.trim());
|
|
9163
9524
|
}
|
|
9164
9525
|
function slackRelayUrl() {
|
|
9165
9526
|
return process.env.SIDEBOARD_SLACK_RELAY_URL?.trim() || BAKED_SLACK_RELAY_URL.trim();
|
|
9166
9527
|
}
|
|
9167
9528
|
|
|
9168
9529
|
// src/slack/oauth-redirect.ts
|
|
9169
|
-
var
|
|
9170
|
-
var
|
|
9171
|
-
var
|
|
9172
|
-
var
|
|
9173
|
-
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}`;
|
|
9174
9536
|
function slackOAuthRedirectUri() {
|
|
9175
9537
|
return process.env.SIDEBOARD_SLACK_OAUTH_REDIRECT?.trim() || SLACK_OAUTH_REDIRECT;
|
|
9176
9538
|
}
|
|
9177
|
-
function slackOAuthLocalBounceTarget(params) {
|
|
9178
|
-
const dest = new URL(SLACK_OAUTH_LOCAL_CALLBACK);
|
|
9179
|
-
for (const key of BOUNCE_PARAMS) {
|
|
9180
|
-
const value = params.get(key);
|
|
9181
|
-
if (value) dest.searchParams.set(key, value);
|
|
9182
|
-
}
|
|
9183
|
-
return dest.toString();
|
|
9184
|
-
}
|
|
9185
9539
|
function escapeHtml(value) {
|
|
9186
9540
|
return value.replace(/&/g, "&").replace(/"/g, """).replace(/</g, "<").replace(/>/g, ">");
|
|
9187
9541
|
}
|
|
9188
|
-
function
|
|
9189
|
-
|
|
9190
|
-
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>
|
|
9191
9544
|
<style>body{font-family:ui-sans-serif,system-ui,sans-serif;padding:48px 24px;max-width:36rem;margin:0 auto;color:#1a1a1a}
|
|
9192
|
-
h1{font-size:1.25rem}p{line-height:1.5;color:#444}
|
|
9193
|
-
<script>location.replace(${JSON.stringify(target)})</script>
|
|
9194
|
-
</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>`;
|
|
9195
9546
|
}
|
|
9196
|
-
function
|
|
9547
|
+
function parseSlackOAuthCallbackUrl(reqUrl) {
|
|
9197
9548
|
let url;
|
|
9198
9549
|
try {
|
|
9199
9550
|
url = new URL(reqUrl, SLACK_OAUTH_REDIRECT);
|
|
9200
9551
|
} catch {
|
|
9201
9552
|
return null;
|
|
9202
9553
|
}
|
|
9203
|
-
if (url.pathname !==
|
|
9204
|
-
|
|
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");
|
|
9205
9633
|
return {
|
|
9206
|
-
|
|
9207
|
-
|
|
9208
|
-
|
|
9209
|
-
|
|
9210
|
-
|
|
9211
|
-
|
|
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(",")
|
|
9212
9640
|
};
|
|
9213
9641
|
}
|
|
9214
9642
|
|
|
@@ -9244,13 +9672,13 @@ var SLACK_USER_SCOPES = [
|
|
|
9244
9672
|
function slackOAuthCredentials() {
|
|
9245
9673
|
const settings = loadAppSettings();
|
|
9246
9674
|
const clientId = process.env.SIDEBOARD_SLACK_CLIENT_ID?.trim() || settings.integrations.slackClientId?.trim() || BAKED_SLACK_CLIENT_ID.trim();
|
|
9247
|
-
const clientSecret = process.env.SIDEBOARD_SLACK_CLIENT_SECRET?.trim() || settings.integrations.slackClientSecret?.trim() ||
|
|
9248
|
-
if (!clientId
|
|
9675
|
+
const clientSecret = process.env.SIDEBOARD_SLACK_CLIENT_SECRET?.trim() || settings.integrations.slackClientSecret?.trim() || "";
|
|
9676
|
+
if (!clientId) {
|
|
9249
9677
|
throw new Error(
|
|
9250
|
-
"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."
|
|
9251
9679
|
);
|
|
9252
9680
|
}
|
|
9253
|
-
return { clientId, clientSecret };
|
|
9681
|
+
return { clientId, clientSecret: clientSecret || null };
|
|
9254
9682
|
}
|
|
9255
9683
|
var SLACK_OAUTH_CANCELLED = "Slack sign-in cancelled";
|
|
9256
9684
|
var SlackOAuthCancelledError = class extends Error {
|
|
@@ -9275,127 +9703,100 @@ function slackOAuthAuthorizeUrl(clientId, state) {
|
|
|
9275
9703
|
});
|
|
9276
9704
|
return `https://slack.com/oauth/v2/authorize?${params.toString()}`;
|
|
9277
9705
|
}
|
|
9278
|
-
function
|
|
9279
|
-
return
|
|
9280
|
-
|
|
9281
|
-
|
|
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
|
+
});
|
|
9282
9733
|
}
|
|
9283
9734
|
async function startSlackOAuth(opts) {
|
|
9284
9735
|
if (opts?.signal?.aborted) {
|
|
9285
9736
|
throw new SlackOAuthCancelledError();
|
|
9286
9737
|
}
|
|
9287
|
-
const { clientId
|
|
9738
|
+
const { clientId } = slackOAuthCredentials();
|
|
9288
9739
|
const state = randomBytes2(16).toString("hex");
|
|
9289
9740
|
const authorizeUrl = slackOAuthAuthorizeUrl(clientId, state);
|
|
9290
9741
|
const timeoutMs = opts?.timeoutMs ?? 5 * 6e4;
|
|
9291
|
-
const
|
|
9292
|
-
|
|
9293
|
-
|
|
9294
|
-
|
|
9295
|
-
|
|
9296
|
-
|
|
9297
|
-
|
|
9298
|
-
|
|
9299
|
-
|
|
9300
|
-
const server = createServer3((req, res2) => {
|
|
9301
|
-
try {
|
|
9302
|
-
const url = new URL(req.url || "/", `http://127.0.0.1:${SLACK_OAUTH_PORT}`);
|
|
9303
|
-
if (url.pathname !== "/callback") {
|
|
9304
|
-
res2.writeHead(404);
|
|
9305
|
-
res2.end("Not found");
|
|
9306
|
-
return;
|
|
9307
|
-
}
|
|
9308
|
-
const err = url.searchParams.get("error");
|
|
9309
|
-
const gotState = url.searchParams.get("state");
|
|
9310
|
-
const gotCode = url.searchParams.get("code");
|
|
9311
|
-
if (err) {
|
|
9312
|
-
res2.writeHead(400, { "Content-Type": "text/html" });
|
|
9313
|
-
res2.end(htmlPage2("Slack", `<h1>Authorization cancelled</h1><p>${err}</p>`));
|
|
9314
|
-
finish(new SlackOAuthCancelledError(`Slack OAuth: ${err}`));
|
|
9315
|
-
return;
|
|
9316
|
-
}
|
|
9317
|
-
if (gotState !== state || !gotCode) {
|
|
9318
|
-
res2.writeHead(400, { "Content-Type": "text/html" });
|
|
9319
|
-
res2.end(htmlPage2("Slack", "<h1>Invalid callback</h1><p>State or code missing.</p>"));
|
|
9320
|
-
finish(new Error("Slack OAuth callback was invalid"));
|
|
9321
|
-
return;
|
|
9322
|
-
}
|
|
9323
|
-
res2.writeHead(200, { "Content-Type": "text/html" });
|
|
9324
|
-
res2.end(
|
|
9325
|
-
htmlPage2(
|
|
9326
|
-
"Slack connected",
|
|
9327
|
-
"<h1>Slack workspace connected</h1><p>You can close this tab and return to Sideboard.</p>"
|
|
9328
|
-
)
|
|
9329
|
-
);
|
|
9330
|
-
finish(void 0, gotCode);
|
|
9331
|
-
} catch (e) {
|
|
9332
|
-
finish(e instanceof Error ? e : new Error(String(e)));
|
|
9333
|
-
}
|
|
9334
|
-
});
|
|
9335
|
-
const timer = setTimeout(() => {
|
|
9336
|
-
finish(new Error("Slack sign-in timed out \u2014 try again"));
|
|
9337
|
-
}, timeoutMs);
|
|
9338
|
-
const onAbort = () => finish(new SlackOAuthCancelledError());
|
|
9339
|
-
const cleanup = () => {
|
|
9340
|
-
clearTimeout(timer);
|
|
9341
|
-
opts?.signal?.removeEventListener("abort", onAbort);
|
|
9342
|
-
server.close();
|
|
9343
|
-
};
|
|
9344
|
-
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) {
|
|
9345
9751
|
if (opts?.signal?.aborted) {
|
|
9346
|
-
|
|
9347
|
-
return;
|
|
9752
|
+
throw new SlackOAuthCancelledError();
|
|
9348
9753
|
}
|
|
9349
|
-
|
|
9350
|
-
|
|
9351
|
-
|
|
9352
|
-
|
|
9353
|
-
|
|
9354
|
-
);
|
|
9355
|
-
|
|
9356
|
-
|
|
9357
|
-
|
|
9358
|
-
|
|
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() || ""
|
|
9359
9777
|
});
|
|
9360
|
-
}
|
|
9361
|
-
|
|
9362
|
-
|
|
9363
|
-
|
|
9364
|
-
|
|
9365
|
-
|
|
9366
|
-
|
|
9367
|
-
|
|
9368
|
-
|
|
9369
|
-
method: "POST",
|
|
9370
|
-
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
9371
|
-
body
|
|
9372
|
-
});
|
|
9373
|
-
const data = await res.json();
|
|
9374
|
-
if (!data.ok) {
|
|
9375
|
-
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);
|
|
9376
9787
|
}
|
|
9377
|
-
|
|
9378
|
-
if (!teamId) throw new Error("Slack OAuth did not return a team id");
|
|
9379
|
-
return upsertSlackWorkspace({
|
|
9380
|
-
team_id: teamId,
|
|
9381
|
-
team_name: data.team?.name?.trim() || teamId,
|
|
9382
|
-
user_id: data.authed_user?.id,
|
|
9383
|
-
bot_token: data.access_token,
|
|
9384
|
-
user_token: data.authed_user?.access_token,
|
|
9385
|
-
scopes: [data.scope, data.authed_user?.scope].filter(Boolean).join(","),
|
|
9386
|
-
connected_at: (/* @__PURE__ */ new Date()).toISOString()
|
|
9387
|
-
});
|
|
9788
|
+
throw new Error("Slack sign-in timed out \u2014 try again");
|
|
9388
9789
|
}
|
|
9389
9790
|
|
|
9390
9791
|
// src/slack/socket-mode.ts
|
|
9391
9792
|
import { WebSocket as WsWebSocket } from "ws";
|
|
9392
9793
|
var BACKOFF_START_MS = 1e3;
|
|
9393
9794
|
var BACKOFF_MAX_MS = 3e4;
|
|
9394
|
-
function stripSlackMentions(
|
|
9395
|
-
return
|
|
9795
|
+
function stripSlackMentions(text3) {
|
|
9796
|
+
return text3.replace(/<@[^>]+>/g, "").replace(/\s+/g, " ").trim();
|
|
9396
9797
|
}
|
|
9397
|
-
function isSlackStopCommand(
|
|
9398
|
-
const t = stripSlackMentions(
|
|
9798
|
+
function isSlackStopCommand(text3) {
|
|
9799
|
+
const t = stripSlackMentions(text3).toLowerCase();
|
|
9399
9800
|
return t === "stop" || t === "sideboard_force_stop";
|
|
9400
9801
|
}
|
|
9401
9802
|
function parseSlackSocketFrame(raw) {
|
|
@@ -9417,8 +9818,8 @@ function inboundFromSocketFrame(frame) {
|
|
|
9417
9818
|
const ts = event.ts?.trim();
|
|
9418
9819
|
if (!channelId || !ts) return null;
|
|
9419
9820
|
const rawText = event.text?.trim() ?? "";
|
|
9420
|
-
const
|
|
9421
|
-
if (!
|
|
9821
|
+
const text3 = stripSlackMentions(rawText);
|
|
9822
|
+
if (!text3) return null;
|
|
9422
9823
|
const teamId = (frame.payload?.team_id || event.team || "").trim();
|
|
9423
9824
|
if (!teamId) return null;
|
|
9424
9825
|
const isDm = event.type === "message" && (event.channel_type === "im" || event.channel_type === "mpim" || !event.channel_type && channelId.startsWith("D"));
|
|
@@ -9430,7 +9831,7 @@ function inboundFromSocketFrame(frame) {
|
|
|
9430
9831
|
ts,
|
|
9431
9832
|
threadTs: event.thread_ts?.trim() || void 0,
|
|
9432
9833
|
userId: event.user?.trim(),
|
|
9433
|
-
text:
|
|
9834
|
+
text: text3,
|
|
9434
9835
|
kind: isMention ? "mention" : "dm"
|
|
9435
9836
|
};
|
|
9436
9837
|
}
|
|
@@ -9681,7 +10082,40 @@ function parseSlackRelayServerMessage(raw) {
|
|
|
9681
10082
|
}
|
|
9682
10083
|
}
|
|
9683
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
|
+
|
|
9684
10117
|
// src/slack/relay-client.ts
|
|
10118
|
+
import { WebSocket as WsWebSocket2 } from "ws";
|
|
9685
10119
|
var BACKOFF_START_MS2 = 1e3;
|
|
9686
10120
|
var BACKOFF_MAX_MS2 = 3e4;
|
|
9687
10121
|
function wait2(ms, signal) {
|
|
@@ -9706,7 +10140,11 @@ function send(ws, msg) {
|
|
|
9706
10140
|
}
|
|
9707
10141
|
async function runSlackRelayClient(opts) {
|
|
9708
10142
|
const log = opts.onLog ?? (() => void 0);
|
|
9709
|
-
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
|
+
};
|
|
9710
10148
|
const url = opts.url.trim();
|
|
9711
10149
|
const deviceId = opts.deviceId.trim();
|
|
9712
10150
|
if (!url) throw new Error("Slack relay URL is empty");
|
|
@@ -9869,12 +10307,12 @@ function formatSlackInboundPrompt(msg) {
|
|
|
9869
10307
|
|
|
9870
10308
|
${msg.text}`;
|
|
9871
10309
|
}
|
|
9872
|
-
function isSlackInboundUserPrompt(
|
|
9873
|
-
return
|
|
10310
|
+
function isSlackInboundUserPrompt(text3) {
|
|
10311
|
+
return text3.startsWith("Slack DM\n") || text3.startsWith("Slack @mention\n");
|
|
9874
10312
|
}
|
|
9875
|
-
function formatSlackSignedReply(deviceLabel,
|
|
10313
|
+
function formatSlackSignedReply(deviceLabel, text3) {
|
|
9876
10314
|
const label = deviceLabel.trim();
|
|
9877
|
-
const body =
|
|
10315
|
+
const body = text3.trim();
|
|
9878
10316
|
if (!body) return body;
|
|
9879
10317
|
if (!label) return body;
|
|
9880
10318
|
const head = `${label}:`;
|
|
@@ -9883,8 +10321,8 @@ function formatSlackSignedReply(deviceLabel, text2) {
|
|
|
9883
10321
|
}
|
|
9884
10322
|
return `${label}: ${body}`;
|
|
9885
10323
|
}
|
|
9886
|
-
function signForThisMac(
|
|
9887
|
-
return formatSlackSignedReply(ensureSlackDeviceIdentity().deviceLabel,
|
|
10324
|
+
function signForThisMac(text3) {
|
|
10325
|
+
return formatSlackSignedReply(ensureSlackDeviceIdentity().deviceLabel, text3);
|
|
9888
10326
|
}
|
|
9889
10327
|
function slackReplyThreadTs(msg) {
|
|
9890
10328
|
if (msg.threadTs && msg.threadTs !== msg.ts) return msg.threadTs;
|
|
@@ -9927,7 +10365,7 @@ async function ackSlackInboundSeen(msg, opts) {
|
|
|
9927
10365
|
log(`react error: ${errMsg}${hint}`);
|
|
9928
10366
|
}
|
|
9929
10367
|
}
|
|
9930
|
-
async function postSlackText(target,
|
|
10368
|
+
async function postSlackText(target, text3, opts) {
|
|
9931
10369
|
const stub = {
|
|
9932
10370
|
teamId: target.teamId,
|
|
9933
10371
|
channelId: target.channelId,
|
|
@@ -9937,7 +10375,7 @@ async function postSlackText(target, text2, opts) {
|
|
|
9937
10375
|
kind: "dm"
|
|
9938
10376
|
};
|
|
9939
10377
|
if (opts.postReply) {
|
|
9940
|
-
await opts.postReply(stub,
|
|
10378
|
+
await opts.postReply(stub, text3);
|
|
9941
10379
|
return;
|
|
9942
10380
|
}
|
|
9943
10381
|
const token = writeTokenForTeam(target.teamId);
|
|
@@ -9946,29 +10384,29 @@ async function postSlackText(target, text2, opts) {
|
|
|
9946
10384
|
"chat.postMessage",
|
|
9947
10385
|
{
|
|
9948
10386
|
channel: target.channelId,
|
|
9949
|
-
text:
|
|
10387
|
+
text: text3,
|
|
9950
10388
|
thread_ts: target.threadTs
|
|
9951
10389
|
},
|
|
9952
10390
|
opts.fetchImpl
|
|
9953
10391
|
);
|
|
9954
10392
|
}
|
|
9955
|
-
async function postSlackReply(msg,
|
|
10393
|
+
async function postSlackReply(msg, text3, opts) {
|
|
9956
10394
|
await postSlackText(
|
|
9957
10395
|
{
|
|
9958
10396
|
teamId: msg.teamId,
|
|
9959
10397
|
channelId: msg.channelId,
|
|
9960
10398
|
threadTs: slackReplyThreadTs(msg)
|
|
9961
10399
|
},
|
|
9962
|
-
signForThisMac(
|
|
10400
|
+
signForThisMac(text3),
|
|
9963
10401
|
opts
|
|
9964
10402
|
);
|
|
9965
10403
|
}
|
|
9966
10404
|
var lastRelayed = /* @__PURE__ */ new Map();
|
|
9967
|
-
function markRelayed(threadId,
|
|
9968
|
-
lastRelayed.set(threadId, `${threadId}:${
|
|
10405
|
+
function markRelayed(threadId, text3) {
|
|
10406
|
+
lastRelayed.set(threadId, `${threadId}:${text3}`);
|
|
9969
10407
|
}
|
|
9970
|
-
function alreadyRelayed(threadId,
|
|
9971
|
-
return lastRelayed.get(threadId) === `${threadId}:${
|
|
10408
|
+
function alreadyRelayed(threadId, text3) {
|
|
10409
|
+
return lastRelayed.get(threadId) === `${threadId}:${text3}`;
|
|
9972
10410
|
}
|
|
9973
10411
|
async function relayCoordinatorReplyToSlack(threadId, opts) {
|
|
9974
10412
|
const target = getSlackReplyTarget(threadId);
|
|
@@ -9977,14 +10415,14 @@ async function relayCoordinatorReplyToSlack(threadId, opts) {
|
|
|
9977
10415
|
const lastUser = thread ? [...thread.messages].reverse().find((m) => m.role === "user") : void 0;
|
|
9978
10416
|
if (lastUser && isSlackInboundUserPrompt(lastUser.text)) return;
|
|
9979
10417
|
const result = getOrchestrator().getTurnResult(threadId);
|
|
9980
|
-
const
|
|
9981
|
-
if (!
|
|
9982
|
-
if (alreadyRelayed(threadId,
|
|
9983
|
-
markRelayed(threadId,
|
|
10418
|
+
const text3 = result.text.trim();
|
|
10419
|
+
if (!text3) return;
|
|
10420
|
+
if (alreadyRelayed(threadId, text3)) return;
|
|
10421
|
+
markRelayed(threadId, text3);
|
|
9984
10422
|
const log = opts.onLog ?? (() => void 0);
|
|
9985
10423
|
try {
|
|
9986
|
-
await postSlackText(target, signForThisMac(
|
|
9987
|
-
log(`replied ${target.threadTs ?? target.channelId} (${
|
|
10424
|
+
await postSlackText(target, signForThisMac(text3), opts);
|
|
10425
|
+
log(`replied ${target.threadTs ?? target.channelId} (${text3.length} chars)`);
|
|
9988
10426
|
} catch (err) {
|
|
9989
10427
|
lastRelayed.delete(threadId);
|
|
9990
10428
|
const errMsg = err instanceof Error ? err.message : String(err);
|
|
@@ -10148,10 +10586,10 @@ function resolveSlackListenMode(opts) {
|
|
|
10148
10586
|
}
|
|
10149
10587
|
|
|
10150
10588
|
// src/slack/relay-hub.ts
|
|
10151
|
-
function parseSlackDeviceDestination(
|
|
10152
|
-
const m =
|
|
10153
|
-
if (!m) return { label: null, rest:
|
|
10154
|
-
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) };
|
|
10155
10593
|
}
|
|
10156
10594
|
var SlackRelayHub = class {
|
|
10157
10595
|
sessions = /* @__PURE__ */ new Map();
|
|
@@ -10369,40 +10807,126 @@ var SlackRelayHub = class {
|
|
|
10369
10807
|
};
|
|
10370
10808
|
|
|
10371
10809
|
// src/slack/relay-server.ts
|
|
10372
|
-
import { createServer as
|
|
10810
|
+
import { createServer as createServer3 } from "http";
|
|
10373
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
|
+
}
|
|
10374
10820
|
async function startSlackRelayServer(opts) {
|
|
10375
10821
|
const log = opts.onLog ?? console.log;
|
|
10376
10822
|
const appToken = opts.appToken.trim();
|
|
10377
10823
|
if (!appToken.startsWith("xapp-")) {
|
|
10378
10824
|
throw new Error("SIDEBOARD_SLACK_APP_TOKEN must be an xapp-\u2026 app-level token");
|
|
10379
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();
|
|
10380
10830
|
const hub = opts.hub ?? new SlackRelayHub({
|
|
10381
10831
|
fetchImpl: opts.fetchImpl,
|
|
10382
10832
|
onLog: log
|
|
10383
10833
|
});
|
|
10384
|
-
const
|
|
10385
|
-
const
|
|
10386
|
-
if (
|
|
10387
|
-
|
|
10388
|
-
|
|
10389
|
-
|
|
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;
|
|
10390
10849
|
}
|
|
10391
|
-
if (
|
|
10392
|
-
res
|
|
10393
|
-
|
|
10394
|
-
|
|
10395
|
-
|
|
10396
|
-
|
|
10397
|
-
|
|
10398
|
-
|
|
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>"
|
|
10399
10864
|
);
|
|
10400
|
-
return;
|
|
10865
|
+
return true;
|
|
10401
10866
|
}
|
|
10402
|
-
|
|
10403
|
-
|
|
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;
|
|
10901
|
+
}
|
|
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
|
+
});
|
|
10404
10928
|
});
|
|
10405
|
-
const wss = new WebSocketServer({ server: httpServer, path:
|
|
10929
|
+
const wss = new WebSocketServer({ server: httpServer, path: SLACK_RELAY_DESKTOP_PATH });
|
|
10406
10930
|
wss.on("connection", (ws, _req) => {
|
|
10407
10931
|
const socket = {
|
|
10408
10932
|
send: (data) => {
|
|
@@ -10440,12 +10964,12 @@ async function startSlackRelayServer(opts) {
|
|
|
10440
10964
|
});
|
|
10441
10965
|
const address = httpServer.address();
|
|
10442
10966
|
const boundPort = typeof address === "object" && address ? address.port : typeof port === "number" ? port : 0;
|
|
10443
|
-
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}`;
|
|
10444
10968
|
log(`relay listening on ${url}`);
|
|
10445
10969
|
const ac = new AbortController();
|
|
10446
10970
|
const onAbort = () => ac.abort();
|
|
10447
10971
|
opts.signal?.addEventListener("abort", onAbort, { once: true });
|
|
10448
|
-
const socketModeDone = runSlackSocketMode({
|
|
10972
|
+
const socketModeDone = opts.skipSocketMode ? Promise.resolve() : runSlackSocketMode({
|
|
10449
10973
|
appToken,
|
|
10450
10974
|
signal: ac.signal,
|
|
10451
10975
|
fetchImpl: opts.fetchImpl,
|
|
@@ -10522,8 +11046,6 @@ export {
|
|
|
10522
11046
|
SLACK_LISTEN_STOPPED_REPLY,
|
|
10523
11047
|
SLACK_LISTEN_TIMEOUT_REPLY,
|
|
10524
11048
|
SLACK_OAUTH_CANCELLED,
|
|
10525
|
-
SLACK_OAUTH_LOCAL_CALLBACK,
|
|
10526
|
-
SLACK_OAUTH_PORT,
|
|
10527
11049
|
SLACK_OAUTH_REDIRECT,
|
|
10528
11050
|
SLACK_REPLY_FORMATTING,
|
|
10529
11051
|
SLACK_SEEN_REACTION,
|
|
@@ -10590,6 +11112,7 @@ export {
|
|
|
10590
11112
|
codexAdapter,
|
|
10591
11113
|
coerceOrchestratorAgent,
|
|
10592
11114
|
collectTakenTeamSlugs,
|
|
11115
|
+
commentLinearIssue,
|
|
10593
11116
|
commitAll,
|
|
10594
11117
|
conductorBundledBinDir,
|
|
10595
11118
|
conductorDbPath,
|
|
@@ -10605,6 +11128,7 @@ export {
|
|
|
10605
11128
|
createEmptyThread,
|
|
10606
11129
|
createExistingBranchWorktree,
|
|
10607
11130
|
createGlobalChat,
|
|
11131
|
+
createLinearIssue,
|
|
10608
11132
|
createLinearPkce,
|
|
10609
11133
|
createOrUpdatePr,
|
|
10610
11134
|
createPrStack,
|
|
@@ -10657,6 +11181,7 @@ export {
|
|
|
10657
11181
|
formatAgentInstructions,
|
|
10658
11182
|
formatArtifactDirective,
|
|
10659
11183
|
formatBrightsyFetchError,
|
|
11184
|
+
formatFetchError,
|
|
10660
11185
|
formatGhLandError,
|
|
10661
11186
|
formatIpcInvokeError,
|
|
10662
11187
|
formatMessagesAsTranscript,
|
|
@@ -10687,6 +11212,7 @@ export {
|
|
|
10687
11212
|
getIssueSource,
|
|
10688
11213
|
getLinearApiKey,
|
|
10689
11214
|
getLinearAuthToken,
|
|
11215
|
+
getLinearIssue,
|
|
10690
11216
|
getOrchestrator,
|
|
10691
11217
|
getPr,
|
|
10692
11218
|
getPrChecks,
|
|
@@ -10711,6 +11237,7 @@ export {
|
|
|
10711
11237
|
hasRepoHook,
|
|
10712
11238
|
hasWorkspaceHook,
|
|
10713
11239
|
healOrchestrationSoccerTitles,
|
|
11240
|
+
httpFetch,
|
|
10714
11241
|
importConductorWorkspace,
|
|
10715
11242
|
importConductorWorkspaceAsync,
|
|
10716
11243
|
initPrStack,
|
|
@@ -10744,8 +11271,10 @@ export {
|
|
|
10744
11271
|
isSlackExternalReplyPrompt,
|
|
10745
11272
|
isSlackOAuthCancelled,
|
|
10746
11273
|
isThinkingEffort,
|
|
11274
|
+
isThreadCaffeinated,
|
|
10747
11275
|
isWorkspaceScratchPath,
|
|
10748
11276
|
linearAuthorizationHeader,
|
|
11277
|
+
linearGraphql,
|
|
10749
11278
|
linearOAuthAuthorizeUrl,
|
|
10750
11279
|
linearOAuthCredentials,
|
|
10751
11280
|
listAgentSetupInfo,
|
|
@@ -10762,6 +11291,7 @@ export {
|
|
|
10762
11291
|
listIssues,
|
|
10763
11292
|
listLinearIssues,
|
|
10764
11293
|
listLinearIssuesDirect,
|
|
11294
|
+
listLinearTeams,
|
|
10765
11295
|
listModelsForAgent,
|
|
10766
11296
|
listOpencodeModels,
|
|
10767
11297
|
listPrs,
|
|
@@ -10837,6 +11367,7 @@ export {
|
|
|
10837
11367
|
recordSlackOutboundWatch,
|
|
10838
11368
|
refreshGitHubAuth,
|
|
10839
11369
|
refreshSlackReplyBadges,
|
|
11370
|
+
releaseCaffeinateHoldForThread,
|
|
10840
11371
|
removeWorkspace,
|
|
10841
11372
|
removeWorktree,
|
|
10842
11373
|
repoSlug,
|
|
@@ -10855,6 +11386,8 @@ export {
|
|
|
10855
11386
|
resolveFilesToCopy,
|
|
10856
11387
|
resolveGhAuthToken,
|
|
10857
11388
|
resolveGithubRepoSlug,
|
|
11389
|
+
resolveLinearState,
|
|
11390
|
+
resolveLinearTeam,
|
|
10858
11391
|
resolveLoginCommand,
|
|
10859
11392
|
resolveNewThreadOptions,
|
|
10860
11393
|
resolvePlanMarkdown,
|
|
@@ -10867,6 +11400,7 @@ export {
|
|
|
10867
11400
|
resolveThreadEffort,
|
|
10868
11401
|
resolveVaultKey,
|
|
10869
11402
|
resolveWorktreeStartPoint,
|
|
11403
|
+
rewriteLinearError,
|
|
10870
11404
|
run,
|
|
10871
11405
|
runArchiveScript,
|
|
10872
11406
|
runCloudConnect,
|
|
@@ -10880,6 +11414,7 @@ export {
|
|
|
10880
11414
|
saveLinearOAuth,
|
|
10881
11415
|
secureFileUnlocksWith,
|
|
10882
11416
|
setCaffeinateHold,
|
|
11417
|
+
setHttpFetchImpl,
|
|
10883
11418
|
setStatus,
|
|
10884
11419
|
setVaultMasterKey,
|
|
10885
11420
|
settingsSourceLabel,
|
|
@@ -10897,6 +11432,7 @@ export {
|
|
|
10897
11432
|
slackCoordinatorSourceRef,
|
|
10898
11433
|
slackListenEnabled,
|
|
10899
11434
|
slackOAuthCredentials,
|
|
11435
|
+
slackOAuthResultUrl,
|
|
10900
11436
|
slackRelayUrl,
|
|
10901
11437
|
slugify,
|
|
10902
11438
|
spawnAgentTurn,
|
|
@@ -10942,6 +11478,7 @@ export {
|
|
|
10942
11478
|
updateCodexSettings,
|
|
10943
11479
|
updateDefaultsSettings,
|
|
10944
11480
|
updateIntegrationsSettings,
|
|
11481
|
+
updateLinearIssue,
|
|
10945
11482
|
updateOpencodeSettings,
|
|
10946
11483
|
updateThread,
|
|
10947
11484
|
validateLinearApiKey,
|