@sideboard-ai/core 0.1.68 → 0.1.71
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{agents-PE3RHQS5.js → agents-GB7XL2LI.js} +6 -5
- package/dist/{agents-JBD7IAGK.js → agents-TCAK3MXN.js} +6 -5
- package/dist/{app-settings-ONKMFSEJ.js → app-settings-3TLA2EJ7.js} +16 -1
- package/dist/{app-settings-GJVZGU3M.js → app-settings-P42Z3VOB.js} +16 -1
- package/dist/caffeinate-hold-BEJIYPJ7.js +107 -0
- package/dist/caffeinate-hold-KLC6SABD.js +14 -0
- package/dist/{chunk-DPDGMMGI.js → chunk-3CY6WJ7O.js} +2 -2
- package/dist/{chunk-ROXEENBT.js → chunk-57ROTAFD.js} +62 -3
- package/dist/{chunk-N5LMEP65.js → chunk-7NCIHRAU.js} +22 -4
- package/dist/{chunk-OICHV4DH.js → chunk-DGPUAZA4.js} +62 -3
- package/dist/{chunk-VX6ACNXS.js → chunk-H2IXNFQ2.js} +22 -4
- package/dist/chunk-JAWEDVVA.js +106 -0
- package/dist/chunk-JT7R45JB.js +20 -0
- package/dist/{chunk-EEBDVLDK.js → chunk-MF2YMEGD.js} +61 -9
- package/dist/{chunk-47RBLBRK.js → chunk-NG7S3OPX.js} +10 -4
- package/dist/chunk-NSTQ6QKD.js +23 -0
- package/dist/{chunk-REKGFVUX.js → chunk-OE7YBB5X.js} +61 -9
- package/dist/{chunk-BPOTVI5J.js → chunk-PTJJIQK7.js} +472 -18
- package/dist/{chunk-6Q7H6SMX.js → chunk-S25IHL5H.js} +10 -4
- package/dist/{chunk-QQC2D4MY.js → chunk-UH57WVFP.js} +2 -2
- package/dist/{chunk-MHR4OGZ4.js → chunk-W7YOTDVO.js} +493 -18
- package/dist/{coordinator-prompt-YZYZ5P6P.js → coordinator-prompt-2J4PIMUF.js} +6 -3
- package/dist/{coordinator-prompt-HD6IMCKS.js → coordinator-prompt-QYYG2IBB.js} +6 -3
- package/dist/{global-workspace-Z6GF7D2K.js → global-workspace-KHIFQUUM.js} +11 -4
- package/dist/{global-workspace-7P5NG3JZ.js → global-workspace-SVRYNJZA.js} +11 -4
- package/dist/index.cjs +4184 -850
- package/dist/index.d.cts +543 -21
- package/dist/index.d.ts +543 -21
- package/dist/index.js +3285 -748
- package/dist/mcp/run-stdio.cjs +1995 -482
- package/dist/mcp/run-stdio.js +943 -151
- package/dist/{workspaces-BWJNGEE6.js → workspaces-AHFTHVBT.js} +6 -5
- package/dist/{workspaces-O6EZGKQK.js → workspaces-SVQIEVIZ.js} +6 -5
- package/dist/{worktree-VDXLUOWR.js → worktree-KWXHMKRN.js} +1 -1
- package/dist/{worktree-ZPSKEZFE.js → worktree-VOCH3WS3.js} +1 -1
- package/package.json +3 -1
package/dist/index.js
CHANGED
|
@@ -4,7 +4,7 @@ import {
|
|
|
4
4
|
listWorkspaces,
|
|
5
5
|
removeWorkspace,
|
|
6
6
|
syncWorkspacesFromThreads
|
|
7
|
-
} from "./chunk-
|
|
7
|
+
} from "./chunk-3CY6WJ7O.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-S25IHL5H.js";
|
|
62
62
|
import {
|
|
63
63
|
CLOUD_COORDINATOR_BUSY_REPLY,
|
|
64
64
|
CLOUD_COORDINATOR_STOPPED_REPLY,
|
|
@@ -71,18 +71,30 @@ import {
|
|
|
71
71
|
coerceOrchestratorAgent,
|
|
72
72
|
createGlobalChat,
|
|
73
73
|
ensureCloudCoordinator,
|
|
74
|
+
ensureSlackCoordinator,
|
|
74
75
|
healOrchestrationSoccerTitles,
|
|
75
76
|
isCloudCoordinatorThread,
|
|
76
77
|
isGlobalRepoPath,
|
|
77
78
|
isGlobalThread,
|
|
78
79
|
isOrchestratorCapableAgent,
|
|
79
80
|
isOrchestratorThread,
|
|
81
|
+
isSlackCoordinatorThread,
|
|
80
82
|
listGlobalThreads,
|
|
81
83
|
orchestrationTitleNeedsSoccerNickname,
|
|
82
84
|
orchestratorSessionPoisonedByBuiltins,
|
|
83
85
|
parseForceStopMessage,
|
|
86
|
+
slackCoordinatorSourceRef,
|
|
84
87
|
takenTeamSlugsForOrchestration
|
|
85
|
-
} from "./chunk-
|
|
88
|
+
} from "./chunk-57ROTAFD.js";
|
|
89
|
+
import {
|
|
90
|
+
COORDINATOR_TOOL_PLAYBOOK,
|
|
91
|
+
SLACK_REPLY_FORMATTING,
|
|
92
|
+
coordinatorSystemPrompt,
|
|
93
|
+
coordinatorTurnReminder,
|
|
94
|
+
enrichWorkspacesWithGithub,
|
|
95
|
+
ensureGlobalCoordinatorCwd,
|
|
96
|
+
formatWorkspaceInventory
|
|
97
|
+
} from "./chunk-H2IXNFQ2.js";
|
|
86
98
|
import {
|
|
87
99
|
brightsyConfigPath,
|
|
88
100
|
brightsyMcpServerName,
|
|
@@ -117,14 +129,6 @@ import {
|
|
|
117
129
|
pushTurnStderr,
|
|
118
130
|
summarizeTurnStderr
|
|
119
131
|
} from "./chunk-3WJAUKIL.js";
|
|
120
|
-
import {
|
|
121
|
-
COORDINATOR_TOOL_PLAYBOOK,
|
|
122
|
-
coordinatorSystemPrompt,
|
|
123
|
-
coordinatorTurnReminder,
|
|
124
|
-
enrichWorkspacesWithGithub,
|
|
125
|
-
ensureGlobalCoordinatorCwd,
|
|
126
|
-
formatWorkspaceInventory
|
|
127
|
-
} from "./chunk-VX6ACNXS.js";
|
|
128
132
|
import {
|
|
129
133
|
HARNESS_ENV_KEYS,
|
|
130
134
|
appSettingsPath,
|
|
@@ -138,10 +142,13 @@ import {
|
|
|
138
142
|
brightsyInjectWorktreeMcpEnabled,
|
|
139
143
|
caffeinateWhileCloudConnectEnabled,
|
|
140
144
|
caffeinateWhileRunningEnabled,
|
|
145
|
+
caffeinateWhileSlackListenEnabled,
|
|
141
146
|
childEnvWithAppSettings,
|
|
142
147
|
claudeChromeEnabled,
|
|
143
148
|
claudeUserSettingsPath,
|
|
144
149
|
deleteBranchOnPurgeEnabled,
|
|
150
|
+
disconnectLinearConnection,
|
|
151
|
+
ensureSlackDeviceIdentity,
|
|
145
152
|
getDefaultAgent,
|
|
146
153
|
getDefaultEffort,
|
|
147
154
|
getDefaultFast,
|
|
@@ -150,16 +157,27 @@ import {
|
|
|
150
157
|
getLinearApiKey,
|
|
151
158
|
harnessEnvKey,
|
|
152
159
|
isLinearConnected,
|
|
160
|
+
isSecureFileEncrypted,
|
|
153
161
|
loadAppSettings,
|
|
154
162
|
maxConcurrentAgents,
|
|
155
163
|
orchestrationQuotaFallbackAgent,
|
|
156
164
|
orchestrationQuotaOnLimit,
|
|
165
|
+
persistVaultKeyInKeychain,
|
|
166
|
+
readKeychainVaultKey,
|
|
167
|
+
readSecureJson,
|
|
157
168
|
resolveAgentExecutable,
|
|
158
169
|
resolveClaudeExecutable,
|
|
159
170
|
resolveEffectiveIssueSource,
|
|
160
171
|
resolveNewThreadOptions,
|
|
161
172
|
resolveThreadDefaults,
|
|
173
|
+
resolveVaultKey,
|
|
162
174
|
saveAppSettings,
|
|
175
|
+
saveLinearOAuth,
|
|
176
|
+
secureFileUnlocksWith,
|
|
177
|
+
setVaultMasterKey,
|
|
178
|
+
slackAppLevelToken,
|
|
179
|
+
slackListenEnabled,
|
|
180
|
+
toPublicAppSettings,
|
|
163
181
|
updateAdvancedSettings,
|
|
164
182
|
updateAgentExecutable,
|
|
165
183
|
updateAppEnvironment,
|
|
@@ -168,8 +186,17 @@ import {
|
|
|
168
186
|
updateCodexSettings,
|
|
169
187
|
updateDefaultsSettings,
|
|
170
188
|
updateIntegrationsSettings,
|
|
171
|
-
updateOpencodeSettings
|
|
172
|
-
|
|
189
|
+
updateOpencodeSettings,
|
|
190
|
+
writeSecureJson
|
|
191
|
+
} from "./chunk-W7YOTDVO.js";
|
|
192
|
+
import {
|
|
193
|
+
caffeinateHoldPath,
|
|
194
|
+
getCaffeinateHold,
|
|
195
|
+
setCaffeinateHold
|
|
196
|
+
} from "./chunk-JAWEDVVA.js";
|
|
197
|
+
import {
|
|
198
|
+
writePrivateFile
|
|
199
|
+
} from "./chunk-JT7R45JB.js";
|
|
173
200
|
import {
|
|
174
201
|
FAMOUS_SOCCER_TEAMS,
|
|
175
202
|
addPrStackLayer,
|
|
@@ -232,7 +259,7 @@ import {
|
|
|
232
259
|
worktreeDisplayLabel,
|
|
233
260
|
worktreeDisplayLabelForGroup,
|
|
234
261
|
worktreeNameFromPath
|
|
235
|
-
} from "./chunk-
|
|
262
|
+
} from "./chunk-MF2YMEGD.js";
|
|
236
263
|
import {
|
|
237
264
|
ATTACHMENTS_DIR,
|
|
238
265
|
LEGACY_ATTACHMENTS_DIR,
|
|
@@ -322,9 +349,9 @@ async function getGitHubStatus() {
|
|
|
322
349
|
};
|
|
323
350
|
}
|
|
324
351
|
const status = await run("gh", ["auth", "status"], { reject: false });
|
|
325
|
-
const
|
|
352
|
+
const text2 = `${status.stdout}
|
|
326
353
|
${status.stderr}`;
|
|
327
|
-
const loginMatch =
|
|
354
|
+
const loginMatch = text2.match(/Logged in to ([^\s]+) account (\S+)/i) ?? text2.match(/Logged in to ([^\s]+) as (\S+)/i);
|
|
328
355
|
if (loginMatch) {
|
|
329
356
|
return {
|
|
330
357
|
connected: true,
|
|
@@ -344,8 +371,255 @@ async function refreshGitHubAuth() {
|
|
|
344
371
|
return getGitHubStatus();
|
|
345
372
|
}
|
|
346
373
|
|
|
347
|
-
// src/integrations/linear.ts
|
|
374
|
+
// src/integrations/linear-oauth.ts
|
|
375
|
+
import { createHash, randomBytes } from "crypto";
|
|
376
|
+
import { createServer } from "http";
|
|
377
|
+
|
|
378
|
+
// src/integrations/linear-app.ts
|
|
379
|
+
var BAKED_LINEAR_CLIENT_ID = "39a15abc7ea5bea17c47f47f85ff5fd0";
|
|
380
|
+
var BAKED_LINEAR_CLIENT_SECRET = "";
|
|
381
|
+
function hasBakedLinearOAuth() {
|
|
382
|
+
return Boolean(BAKED_LINEAR_CLIENT_ID.trim());
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
// src/integrations/linear-oauth.ts
|
|
386
|
+
var LINEAR_OAUTH_PORT = 19848;
|
|
387
|
+
var LINEAR_OAUTH_REDIRECT = `http://127.0.0.1:${LINEAR_OAUTH_PORT}/callback`;
|
|
388
|
+
var LINEAR_AUTHORIZE = "https://linear.app/oauth/authorize";
|
|
389
|
+
var LINEAR_TOKEN = "https://api.linear.app/oauth/token";
|
|
390
|
+
var LINEAR_REVOKE = "https://api.linear.app/oauth/revoke";
|
|
348
391
|
var LINEAR_GRAPHQL = "https://api.linear.app/graphql";
|
|
392
|
+
var LINEAR_OAUTH_SCOPES = "read";
|
|
393
|
+
var REFRESH_SKEW_MS = 5 * 6e4;
|
|
394
|
+
function linearOAuthCredentials() {
|
|
395
|
+
const settings = loadAppSettings();
|
|
396
|
+
const clientId = process.env.SIDEBOARD_LINEAR_CLIENT_ID?.trim() || settings.integrations.linearClientId?.trim() || BAKED_LINEAR_CLIENT_ID.trim();
|
|
397
|
+
const clientSecret = process.env.SIDEBOARD_LINEAR_CLIENT_SECRET?.trim() || settings.integrations.linearClientSecret?.trim() || BAKED_LINEAR_CLIENT_SECRET.trim();
|
|
398
|
+
if (!clientId) {
|
|
399
|
+
throw new Error(
|
|
400
|
+
`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.`
|
|
401
|
+
);
|
|
402
|
+
}
|
|
403
|
+
return { clientId, clientSecret };
|
|
404
|
+
}
|
|
405
|
+
function createLinearPkce() {
|
|
406
|
+
const verifier = randomBytes(32).toString("base64url");
|
|
407
|
+
const challenge = createHash("sha256").update(verifier).digest("base64url");
|
|
408
|
+
return { verifier, challenge };
|
|
409
|
+
}
|
|
410
|
+
function linearOAuthAuthorizeUrl(clientId, state, codeChallenge) {
|
|
411
|
+
const params = new URLSearchParams({
|
|
412
|
+
client_id: clientId,
|
|
413
|
+
redirect_uri: LINEAR_OAUTH_REDIRECT,
|
|
414
|
+
response_type: "code",
|
|
415
|
+
scope: LINEAR_OAUTH_SCOPES,
|
|
416
|
+
state,
|
|
417
|
+
prompt: "consent",
|
|
418
|
+
actor: "user",
|
|
419
|
+
code_challenge: codeChallenge,
|
|
420
|
+
code_challenge_method: "S256"
|
|
421
|
+
});
|
|
422
|
+
return `${LINEAR_AUTHORIZE}?${params.toString()}`;
|
|
423
|
+
}
|
|
424
|
+
function linearAuthorizationHeader(token) {
|
|
425
|
+
const t = token.trim();
|
|
426
|
+
if (!t) return t;
|
|
427
|
+
if (/^bearer\s+/i.test(t)) return t;
|
|
428
|
+
if (t.startsWith("lin_api_")) return t;
|
|
429
|
+
return `Bearer ${t}`;
|
|
430
|
+
}
|
|
431
|
+
function htmlPage(title, body) {
|
|
432
|
+
return `<!doctype html><html><head><meta charset="utf-8"><title>${title}</title>
|
|
433
|
+
<style>body{font-family:ui-sans-serif,system-ui,sans-serif;padding:48px 24px;max-width:36rem;margin:0 auto;color:#1a1a1a}
|
|
434
|
+
h1{font-size:1.25rem}p{line-height:1.5;color:#444}</style></head><body>${body}</body></html>`;
|
|
435
|
+
}
|
|
436
|
+
async function exchangeLinearToken(body) {
|
|
437
|
+
const res = await fetch(LINEAR_TOKEN, {
|
|
438
|
+
method: "POST",
|
|
439
|
+
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
440
|
+
body
|
|
441
|
+
});
|
|
442
|
+
const data = await res.json().catch(() => ({}));
|
|
443
|
+
if (!res.ok || data.error || !data.access_token?.trim()) {
|
|
444
|
+
const detail = data.error_description || data.error || `HTTP ${res.status}`;
|
|
445
|
+
throw new Error(`Linear OAuth token exchange failed: ${detail}`);
|
|
446
|
+
}
|
|
447
|
+
return data;
|
|
448
|
+
}
|
|
449
|
+
async function persistLinearTokens(data, viewer) {
|
|
450
|
+
return saveLinearOAuth({
|
|
451
|
+
accessToken: data.access_token.trim(),
|
|
452
|
+
refreshToken: data.refresh_token?.trim(),
|
|
453
|
+
expiresIn: data.expires_in,
|
|
454
|
+
viewerName: viewer?.name,
|
|
455
|
+
organizationName: viewer?.organizationName
|
|
456
|
+
});
|
|
457
|
+
}
|
|
458
|
+
async function fetchLinearViewer(accessToken) {
|
|
459
|
+
const res = await fetch(LINEAR_GRAPHQL, {
|
|
460
|
+
method: "POST",
|
|
461
|
+
headers: {
|
|
462
|
+
"Content-Type": "application/json",
|
|
463
|
+
Authorization: linearAuthorizationHeader(accessToken)
|
|
464
|
+
},
|
|
465
|
+
body: JSON.stringify({
|
|
466
|
+
query: "{ viewer { name organization { name } } }"
|
|
467
|
+
})
|
|
468
|
+
});
|
|
469
|
+
if (!res.ok) return {};
|
|
470
|
+
const json = await res.json().catch(() => ({}));
|
|
471
|
+
const viewer = json.data?.viewer;
|
|
472
|
+
return {
|
|
473
|
+
name: viewer?.name?.trim() || void 0,
|
|
474
|
+
organizationName: viewer?.organization?.name?.trim() || void 0
|
|
475
|
+
};
|
|
476
|
+
}
|
|
477
|
+
var refreshInFlight = null;
|
|
478
|
+
async function refreshLinearAccessToken(refreshToken) {
|
|
479
|
+
const { clientId, clientSecret } = linearOAuthCredentials();
|
|
480
|
+
const body = new URLSearchParams({
|
|
481
|
+
grant_type: "refresh_token",
|
|
482
|
+
refresh_token: refreshToken,
|
|
483
|
+
client_id: clientId
|
|
484
|
+
});
|
|
485
|
+
if (clientSecret) body.set("client_secret", clientSecret);
|
|
486
|
+
try {
|
|
487
|
+
const data = await exchangeLinearToken(body);
|
|
488
|
+
await persistLinearTokens(data);
|
|
489
|
+
return data.access_token.trim();
|
|
490
|
+
} catch (err) {
|
|
491
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
492
|
+
if (/invalid_grant|invalid_token|unauthorized/i.test(message)) {
|
|
493
|
+
disconnectLinearConnection();
|
|
494
|
+
throw new Error("Linear sign-in expired \u2014 connect again in Account settings");
|
|
495
|
+
}
|
|
496
|
+
throw err;
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
async function getLinearAuthToken(settings = loadAppSettings()) {
|
|
500
|
+
const access = settings.integrations.linearAccessToken?.trim() || "";
|
|
501
|
+
const refresh = settings.integrations.linearRefreshToken?.trim() || "";
|
|
502
|
+
const expiresAt = settings.integrations.linearTokenExpiresAt ?? 0;
|
|
503
|
+
const apiKey = settings.integrations.linearApiKey?.trim() || "";
|
|
504
|
+
if (access && (!refresh || Date.now() < expiresAt - REFRESH_SKEW_MS)) {
|
|
505
|
+
return access;
|
|
506
|
+
}
|
|
507
|
+
if (refresh) {
|
|
508
|
+
if (!refreshInFlight) {
|
|
509
|
+
refreshInFlight = refreshLinearAccessToken(refresh).finally(() => {
|
|
510
|
+
refreshInFlight = null;
|
|
511
|
+
});
|
|
512
|
+
}
|
|
513
|
+
return refreshInFlight.catch((err) => {
|
|
514
|
+
if (apiKey) return apiKey;
|
|
515
|
+
throw err;
|
|
516
|
+
});
|
|
517
|
+
}
|
|
518
|
+
return apiKey || null;
|
|
519
|
+
}
|
|
520
|
+
async function startLinearOAuth(opts) {
|
|
521
|
+
const { clientId, clientSecret } = linearOAuthCredentials();
|
|
522
|
+
const state = randomBytes(16).toString("hex");
|
|
523
|
+
const pkce = createLinearPkce();
|
|
524
|
+
const authorizeUrl = linearOAuthAuthorizeUrl(clientId, state, pkce.challenge);
|
|
525
|
+
const timeoutMs = opts?.timeoutMs ?? 5 * 6e4;
|
|
526
|
+
const code = await new Promise((resolve, reject) => {
|
|
527
|
+
const server = createServer((req, res) => {
|
|
528
|
+
try {
|
|
529
|
+
const url = new URL(req.url || "/", `http://127.0.0.1:${LINEAR_OAUTH_PORT}`);
|
|
530
|
+
if (url.pathname !== "/callback") {
|
|
531
|
+
res.writeHead(404);
|
|
532
|
+
res.end("Not found");
|
|
533
|
+
return;
|
|
534
|
+
}
|
|
535
|
+
const err = url.searchParams.get("error");
|
|
536
|
+
const gotState = url.searchParams.get("state");
|
|
537
|
+
const gotCode = url.searchParams.get("code");
|
|
538
|
+
if (err) {
|
|
539
|
+
res.writeHead(400, { "Content-Type": "text/html" });
|
|
540
|
+
res.end(htmlPage("Linear", `<h1>Authorization cancelled</h1><p>${err}</p>`));
|
|
541
|
+
cleanup();
|
|
542
|
+
reject(new Error(`Linear OAuth: ${err}`));
|
|
543
|
+
return;
|
|
544
|
+
}
|
|
545
|
+
if (gotState !== state || !gotCode) {
|
|
546
|
+
res.writeHead(400, { "Content-Type": "text/html" });
|
|
547
|
+
res.end(htmlPage("Linear", "<h1>Invalid callback</h1><p>State or code missing.</p>"));
|
|
548
|
+
cleanup();
|
|
549
|
+
reject(new Error("Linear OAuth callback was invalid"));
|
|
550
|
+
return;
|
|
551
|
+
}
|
|
552
|
+
res.writeHead(200, { "Content-Type": "text/html" });
|
|
553
|
+
res.end(
|
|
554
|
+
htmlPage(
|
|
555
|
+
"Linear connected",
|
|
556
|
+
"<h1>Linear connected</h1><p>You can close this tab and return to Sideboard.</p>"
|
|
557
|
+
)
|
|
558
|
+
);
|
|
559
|
+
cleanup();
|
|
560
|
+
resolve(gotCode);
|
|
561
|
+
} catch (e) {
|
|
562
|
+
cleanup();
|
|
563
|
+
reject(e instanceof Error ? e : new Error(String(e)));
|
|
564
|
+
}
|
|
565
|
+
});
|
|
566
|
+
const timer = setTimeout(() => {
|
|
567
|
+
cleanup();
|
|
568
|
+
reject(new Error("Linear sign-in timed out \u2014 try again"));
|
|
569
|
+
}, timeoutMs);
|
|
570
|
+
const cleanup = () => {
|
|
571
|
+
clearTimeout(timer);
|
|
572
|
+
server.close();
|
|
573
|
+
};
|
|
574
|
+
server.on("error", (e) => {
|
|
575
|
+
cleanup();
|
|
576
|
+
reject(
|
|
577
|
+
e instanceof Error && e.code === "EADDRINUSE" ? new Error(
|
|
578
|
+
`Port ${LINEAR_OAUTH_PORT} is in use. Close whatever is bound there, or paste a Linear API key instead.`
|
|
579
|
+
) : e
|
|
580
|
+
);
|
|
581
|
+
});
|
|
582
|
+
server.listen(LINEAR_OAUTH_PORT, "127.0.0.1", () => {
|
|
583
|
+
void Promise.resolve(opts?.openUrl?.(authorizeUrl)).catch((e) => {
|
|
584
|
+
cleanup();
|
|
585
|
+
reject(e instanceof Error ? e : new Error(String(e)));
|
|
586
|
+
});
|
|
587
|
+
});
|
|
588
|
+
});
|
|
589
|
+
const body = new URLSearchParams({
|
|
590
|
+
grant_type: "authorization_code",
|
|
591
|
+
code,
|
|
592
|
+
redirect_uri: LINEAR_OAUTH_REDIRECT,
|
|
593
|
+
client_id: clientId,
|
|
594
|
+
code_verifier: pkce.verifier
|
|
595
|
+
});
|
|
596
|
+
if (clientSecret) body.set("client_secret", clientSecret);
|
|
597
|
+
const data = await exchangeLinearToken(body);
|
|
598
|
+
const viewer = await fetchLinearViewer(data.access_token.trim()).catch(() => ({}));
|
|
599
|
+
return persistLinearTokens(data, viewer);
|
|
600
|
+
}
|
|
601
|
+
async function revokeLinearToken(token, tokenTypeHint) {
|
|
602
|
+
const body = new URLSearchParams({
|
|
603
|
+
token,
|
|
604
|
+
token_type_hint: tokenTypeHint
|
|
605
|
+
});
|
|
606
|
+
await fetch(LINEAR_REVOKE, {
|
|
607
|
+
method: "POST",
|
|
608
|
+
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
609
|
+
body
|
|
610
|
+
}).catch(() => void 0);
|
|
611
|
+
}
|
|
612
|
+
async function disconnectLinear() {
|
|
613
|
+
const settings = loadAppSettings();
|
|
614
|
+
const access = settings.integrations.linearAccessToken?.trim();
|
|
615
|
+
const refresh = settings.integrations.linearRefreshToken?.trim();
|
|
616
|
+
if (access) await revokeLinearToken(access, "access_token");
|
|
617
|
+
if (refresh) await revokeLinearToken(refresh, "refresh_token");
|
|
618
|
+
return disconnectLinearConnection();
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
// src/integrations/linear.ts
|
|
622
|
+
var LINEAR_GRAPHQL2 = "https://api.linear.app/graphql";
|
|
349
623
|
var ASSIGNED_ISSUES_QUERY = `
|
|
350
624
|
query SideboardAssignedIssues($first: Int!) {
|
|
351
625
|
viewer {
|
|
@@ -366,16 +640,16 @@ query SideboardAssignedIssues($first: Int!) {
|
|
|
366
640
|
}
|
|
367
641
|
`;
|
|
368
642
|
async function listLinearIssuesDirect(opts) {
|
|
369
|
-
const apiKey = (opts?.apiKey ??
|
|
643
|
+
const apiKey = (opts?.apiKey ?? await getLinearAuthToken())?.trim();
|
|
370
644
|
if (!apiKey) {
|
|
371
|
-
throw new Error("Linear is not connected \u2014
|
|
645
|
+
throw new Error("Linear is not connected \u2014 sign in from Account settings");
|
|
372
646
|
}
|
|
373
647
|
const first = Math.max(1, Math.min(100, opts?.limit ?? 50));
|
|
374
|
-
const res = await fetch(
|
|
648
|
+
const res = await fetch(LINEAR_GRAPHQL2, {
|
|
375
649
|
method: "POST",
|
|
376
650
|
headers: {
|
|
377
651
|
"Content-Type": "application/json",
|
|
378
|
-
Authorization: apiKey
|
|
652
|
+
Authorization: linearAuthorizationHeader(apiKey)
|
|
379
653
|
},
|
|
380
654
|
body: JSON.stringify({
|
|
381
655
|
query: ASSIGNED_ISSUES_QUERY,
|
|
@@ -545,9 +819,9 @@ function toolFilePath(input) {
|
|
|
545
819
|
if (!input) return void 0;
|
|
546
820
|
return str(input.file_path) ?? str(input.path) ?? str(input.filePath) ?? str(input.filename);
|
|
547
821
|
}
|
|
548
|
-
function countLines(
|
|
549
|
-
if (!
|
|
550
|
-
return
|
|
822
|
+
function countLines(text2) {
|
|
823
|
+
if (!text2) return 0;
|
|
824
|
+
return text2.split("\n").length;
|
|
551
825
|
}
|
|
552
826
|
function diffFromInput(input) {
|
|
553
827
|
if (!input) return {};
|
|
@@ -674,9 +948,9 @@ function applyAgentEvent(parts, event) {
|
|
|
674
948
|
function partsToAssistantText(parts) {
|
|
675
949
|
return parts.filter((p) => p.type === "text").map((p) => p.text).join("").trim();
|
|
676
950
|
}
|
|
677
|
-
function stripBrightsyNdjsonNoise(
|
|
678
|
-
if (!
|
|
679
|
-
let out =
|
|
951
|
+
function stripBrightsyNdjsonNoise(text2) {
|
|
952
|
+
if (!text2 || !text2.includes('"type"')) return text2;
|
|
953
|
+
let out = text2;
|
|
680
954
|
out = out.replace(
|
|
681
955
|
/\{"type":"(?:tool_use|tool_result|tool|thinking|usage|done|error)"[\s\S]*?\}\s*(?=\{"type":"|$|(?=[A-Za-z*#]))/g,
|
|
682
956
|
""
|
|
@@ -724,9 +998,9 @@ async function spawnAgentTurn(thread, input, onEvent) {
|
|
|
724
998
|
`Cannot spawn ${thread.agent}: thread ${thread.id} has no worktreePath`
|
|
725
999
|
);
|
|
726
1000
|
}
|
|
727
|
-
const { isGlobalThread: isGlobalThread2 } = await import("./global-workspace-
|
|
1001
|
+
const { isGlobalThread: isGlobalThread2 } = await import("./global-workspace-SVRYNJZA.js");
|
|
728
1002
|
if (isGlobalThread2(thread)) {
|
|
729
|
-
const { ensureGlobalCoordinatorCwd: ensureGlobalCoordinatorCwd2 } = await import("./coordinator-prompt-
|
|
1003
|
+
const { ensureGlobalCoordinatorCwd: ensureGlobalCoordinatorCwd2 } = await import("./coordinator-prompt-QYYG2IBB.js");
|
|
730
1004
|
ensureGlobalCoordinatorCwd2(
|
|
731
1005
|
isOrchestratorThread(thread) ? { orchestratorThreadId: thread.id } : void 0
|
|
732
1006
|
);
|
|
@@ -813,8 +1087,8 @@ async function spawnAgentTurn(thread, input, onEvent) {
|
|
|
813
1087
|
onEvent({ type: "exit", data: exitCode });
|
|
814
1088
|
const finalized = finalizeParts(parts);
|
|
815
1089
|
const rawText = assistantText.trim() || partsToAssistantText(finalized);
|
|
816
|
-
const
|
|
817
|
-
return { exitCode, sessionId, assistantText:
|
|
1090
|
+
const text2 = thread.agent === "brightsy" ? stripBrightsyNdjsonNoise(rawText) : rawText;
|
|
1091
|
+
return { exitCode, sessionId, assistantText: text2, parts: finalized, usage };
|
|
818
1092
|
});
|
|
819
1093
|
return {
|
|
820
1094
|
pid: child.pid,
|
|
@@ -1054,7 +1328,7 @@ import {
|
|
|
1054
1328
|
readdirSync,
|
|
1055
1329
|
readFileSync as readFileSync2
|
|
1056
1330
|
} from "fs";
|
|
1057
|
-
import { createServer } from "net";
|
|
1331
|
+
import { createServer as createServer2 } from "net";
|
|
1058
1332
|
import { basename, dirname, join as join2 } from "path";
|
|
1059
1333
|
import { execa as execa2 } from "execa";
|
|
1060
1334
|
import { createInterface as createInterface2 } from "readline";
|
|
@@ -1306,7 +1580,7 @@ function getRunMode(worktreePath, repoPath) {
|
|
|
1306
1580
|
}
|
|
1307
1581
|
async function allocatePort() {
|
|
1308
1582
|
return new Promise((resolve, reject) => {
|
|
1309
|
-
const server =
|
|
1583
|
+
const server = createServer2();
|
|
1310
1584
|
server.listen(0, "127.0.0.1", () => {
|
|
1311
1585
|
const addr = server.address();
|
|
1312
1586
|
if (!addr || typeof addr === "string") {
|
|
@@ -1326,7 +1600,7 @@ async function allocatePortRange(size = PORT_RANGE_SIZE) {
|
|
|
1326
1600
|
for (let i = 1; i < size; i++) {
|
|
1327
1601
|
const candidate = base + i;
|
|
1328
1602
|
const free = await new Promise((resolve) => {
|
|
1329
|
-
const server =
|
|
1603
|
+
const server = createServer2();
|
|
1330
1604
|
server.once("error", () => resolve(false));
|
|
1331
1605
|
server.listen(candidate, "127.0.0.1", () => {
|
|
1332
1606
|
server.close(() => resolve(true));
|
|
@@ -1679,13 +1953,13 @@ function formatStat(files) {
|
|
|
1679
1953
|
return `${n} file${n === 1 ? "" : "s"} changed, ${additions} insertions(+), ${deletions} deletions(-)`;
|
|
1680
1954
|
}
|
|
1681
1955
|
function emptyScopeStats() {
|
|
1682
|
-
const
|
|
1956
|
+
const z3 = emptyStat();
|
|
1683
1957
|
return {
|
|
1684
|
-
commits:
|
|
1685
|
-
uncommitted:
|
|
1686
|
-
staged:
|
|
1687
|
-
unstaged:
|
|
1688
|
-
last_turn:
|
|
1958
|
+
commits: z3,
|
|
1959
|
+
uncommitted: z3,
|
|
1960
|
+
staged: z3,
|
|
1961
|
+
unstaged: z3,
|
|
1962
|
+
last_turn: z3
|
|
1689
1963
|
};
|
|
1690
1964
|
}
|
|
1691
1965
|
function filesFromDiff(nameStatus, numstat, combinedDiff, maxHunk) {
|
|
@@ -2674,16 +2948,16 @@ var PASTE_ATTACH_MIN_CHARS = 1200;
|
|
|
2674
2948
|
var PASTE_ATTACH_MIN_LINES = 15;
|
|
2675
2949
|
var PASTED_NAME_RE = /^Pasted text #(\d+)\.txt$/i;
|
|
2676
2950
|
var PASTED_NAME_ALT_RE = /^pasted-(\d+)\.txt$/i;
|
|
2677
|
-
function pastedTextStats(
|
|
2678
|
-
const chars =
|
|
2951
|
+
function pastedTextStats(text2) {
|
|
2952
|
+
const chars = text2.length;
|
|
2679
2953
|
if (chars === 0) return { chars: 0, lines: 0 };
|
|
2680
|
-
const lines =
|
|
2954
|
+
const lines = text2.split(/\r\n|\r|\n/).length;
|
|
2681
2955
|
return { chars, lines };
|
|
2682
2956
|
}
|
|
2683
|
-
function shouldAttachPastedText(
|
|
2684
|
-
const trimmed =
|
|
2957
|
+
function shouldAttachPastedText(text2) {
|
|
2958
|
+
const trimmed = text2.trim();
|
|
2685
2959
|
if (!trimmed) return false;
|
|
2686
|
-
const { chars, lines } = pastedTextStats(
|
|
2960
|
+
const { chars, lines } = pastedTextStats(text2);
|
|
2687
2961
|
return chars >= PASTE_ATTACH_MIN_CHARS || lines >= PASTE_ATTACH_MIN_LINES;
|
|
2688
2962
|
}
|
|
2689
2963
|
function nextPastedTextName(existing) {
|
|
@@ -2694,13 +2968,13 @@ function nextPastedTextName(existing) {
|
|
|
2694
2968
|
}
|
|
2695
2969
|
return `Pasted text #${max + 1}.txt`;
|
|
2696
2970
|
}
|
|
2697
|
-
function buildPastedTextAttachment(
|
|
2971
|
+
function buildPastedTextAttachment(text2, opts) {
|
|
2698
2972
|
return {
|
|
2699
2973
|
id: opts?.id ?? randomUUID2(),
|
|
2700
2974
|
name: opts?.name ?? "Pasted text #1.txt",
|
|
2701
2975
|
kind: "file",
|
|
2702
2976
|
path: opts?.path,
|
|
2703
|
-
content:
|
|
2977
|
+
content: text2
|
|
2704
2978
|
};
|
|
2705
2979
|
}
|
|
2706
2980
|
|
|
@@ -2748,9 +3022,9 @@ async function tryClaudeSummary(transcript, opts) {
|
|
|
2748
3022
|
{ cwd: opts?.cwd, reject: false }
|
|
2749
3023
|
);
|
|
2750
3024
|
if (exitCode !== 0) return null;
|
|
2751
|
-
const
|
|
2752
|
-
if (
|
|
2753
|
-
return
|
|
3025
|
+
const text2 = stdout.trim();
|
|
3026
|
+
if (text2.length < 40) return null;
|
|
3027
|
+
return text2;
|
|
2754
3028
|
} catch {
|
|
2755
3029
|
return null;
|
|
2756
3030
|
}
|
|
@@ -2799,9 +3073,9 @@ function extractiveSummary(transcript) {
|
|
|
2799
3073
|
);
|
|
2800
3074
|
return parts.join("\n");
|
|
2801
3075
|
}
|
|
2802
|
-
function clipSummary(
|
|
2803
|
-
if (
|
|
2804
|
-
return `${
|
|
3076
|
+
function clipSummary(text2) {
|
|
3077
|
+
if (text2.length <= MAX_SUMMARY_CHARS) return text2;
|
|
3078
|
+
return `${text2.slice(0, MAX_SUMMARY_CHARS)}
|
|
2805
3079
|
|
|
2806
3080
|
[\u2026summary truncated\u2026]`;
|
|
2807
3081
|
}
|
|
@@ -3203,7 +3477,7 @@ async function createThread(input, _onSetupLine) {
|
|
|
3203
3477
|
return readThread(thread.id) ?? thread;
|
|
3204
3478
|
}
|
|
3205
3479
|
async function listLinearIssues(agent, repoPath) {
|
|
3206
|
-
const { getAdapter: getAdapter2 } = await import("./agents-
|
|
3480
|
+
const { getAdapter: getAdapter2 } = await import("./agents-TCAK3MXN.js");
|
|
3207
3481
|
await requireAgent(agent, { requireLinear: true });
|
|
3208
3482
|
const adapter = getAdapter2(agent);
|
|
3209
3483
|
if (!adapter.listLinearIssues) {
|
|
@@ -3701,13 +3975,13 @@ function resolveConductorCursorAgentId(workspacePath) {
|
|
|
3701
3975
|
for (const hash of hashes) {
|
|
3702
3976
|
const agentsFile = join7(CURSOR_SDK_STORE, hash, "agents.ndjson");
|
|
3703
3977
|
if (!existsSync9(agentsFile)) continue;
|
|
3704
|
-
let
|
|
3978
|
+
let text2;
|
|
3705
3979
|
try {
|
|
3706
|
-
|
|
3980
|
+
text2 = readFileSync7(agentsFile, "utf8");
|
|
3707
3981
|
} catch {
|
|
3708
3982
|
continue;
|
|
3709
3983
|
}
|
|
3710
|
-
for (const line of
|
|
3984
|
+
for (const line of text2.split("\n")) {
|
|
3711
3985
|
const trimmed = line.trim();
|
|
3712
3986
|
if (!trimmed) continue;
|
|
3713
3987
|
try {
|
|
@@ -3746,7 +4020,7 @@ async function adoptThread(input) {
|
|
|
3746
4020
|
messages: input.messages ?? []
|
|
3747
4021
|
});
|
|
3748
4022
|
writeThread(thread);
|
|
3749
|
-
const { ensureWorkspace: ensureWorkspace2 } = await import("./workspaces-
|
|
4023
|
+
const { ensureWorkspace: ensureWorkspace2 } = await import("./workspaces-AHFTHVBT.js");
|
|
3750
4024
|
await ensureWorkspace2(repoPath);
|
|
3751
4025
|
return thread;
|
|
3752
4026
|
}
|
|
@@ -4435,7 +4709,7 @@ function buildReviewRequestAttachment(content, opts) {
|
|
|
4435
4709
|
function readExistingReviewRequestFile(worktreePath) {
|
|
4436
4710
|
return readTextIfPresent(join10(worktreePath, REPO_REVIEW_PATH)) ?? readTextIfPresent(join10(worktreePath, REVIEW_REQUEST_PATH)) ?? readTextIfPresent(join10(worktreePath, LEGACY_REVIEW_REQUEST_PATH));
|
|
4437
4711
|
}
|
|
4438
|
-
async function requestReview(threadRef,
|
|
4712
|
+
async function requestReview(threadRef, send2) {
|
|
4439
4713
|
const from = findThreadByRef(threadRef);
|
|
4440
4714
|
if (!from) throw new Error(`Thread not found: ${threadRef}`);
|
|
4441
4715
|
if (isOrchestratorThread(from)) {
|
|
@@ -4457,7 +4731,7 @@ async function requestReview(threadRef, send) {
|
|
|
4457
4731
|
})
|
|
4458
4732
|
]
|
|
4459
4733
|
});
|
|
4460
|
-
const started = await
|
|
4734
|
+
const started = await send2(tab.id, REVIEW_REQUEST_PREFILL);
|
|
4461
4735
|
return { tab: started, from };
|
|
4462
4736
|
}
|
|
4463
4737
|
|
|
@@ -4513,8 +4787,8 @@ function buildQuotaHandoffAttachment(from, limitText, fallbackAgent) {
|
|
|
4513
4787
|
);
|
|
4514
4788
|
const recent = from.messages.slice(-8).map((m) => {
|
|
4515
4789
|
const role = m.role === "user" ? "User" : m.role === "agent" ? "Agent" : "Summary";
|
|
4516
|
-
const
|
|
4517
|
-
return
|
|
4790
|
+
const text2 = m.text.trim().replace(/\s+/g, " ").slice(0, 280);
|
|
4791
|
+
return text2 ? `- ${role}: ${text2}` : null;
|
|
4518
4792
|
}).filter(Boolean);
|
|
4519
4793
|
const body = [
|
|
4520
4794
|
`# Orchestration handoff`,
|
|
@@ -4697,7 +4971,7 @@ var Orchestrator = class {
|
|
|
4697
4971
|
orphans: orphans.map((o) => ({ path: o.path, repoPath: o.repoPath }))
|
|
4698
4972
|
});
|
|
4699
4973
|
}
|
|
4700
|
-
const { autoCleanupOrphansEnabled: autoCleanupOrphansEnabled2 } = await import("./app-settings-
|
|
4974
|
+
const { autoCleanupOrphansEnabled: autoCleanupOrphansEnabled2 } = await import("./app-settings-3TLA2EJ7.js");
|
|
4701
4975
|
if (autoCleanupOrphansEnabled2() && shouldRunWorktreeCleanup() && orphans.length > 0) {
|
|
4702
4976
|
await cleanupOrphanWorktrees({ repoPaths });
|
|
4703
4977
|
}
|
|
@@ -4854,7 +5128,7 @@ var Orchestrator = class {
|
|
|
4854
5128
|
}
|
|
4855
5129
|
async finishCreateThread(threadId, prompt) {
|
|
4856
5130
|
await this.runSetupAfterCreate(threadId);
|
|
4857
|
-
const { autoRunAfterSetupEnabled: autoRunAfterSetupEnabled2 } = await import("./app-settings-
|
|
5131
|
+
const { autoRunAfterSetupEnabled: autoRunAfterSetupEnabled2 } = await import("./app-settings-3TLA2EJ7.js");
|
|
4858
5132
|
if (autoRunAfterSetupEnabled2()) {
|
|
4859
5133
|
try {
|
|
4860
5134
|
await this.startDev(threadId);
|
|
@@ -4934,11 +5208,11 @@ var Orchestrator = class {
|
|
|
4934
5208
|
return results;
|
|
4935
5209
|
}
|
|
4936
5210
|
/** Edit the text of a not-yet-started queued message. */
|
|
4937
|
-
async editQueuedMessage(threadRef, index,
|
|
5211
|
+
async editQueuedMessage(threadRef, index, text2) {
|
|
4938
5212
|
const thread = this.requireThread(threadRef);
|
|
4939
5213
|
return withThreadLock(thread.id, async () => {
|
|
4940
5214
|
const current = this.requireThread(thread.id);
|
|
4941
|
-
const trimmed =
|
|
5215
|
+
const trimmed = text2.trim();
|
|
4942
5216
|
if (!trimmed || index < 0 || index >= current.queue.length) {
|
|
4943
5217
|
return current;
|
|
4944
5218
|
}
|
|
@@ -5083,10 +5357,19 @@ var Orchestrator = class {
|
|
|
5083
5357
|
attachments: sentAttachments ?? thread.attachments
|
|
5084
5358
|
}
|
|
5085
5359
|
);
|
|
5086
|
-
const
|
|
5087
|
-
|
|
5088
|
-
|
|
5089
|
-
|
|
5360
|
+
const slackInbound = /^(Slack DM|Slack @mention)(?:\n|$)/.test(
|
|
5361
|
+
expandedPrompt.trim()
|
|
5362
|
+
);
|
|
5363
|
+
const orchestrationReminder = isOrchestratorThread(thread) ? [
|
|
5364
|
+
coordinatorTurnReminder({
|
|
5365
|
+
parentId: threadId,
|
|
5366
|
+
goal: thread.sourceRef || thread.title
|
|
5367
|
+
}),
|
|
5368
|
+
slackInbound ? [
|
|
5369
|
+
"This turn is a Slack DM or @mention. Your reply is posted back in Slack \u2014 keep it concise. Sideboard signs it with this Mac's destination name; do not prefix the name yourself.",
|
|
5370
|
+
SLACK_REPLY_FORMATTING
|
|
5371
|
+
].join("\n") : null
|
|
5372
|
+
].filter(Boolean).join("\n") : null;
|
|
5090
5373
|
const artifactReminder = thread.agent !== "brightsy" ? [
|
|
5091
5374
|
"Sideboard side column (important):",
|
|
5092
5375
|
"There is no claude.ai Artifact tool here \u2014 that is normal.",
|
|
@@ -5126,7 +5409,7 @@ var Orchestrator = class {
|
|
|
5126
5409
|
});
|
|
5127
5410
|
const artifactDirective = isBrightsy ? null : formatArtifactDirective();
|
|
5128
5411
|
const settings = loadWorkspaceSettings(fresh.worktreePath, fresh.repoPath);
|
|
5129
|
-
const { autoRenameBranchEnabled: autoRenameBranchEnabled2 } = await import("./app-settings-
|
|
5412
|
+
const { autoRenameBranchEnabled: autoRenameBranchEnabled2 } = await import("./app-settings-3TLA2EJ7.js");
|
|
5130
5413
|
const renameBranchDirective = !isBrightsy && !isOrchestration && autoRenameBranchEnabled2() ? formatRenameBranchDirective(fresh, {
|
|
5131
5414
|
customPrompt: settings?.prompts?.renameBranch
|
|
5132
5415
|
}) : null;
|
|
@@ -5146,7 +5429,9 @@ var Orchestrator = class {
|
|
|
5146
5429
|
goal: fresh.sourceRef || fresh.title || "Orchestration",
|
|
5147
5430
|
parentId: fresh.id,
|
|
5148
5431
|
workspaces: inventory,
|
|
5149
|
-
audience:
|
|
5432
|
+
audience: /^(Slack DM|Slack @mention)(?:\n|$)/.test(
|
|
5433
|
+
expandedPrompt.trim()
|
|
5434
|
+
) ? "slack" : "desktop"
|
|
5150
5435
|
});
|
|
5151
5436
|
}
|
|
5152
5437
|
}
|
|
@@ -5771,7 +6056,8 @@ var Orchestrator = class {
|
|
|
5771
6056
|
isDraft: false,
|
|
5772
6057
|
reviewDecision: null,
|
|
5773
6058
|
baseRefName: "",
|
|
5774
|
-
headRefName: ""
|
|
6059
|
+
headRefName: "",
|
|
6060
|
+
isInMergeQueue: false
|
|
5775
6061
|
};
|
|
5776
6062
|
await this.persistPrMetaAndMaybeArchive(thread, metaLike);
|
|
5777
6063
|
return { url: metaLike.url, state };
|
|
@@ -5821,7 +6107,7 @@ var Orchestrator = class {
|
|
|
5821
6107
|
updateThread(thread.id, { title: meta.title });
|
|
5822
6108
|
}
|
|
5823
6109
|
}
|
|
5824
|
-
const { autoArchiveOnMergeEnabled: autoArchiveOnMergeEnabled2 } = await import("./app-settings-
|
|
6110
|
+
const { autoArchiveOnMergeEnabled: autoArchiveOnMergeEnabled2 } = await import("./app-settings-3TLA2EJ7.js");
|
|
5825
6111
|
const latest = this.requireThread(thread.id);
|
|
5826
6112
|
if (!shouldAutoArchiveOnPrMerge({
|
|
5827
6113
|
previousPrState: prevState || null,
|
|
@@ -6045,7 +6331,7 @@ var Orchestrator = class {
|
|
|
6045
6331
|
this.emit({ type: "status_changed", threadId: archived.id, status: "archived" });
|
|
6046
6332
|
if (thread.repoPath && !isGlobalRepoPath(thread.repoPath)) {
|
|
6047
6333
|
try {
|
|
6048
|
-
const { ensureWorkspace: ensureWorkspace2 } = await import("./workspaces-
|
|
6334
|
+
const { ensureWorkspace: ensureWorkspace2 } = await import("./workspaces-AHFTHVBT.js");
|
|
6049
6335
|
await ensureWorkspace2(thread.repoPath);
|
|
6050
6336
|
} catch {
|
|
6051
6337
|
}
|
|
@@ -6066,7 +6352,7 @@ var Orchestrator = class {
|
|
|
6066
6352
|
await runArchiveScript(thread.repoPath, thread.worktreePath);
|
|
6067
6353
|
} catch {
|
|
6068
6354
|
}
|
|
6069
|
-
const { deleteBranchOnPurgeEnabled: deleteBranchOnPurgeEnabled2 } = await import("./app-settings-
|
|
6355
|
+
const { deleteBranchOnPurgeEnabled: deleteBranchOnPurgeEnabled2 } = await import("./app-settings-3TLA2EJ7.js");
|
|
6070
6356
|
const deleteBranch = opts?.deleteBranch ?? deleteBranchOnPurgeEnabled2();
|
|
6071
6357
|
await removeWorktree(thread.repoPath, thread.worktreePath, {
|
|
6072
6358
|
deleteBranch: deleteBranch ? thread.branchName : void 0
|
|
@@ -6087,7 +6373,7 @@ var Orchestrator = class {
|
|
|
6087
6373
|
return restored2;
|
|
6088
6374
|
}
|
|
6089
6375
|
if (!existsSync13(thread.worktreePath)) {
|
|
6090
|
-
const { createThreadWorktree: createThreadWorktree2 } = await import("./worktree-
|
|
6376
|
+
const { createThreadWorktree: createThreadWorktree2 } = await import("./worktree-VOCH3WS3.js");
|
|
6091
6377
|
const { execa: execa6 } = await import("execa");
|
|
6092
6378
|
const slug = thread.worktreePath.split("/").pop();
|
|
6093
6379
|
const dest = thread.worktreePath;
|
|
@@ -6196,7 +6482,7 @@ async function startOrchestration(opts) {
|
|
|
6196
6482
|
sourceRef: "default",
|
|
6197
6483
|
...createOpts
|
|
6198
6484
|
}).catch(async () => {
|
|
6199
|
-
const { resolveDefaultBranch: resolveDefaultBranch2, resolveRepoRoot: resolveRepoRoot2 } = await import("./worktree-
|
|
6485
|
+
const { resolveDefaultBranch: resolveDefaultBranch2, resolveRepoRoot: resolveRepoRoot2 } = await import("./worktree-VOCH3WS3.js");
|
|
6200
6486
|
const repo = await resolveRepoRoot2(repoPath);
|
|
6201
6487
|
const def = await resolveDefaultBranch2(repo);
|
|
6202
6488
|
return createThread({
|
|
@@ -6318,7 +6604,7 @@ function formatPlanQuestionsForChat(questions) {
|
|
|
6318
6604
|
// src/mcp/server.ts
|
|
6319
6605
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
6320
6606
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
6321
|
-
import { z } from "zod";
|
|
6607
|
+
import { z as z2 } from "zod";
|
|
6322
6608
|
import { basename as basename4 } from "path";
|
|
6323
6609
|
|
|
6324
6610
|
// src/mcp/archive-guard.ts
|
|
@@ -6329,379 +6615,1144 @@ function mcpArchiveBlockedReason(thread) {
|
|
|
6329
6615
|
return null;
|
|
6330
6616
|
}
|
|
6331
6617
|
|
|
6332
|
-
// src/mcp/
|
|
6333
|
-
|
|
6334
|
-
|
|
6335
|
-
|
|
6336
|
-
|
|
6337
|
-
|
|
6338
|
-
|
|
6339
|
-
}
|
|
6340
|
-
|
|
6341
|
-
|
|
6342
|
-
|
|
6343
|
-
|
|
6344
|
-
|
|
6345
|
-
|
|
6346
|
-
|
|
6347
|
-
|
|
6348
|
-
|
|
6349
|
-
|
|
6618
|
+
// src/mcp/slack-tools.ts
|
|
6619
|
+
import { z } from "zod";
|
|
6620
|
+
|
|
6621
|
+
// src/slack/api.ts
|
|
6622
|
+
var SLACK_API = "https://slack.com/api";
|
|
6623
|
+
var SlackApiError = class extends Error {
|
|
6624
|
+
constructor(method, slackError) {
|
|
6625
|
+
super(`Slack ${method}: ${slackError}`);
|
|
6626
|
+
this.method = method;
|
|
6627
|
+
this.slackError = slackError;
|
|
6628
|
+
this.name = "SlackApiError";
|
|
6629
|
+
}
|
|
6630
|
+
method;
|
|
6631
|
+
slackError;
|
|
6632
|
+
};
|
|
6633
|
+
async function slackApi(token, method, params, fetchImpl) {
|
|
6634
|
+
const doFetch = fetchImpl ?? fetch;
|
|
6635
|
+
const body = new URLSearchParams();
|
|
6636
|
+
if (params) {
|
|
6637
|
+
for (const [key, value] of Object.entries(params)) {
|
|
6638
|
+
if (value === void 0) continue;
|
|
6639
|
+
body.set(key, String(value));
|
|
6640
|
+
}
|
|
6641
|
+
}
|
|
6642
|
+
const res = await doFetch(`${SLACK_API}/${method}`, {
|
|
6643
|
+
method: "POST",
|
|
6644
|
+
headers: {
|
|
6645
|
+
Authorization: `Bearer ${token}`,
|
|
6646
|
+
"Content-Type": "application/x-www-form-urlencoded"
|
|
6647
|
+
},
|
|
6648
|
+
body
|
|
6350
6649
|
});
|
|
6351
|
-
|
|
6352
|
-
|
|
6353
|
-
|
|
6354
|
-
try {
|
|
6355
|
-
const { maxConcurrentAgents: maxConcurrentAgents2 } = await import("./app-settings-ONKMFSEJ.js");
|
|
6356
|
-
orch.setMaxConcurrent(maxConcurrentAgents2());
|
|
6357
|
-
} catch {
|
|
6650
|
+
const json = await res.json();
|
|
6651
|
+
if (!json.ok) {
|
|
6652
|
+
throw new SlackApiError(method, json.error || `HTTP ${res.status}`);
|
|
6358
6653
|
}
|
|
6359
|
-
|
|
6360
|
-
|
|
6361
|
-
|
|
6362
|
-
|
|
6363
|
-
|
|
6364
|
-
|
|
6654
|
+
return json;
|
|
6655
|
+
}
|
|
6656
|
+
async function slackAuthTest(token) {
|
|
6657
|
+
return slackApi(token, "auth.test");
|
|
6658
|
+
}
|
|
6659
|
+
|
|
6660
|
+
// src/slack/destination.ts
|
|
6661
|
+
function isChannelId(raw) {
|
|
6662
|
+
return /^[CGD][A-Z0-9]+$/i.test(raw);
|
|
6663
|
+
}
|
|
6664
|
+
function isUserId(raw) {
|
|
6665
|
+
return /^U[A-Z0-9]+$/i.test(raw);
|
|
6666
|
+
}
|
|
6667
|
+
function isEmail(raw) {
|
|
6668
|
+
return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(raw);
|
|
6669
|
+
}
|
|
6670
|
+
function normalizeSlackHandle(raw) {
|
|
6671
|
+
let s = raw.trim();
|
|
6672
|
+
const mention = s.match(/^<@([UW][A-Z0-9]+)(?:\|[^>]+)?>$/i);
|
|
6673
|
+
if (mention?.[1]) return mention[1];
|
|
6674
|
+
const channelMention = s.match(/^<#([CGD][A-Z0-9]+)(?:\|[^>]+)?>$/i);
|
|
6675
|
+
if (channelMention?.[1]) return channelMention[1];
|
|
6676
|
+
if (s.startsWith("#") || s.startsWith("@")) s = s.slice(1);
|
|
6677
|
+
return s.trim();
|
|
6678
|
+
}
|
|
6679
|
+
async function listSlackUsers(token, opts) {
|
|
6680
|
+
const query = opts?.query?.trim().toLowerCase() ?? "";
|
|
6681
|
+
const limit = Math.min(200, Math.max(1, opts?.limit ?? 50));
|
|
6682
|
+
const members = [];
|
|
6683
|
+
let cursor;
|
|
6684
|
+
do {
|
|
6685
|
+
const data = await slackApi(
|
|
6686
|
+
token,
|
|
6687
|
+
"users.list",
|
|
6688
|
+
{
|
|
6689
|
+
limit: 200,
|
|
6690
|
+
cursor: cursor || void 0
|
|
6691
|
+
},
|
|
6692
|
+
opts?.fetchImpl
|
|
6365
6693
|
);
|
|
6694
|
+
for (const m of data.members ?? []) {
|
|
6695
|
+
if (!m.id || m.deleted || m.is_bot) continue;
|
|
6696
|
+
members.push(m);
|
|
6697
|
+
}
|
|
6698
|
+
cursor = data.response_metadata?.next_cursor?.trim() || void 0;
|
|
6699
|
+
} while (cursor && members.length < 2e3);
|
|
6700
|
+
const mapped = members.map((m) => ({
|
|
6701
|
+
id: m.id,
|
|
6702
|
+
name: m.name ?? m.id,
|
|
6703
|
+
real_name: m.profile?.real_name,
|
|
6704
|
+
display_name: m.profile?.display_name,
|
|
6705
|
+
email: m.profile?.email
|
|
6706
|
+
}));
|
|
6707
|
+
const filtered = query ? mapped.filter((u) => {
|
|
6708
|
+
const hay = [u.name, u.real_name, u.display_name, u.email, u.id].filter(Boolean).join(" ").toLowerCase();
|
|
6709
|
+
return hay.includes(query);
|
|
6710
|
+
}) : mapped;
|
|
6711
|
+
return filtered.slice(0, limit);
|
|
6712
|
+
}
|
|
6713
|
+
async function findUserId(token, handle, fetchImpl) {
|
|
6714
|
+
if (isUserId(handle)) {
|
|
6715
|
+
return { id: handle, name: handle };
|
|
6366
6716
|
}
|
|
6367
|
-
|
|
6368
|
-
|
|
6369
|
-
|
|
6370
|
-
|
|
6371
|
-
|
|
6372
|
-
|
|
6373
|
-
|
|
6374
|
-
{},
|
|
6375
|
-
async () => {
|
|
6376
|
-
const workspaces = orch.listWorkspaces();
|
|
6377
|
-
const lines = await Promise.all(
|
|
6378
|
-
workspaces.map(async (w) => {
|
|
6379
|
-
const slug = await resolveGithubRepoSlug(w.path).catch(() => null);
|
|
6380
|
-
return slug ? `${w.name} ${w.path} github:${slug}` : `${w.name} ${w.path}`;
|
|
6381
|
-
})
|
|
6717
|
+
if (isEmail(handle)) {
|
|
6718
|
+
try {
|
|
6719
|
+
const data = await slackApi(
|
|
6720
|
+
token,
|
|
6721
|
+
"users.lookupByEmail",
|
|
6722
|
+
{ email: handle },
|
|
6723
|
+
fetchImpl
|
|
6382
6724
|
);
|
|
6383
|
-
|
|
6384
|
-
|
|
6385
|
-
|
|
6386
|
-
|
|
6387
|
-
|
|
6725
|
+
if (data.user?.id) {
|
|
6726
|
+
return {
|
|
6727
|
+
id: data.user.id,
|
|
6728
|
+
name: data.user.name ?? data.user.id,
|
|
6729
|
+
real_name: data.user.profile?.real_name,
|
|
6730
|
+
display_name: data.user.profile?.display_name,
|
|
6731
|
+
email: data.user.profile?.email
|
|
6732
|
+
};
|
|
6733
|
+
}
|
|
6734
|
+
} catch {
|
|
6388
6735
|
}
|
|
6736
|
+
}
|
|
6737
|
+
const users = await listSlackUsers(token, { query: handle, limit: 20, fetchImpl });
|
|
6738
|
+
const exact = users.find(
|
|
6739
|
+
(u) => u.name.toLowerCase() === handle.toLowerCase() || u.display_name?.toLowerCase() === handle.toLowerCase() || u.real_name?.toLowerCase() === handle.toLowerCase() || u.email?.toLowerCase() === handle.toLowerCase()
|
|
6389
6740
|
);
|
|
6390
|
-
|
|
6391
|
-
|
|
6392
|
-
|
|
6393
|
-
{}
|
|
6394
|
-
|
|
6395
|
-
|
|
6396
|
-
|
|
6397
|
-
const repo = t.repoPath === GLOBAL_WORKSPACE_ID ? "Orchestration" : basename4(t.repoPath) || t.repoPath;
|
|
6398
|
-
return `${t.id.slice(0, 8)} ${t.status.padEnd(9)} ${t.agent.padEnd(8)} ${repo} ${t.sourceType}:${t.sourceRef} ${t.title} sideboard://thread/${t.id}${t.devPort ? ` http://localhost:${t.devPort}` : ""}`;
|
|
6399
|
-
});
|
|
6400
|
-
return {
|
|
6401
|
-
content: [{ type: "text", text: lines.join("\n") || "(no threads)" }]
|
|
6402
|
-
};
|
|
6403
|
-
}
|
|
6741
|
+
if (exact) return exact;
|
|
6742
|
+
if (users.length === 1) return users[0];
|
|
6743
|
+
if (users.length === 0) {
|
|
6744
|
+
throw new Error(`No Slack user matching "${handle}"`);
|
|
6745
|
+
}
|
|
6746
|
+
throw new Error(
|
|
6747
|
+
`Ambiguous Slack user "${handle}". Matches: ${users.slice(0, 5).map((u) => `${u.name} (${u.id})`).join(", ")}. Pass a user id (U\u2026) or use slack_list_users.`
|
|
6404
6748
|
);
|
|
6405
|
-
|
|
6406
|
-
|
|
6407
|
-
|
|
6408
|
-
|
|
6409
|
-
|
|
6410
|
-
|
|
6411
|
-
|
|
6412
|
-
return { content: [{ type: "text", text: `Thread not found: ${ref}` }], isError: true };
|
|
6413
|
-
}
|
|
6414
|
-
const summary = {
|
|
6415
|
-
id: t.id,
|
|
6416
|
-
title: t.title,
|
|
6417
|
-
status: t.status,
|
|
6418
|
-
agent: t.agent,
|
|
6419
|
-
sourceType: t.sourceType,
|
|
6420
|
-
sourceRef: t.sourceRef,
|
|
6421
|
-
branchName: t.branchName,
|
|
6422
|
-
worktreePath: t.worktreePath,
|
|
6423
|
-
sessionId: t.sessionId,
|
|
6424
|
-
queueLength: t.queue.length,
|
|
6425
|
-
messageCount: t.messages.length,
|
|
6426
|
-
devPort: t.devPort,
|
|
6427
|
-
prUrl: t.prUrl,
|
|
6428
|
-
lastError: t.lastError ?? null
|
|
6429
|
-
};
|
|
6430
|
-
return { content: [{ type: "text", text: JSON.stringify(summary, null, 2) }] };
|
|
6431
|
-
}
|
|
6749
|
+
}
|
|
6750
|
+
async function openDmChannel(token, userId, fetchImpl) {
|
|
6751
|
+
const data = await slackApi(
|
|
6752
|
+
token,
|
|
6753
|
+
"conversations.open",
|
|
6754
|
+
{ users: userId },
|
|
6755
|
+
fetchImpl
|
|
6432
6756
|
);
|
|
6433
|
-
|
|
6434
|
-
|
|
6435
|
-
|
|
6757
|
+
const id = data.channel?.id?.trim();
|
|
6758
|
+
if (!id) throw new Error(`Could not open Slack DM with user ${userId}`);
|
|
6759
|
+
return id;
|
|
6760
|
+
}
|
|
6761
|
+
async function resolveChannelByName(token, name, fetchImpl) {
|
|
6762
|
+
const data = await slackApi(
|
|
6763
|
+
token,
|
|
6764
|
+
"conversations.list",
|
|
6436
6765
|
{
|
|
6437
|
-
|
|
6438
|
-
|
|
6439
|
-
|
|
6440
|
-
),
|
|
6441
|
-
content: z.string().describe(
|
|
6442
|
-
"Full document body (complete HTML page, SVG markup, markdown, or a React component module with a default export)"
|
|
6443
|
-
),
|
|
6444
|
-
artifact_id: z.string().optional().describe("Stable id when updating the same artifact across turns")
|
|
6766
|
+
types: "public_channel,private_channel",
|
|
6767
|
+
exclude_archived: true,
|
|
6768
|
+
limit: 200
|
|
6445
6769
|
},
|
|
6446
|
-
|
|
6447
|
-
|
|
6448
|
-
|
|
6449
|
-
|
|
6450
|
-
|
|
6451
|
-
|
|
6452
|
-
|
|
6453
|
-
|
|
6770
|
+
fetchImpl
|
|
6771
|
+
);
|
|
6772
|
+
const match = (data.channels ?? []).find((c) => c.name?.toLowerCase() === name.toLowerCase());
|
|
6773
|
+
if (!match?.id) throw new Error(`No Slack channel named #${name}`);
|
|
6774
|
+
return match.id;
|
|
6775
|
+
}
|
|
6776
|
+
async function resolveSlackDestination(token, destination, opts) {
|
|
6777
|
+
const raw = destination.trim();
|
|
6778
|
+
if (!raw) throw new Error("destination is required");
|
|
6779
|
+
const hadAt = raw.startsWith("@") || /^<@/i.test(raw);
|
|
6780
|
+
const hadHash = raw.startsWith("#") || /^<#/i.test(raw);
|
|
6781
|
+
const handle = normalizeSlackHandle(raw);
|
|
6782
|
+
if (isChannelId(handle)) {
|
|
6783
|
+
return {
|
|
6784
|
+
channelId: handle,
|
|
6785
|
+
kind: handle.toUpperCase().startsWith("D") ? "dm" : "channel",
|
|
6786
|
+
label: handle
|
|
6787
|
+
};
|
|
6788
|
+
}
|
|
6789
|
+
if (isUserId(handle) || hadAt || isEmail(handle)) {
|
|
6790
|
+
const user = await findUserId(token, handle, opts?.fetchImpl);
|
|
6791
|
+
if (opts?.forRead) {
|
|
6792
|
+
const channelId2 = await openDmChannel(token, user.id, opts?.fetchImpl);
|
|
6793
|
+
return {
|
|
6794
|
+
channelId: channelId2,
|
|
6795
|
+
kind: "user",
|
|
6796
|
+
userId: user.id,
|
|
6797
|
+
label: user.display_name || user.real_name || user.name
|
|
6454
6798
|
};
|
|
6455
|
-
return { content: [{ type: "text", text: JSON.stringify(payload) }] };
|
|
6456
6799
|
}
|
|
6457
|
-
|
|
6458
|
-
|
|
6459
|
-
|
|
6460
|
-
|
|
6461
|
-
|
|
6462
|
-
|
|
6463
|
-
|
|
6464
|
-
|
|
6465
|
-
|
|
6466
|
-
|
|
6467
|
-
|
|
6468
|
-
|
|
6469
|
-
|
|
6470
|
-
|
|
6471
|
-
|
|
6472
|
-
|
|
6473
|
-
|
|
6474
|
-
|
|
6475
|
-
|
|
6476
|
-
|
|
6477
|
-
|
|
6478
|
-
|
|
6479
|
-
|
|
6480
|
-
|
|
6800
|
+
const channelId = await openDmChannel(token, user.id, opts?.fetchImpl);
|
|
6801
|
+
return {
|
|
6802
|
+
channelId,
|
|
6803
|
+
kind: "user",
|
|
6804
|
+
userId: user.id,
|
|
6805
|
+
label: user.display_name || user.real_name || user.name
|
|
6806
|
+
};
|
|
6807
|
+
}
|
|
6808
|
+
if (hadHash) {
|
|
6809
|
+
const channelId = await resolveChannelByName(token, handle, opts?.fetchImpl);
|
|
6810
|
+
return { channelId, kind: "channel", label: `#${handle}` };
|
|
6811
|
+
}
|
|
6812
|
+
try {
|
|
6813
|
+
const channelId = await resolveChannelByName(token, handle, opts?.fetchImpl);
|
|
6814
|
+
return { channelId, kind: "channel", label: `#${handle}` };
|
|
6815
|
+
} catch (channelErr) {
|
|
6816
|
+
try {
|
|
6817
|
+
const user = await findUserId(token, handle, opts?.fetchImpl);
|
|
6818
|
+
const channelId = await openDmChannel(token, user.id, opts?.fetchImpl);
|
|
6819
|
+
return {
|
|
6820
|
+
channelId,
|
|
6821
|
+
kind: "user",
|
|
6822
|
+
userId: user.id,
|
|
6823
|
+
label: user.display_name || user.real_name || user.name
|
|
6481
6824
|
};
|
|
6482
|
-
|
|
6825
|
+
} catch {
|
|
6826
|
+
throw channelErr;
|
|
6483
6827
|
}
|
|
6484
|
-
|
|
6485
|
-
|
|
6486
|
-
|
|
6487
|
-
|
|
6488
|
-
|
|
6489
|
-
|
|
6490
|
-
|
|
6491
|
-
|
|
6492
|
-
|
|
6493
|
-
|
|
6494
|
-
|
|
6495
|
-
|
|
6496
|
-
|
|
6497
|
-
|
|
6498
|
-
|
|
6499
|
-
|
|
6500
|
-
|
|
6501
|
-
|
|
6502
|
-
|
|
6503
|
-
|
|
6504
|
-
|
|
6505
|
-
|
|
6506
|
-
|
|
6507
|
-
return {
|
|
6828
|
+
}
|
|
6829
|
+
}
|
|
6830
|
+
|
|
6831
|
+
// src/slack/github-link.ts
|
|
6832
|
+
function labelGithubUrl(url) {
|
|
6833
|
+
const raw = url.trim();
|
|
6834
|
+
if (!raw) return null;
|
|
6835
|
+
let parsed;
|
|
6836
|
+
try {
|
|
6837
|
+
parsed = new URL(raw);
|
|
6838
|
+
} catch {
|
|
6839
|
+
return null;
|
|
6840
|
+
}
|
|
6841
|
+
if (!/(^|\.)github\.com$/i.test(parsed.hostname)) return null;
|
|
6842
|
+
const path = parsed.pathname;
|
|
6843
|
+
const hash = parsed.hash || "";
|
|
6844
|
+
const pr = path.match(/\/([^/]+)\/([^/]+)\/pull\/(\d+)/i);
|
|
6845
|
+
if (pr) {
|
|
6846
|
+
const n = pr[3];
|
|
6847
|
+
if (/#discussion_r\d+/i.test(hash) || /#pullrequestreview-\d+/i.test(hash)) {
|
|
6848
|
+
return { kind: "comment", label: `PR #${n} comment`, url: raw };
|
|
6849
|
+
}
|
|
6850
|
+
if (/#issuecomment-\d+/i.test(hash)) {
|
|
6851
|
+
return { kind: "comment", label: `PR #${n} comment`, url: raw };
|
|
6852
|
+
}
|
|
6853
|
+
return { kind: "pr", label: `PR #${n}`, url: raw };
|
|
6854
|
+
}
|
|
6855
|
+
const issueComment = path.match(/\/([^/]+)\/([^/]+)\/issues\/(\d+)/i);
|
|
6856
|
+
if (issueComment && /#issuecomment-\d+/i.test(hash)) {
|
|
6857
|
+
return { kind: "comment", label: `Issue #${issueComment[3]} comment`, url: raw };
|
|
6858
|
+
}
|
|
6859
|
+
const blob = path.match(/\/([^/]+)\/([^/]+)\/blob\/[^/]+\/(.+)$/i);
|
|
6860
|
+
if (blob) {
|
|
6861
|
+
const filePath = decodeURIComponent(blob[3]);
|
|
6862
|
+
const fileName = filePath.split("/").pop() || filePath;
|
|
6863
|
+
const line = hash.match(/^#L(\d+)(?:-L(\d+))?$/i);
|
|
6864
|
+
if (line) {
|
|
6865
|
+
const start = line[1];
|
|
6866
|
+
const end = line[2];
|
|
6867
|
+
const loc = end && end !== start ? `${start}-${end}` : start;
|
|
6868
|
+
return { kind: "code", label: `${fileName}:${loc}`, url: raw };
|
|
6869
|
+
}
|
|
6870
|
+
return { kind: "code", label: fileName, url: raw };
|
|
6871
|
+
}
|
|
6872
|
+
return { kind: "other", label: "GitHub", url: raw };
|
|
6873
|
+
}
|
|
6874
|
+
function appendGithubLink(text2, githubUrl) {
|
|
6875
|
+
const body = text2.trimEnd();
|
|
6876
|
+
if (!githubUrl?.trim()) return body;
|
|
6877
|
+
const labeled = labelGithubUrl(githubUrl);
|
|
6878
|
+
if (!labeled) {
|
|
6879
|
+
return body ? `${body}
|
|
6880
|
+
${githubUrl.trim()}` : githubUrl.trim();
|
|
6881
|
+
}
|
|
6882
|
+
const link = `<${labeled.url}|${labeled.label}>`;
|
|
6883
|
+
return body ? `${body}
|
|
6884
|
+
${link}` : link;
|
|
6885
|
+
}
|
|
6886
|
+
|
|
6887
|
+
// src/slack/outbound-watch.ts
|
|
6888
|
+
import { existsSync as existsSync14, readFileSync as readFileSync9 } from "fs";
|
|
6889
|
+
import { join as join12 } from "path";
|
|
6890
|
+
|
|
6891
|
+
// src/slack/workspaces.ts
|
|
6892
|
+
import { join as join11 } from "path";
|
|
6893
|
+
function storePath() {
|
|
6894
|
+
return join11(appDataDir(), "slack-workspaces.json");
|
|
6895
|
+
}
|
|
6896
|
+
function readStore() {
|
|
6897
|
+
try {
|
|
6898
|
+
const path = storePath();
|
|
6899
|
+
const wasEncrypted = isSecureFileEncrypted(path);
|
|
6900
|
+
const parsed = readSecureJson(path);
|
|
6901
|
+
const workspaces = Array.isArray(parsed?.workspaces) ? parsed.workspaces : [];
|
|
6902
|
+
if (workspaces.length > 0 && !wasEncrypted && resolveVaultKey()) {
|
|
6903
|
+
writeSecureJson(path, { workspaces });
|
|
6904
|
+
}
|
|
6905
|
+
return workspaces;
|
|
6906
|
+
} catch {
|
|
6907
|
+
return [];
|
|
6908
|
+
}
|
|
6909
|
+
}
|
|
6910
|
+
function writeStore(workspaces) {
|
|
6911
|
+
writeSecureJson(storePath(), { workspaces });
|
|
6912
|
+
return workspaces;
|
|
6913
|
+
}
|
|
6914
|
+
function toInfo(ws) {
|
|
6915
|
+
return {
|
|
6916
|
+
team_id: ws.team_id,
|
|
6917
|
+
team_name: ws.team_name,
|
|
6918
|
+
user_id: ws.user_id,
|
|
6919
|
+
has_bot_token: Boolean(ws.bot_token),
|
|
6920
|
+
has_user_token: Boolean(ws.user_token),
|
|
6921
|
+
connected_at: ws.connected_at
|
|
6922
|
+
};
|
|
6923
|
+
}
|
|
6924
|
+
function listSlackWorkspaces() {
|
|
6925
|
+
return readStore().map(toInfo).sort((a, b) => a.team_name.localeCompare(b.team_name));
|
|
6926
|
+
}
|
|
6927
|
+
function listSlackWorkspacesRaw() {
|
|
6928
|
+
return readStore();
|
|
6929
|
+
}
|
|
6930
|
+
function getSlackWorkspace(teamId) {
|
|
6931
|
+
const id = teamId.trim();
|
|
6932
|
+
if (!id) return null;
|
|
6933
|
+
return readStore().find(
|
|
6934
|
+
(ws) => ws.team_id === id || ws.team_name.toLowerCase() === id.toLowerCase()
|
|
6935
|
+
) ?? null;
|
|
6936
|
+
}
|
|
6937
|
+
function upsertSlackWorkspace(entry) {
|
|
6938
|
+
const workspaces = readStore().filter((ws) => ws.team_id !== entry.team_id);
|
|
6939
|
+
workspaces.push(entry);
|
|
6940
|
+
writeStore(workspaces);
|
|
6941
|
+
return toInfo(entry);
|
|
6942
|
+
}
|
|
6943
|
+
function disconnectSlackWorkspace(teamId) {
|
|
6944
|
+
const id = teamId.trim();
|
|
6945
|
+
writeStore(readStore().filter((ws) => ws.team_id !== id));
|
|
6946
|
+
return listSlackWorkspaces();
|
|
6947
|
+
}
|
|
6948
|
+
function tokenKind(token) {
|
|
6949
|
+
if (token.startsWith("xoxb-")) return "bot";
|
|
6950
|
+
if (token.startsWith("xoxp-") || token.startsWith("xoxe.xoxp-")) return "user";
|
|
6951
|
+
return "unknown";
|
|
6952
|
+
}
|
|
6953
|
+
async function connectSlackToken(rawToken) {
|
|
6954
|
+
const token = rawToken.trim();
|
|
6955
|
+
if (!token) throw new Error("Paste a Slack bot (xoxb-) or user (xoxp-) token");
|
|
6956
|
+
const auth = await slackAuthTest(token);
|
|
6957
|
+
const teamId = auth.team_id?.trim();
|
|
6958
|
+
if (!teamId) throw new Error("Slack auth.test did not return a team_id");
|
|
6959
|
+
const existing = getSlackWorkspace(teamId);
|
|
6960
|
+
const kind = tokenKind(token);
|
|
6961
|
+
const next = {
|
|
6962
|
+
team_id: teamId,
|
|
6963
|
+
team_name: auth.team?.trim() || existing?.team_name || teamId,
|
|
6964
|
+
user_id: auth.user_id?.trim() || existing?.user_id,
|
|
6965
|
+
bot_token: kind === "bot" ? token : existing?.bot_token,
|
|
6966
|
+
user_token: kind === "user" || kind === "unknown" ? token : existing?.user_token,
|
|
6967
|
+
scopes: existing?.scopes,
|
|
6968
|
+
connected_at: existing?.connected_at || (/* @__PURE__ */ new Date()).toISOString()
|
|
6969
|
+
};
|
|
6970
|
+
if (kind === "unknown" && auth.bot_id) {
|
|
6971
|
+
next.bot_token = token;
|
|
6972
|
+
next.user_token = existing?.user_token;
|
|
6973
|
+
}
|
|
6974
|
+
return upsertSlackWorkspace(next);
|
|
6975
|
+
}
|
|
6976
|
+
function slackTokenFor(ws, kind = "read") {
|
|
6977
|
+
if (kind === "search") {
|
|
6978
|
+
const token2 = ws.user_token?.trim();
|
|
6979
|
+
if (!token2) {
|
|
6980
|
+
throw new Error(
|
|
6981
|
+
`Slack search needs a user token for ${ws.team_name}. Reconnect via Account \u2192 Slack (browser) or paste an xoxp- token.`
|
|
6982
|
+
);
|
|
6983
|
+
}
|
|
6984
|
+
return token2;
|
|
6985
|
+
}
|
|
6986
|
+
const token = (kind === "write" ? ws.bot_token || ws.user_token : ws.user_token || ws.bot_token)?.trim();
|
|
6987
|
+
if (!token) {
|
|
6988
|
+
throw new Error(`Slack workspace ${ws.team_name} has no token`);
|
|
6989
|
+
}
|
|
6990
|
+
return token;
|
|
6991
|
+
}
|
|
6992
|
+
function requireSlackWorkspace(teamId) {
|
|
6993
|
+
const ws = getSlackWorkspace(teamId);
|
|
6994
|
+
if (!ws) {
|
|
6995
|
+
const connected = listSlackWorkspaces();
|
|
6996
|
+
const hint = connected.length === 0 ? "Connect a workspace in Account \u2192 Slack workspaces." : `Connected: ${connected.map((t) => `${t.team_name} (${t.team_id})`).join(", ")}`;
|
|
6997
|
+
throw new Error(`Unknown Slack team_id "${teamId}". ${hint}`);
|
|
6998
|
+
}
|
|
6999
|
+
return ws;
|
|
7000
|
+
}
|
|
7001
|
+
|
|
7002
|
+
// src/slack/outbound-watch.ts
|
|
7003
|
+
var MAX_WATCHES = 40;
|
|
7004
|
+
var WATCH_TTL_MS = 7 * 24 * 60 * 60 * 1e3;
|
|
7005
|
+
var POLL_INTERVAL_MS = 12e3;
|
|
7006
|
+
var lastPollMs = 0;
|
|
7007
|
+
var nameCache = /* @__PURE__ */ new Map();
|
|
7008
|
+
function storePath2() {
|
|
7009
|
+
return join12(appDataDir(), "slack-outbound-watch.json");
|
|
7010
|
+
}
|
|
7011
|
+
function watchId(teamId, channelId, ts) {
|
|
7012
|
+
return `${teamId}:${channelId}:${ts}`;
|
|
7013
|
+
}
|
|
7014
|
+
function badgeId(teamId, userId) {
|
|
7015
|
+
return `${teamId}:${userId}`;
|
|
7016
|
+
}
|
|
7017
|
+
function slackArchiveUrl(channelId, ts) {
|
|
7018
|
+
return `https://slack.com/archives/${channelId}/p${ts.replace(".", "")}`;
|
|
7019
|
+
}
|
|
7020
|
+
function initialsFromName(name) {
|
|
7021
|
+
const parts = name.trim().split(/\s+/).filter(Boolean);
|
|
7022
|
+
if (parts.length === 0) return "?";
|
|
7023
|
+
if (parts.length === 1) {
|
|
7024
|
+
const w = parts[0];
|
|
7025
|
+
return (w.slice(0, 2) || "?").toUpperCase();
|
|
7026
|
+
}
|
|
7027
|
+
return `${parts[0][0] ?? ""}${parts[parts.length - 1][0] ?? ""}`.toUpperCase();
|
|
7028
|
+
}
|
|
7029
|
+
function hueFromId(id) {
|
|
7030
|
+
let h = 0;
|
|
7031
|
+
for (const c of id) h = h * 31 + c.charCodeAt(0) >>> 0;
|
|
7032
|
+
return h % 360;
|
|
7033
|
+
}
|
|
7034
|
+
function tsNewer(a, b) {
|
|
7035
|
+
return Number(a) > Number(b);
|
|
7036
|
+
}
|
|
7037
|
+
function readStore2() {
|
|
7038
|
+
const path = storePath2();
|
|
7039
|
+
if (!existsSync14(path)) return [];
|
|
7040
|
+
try {
|
|
7041
|
+
const parsed = isSecureFileEncrypted(path) ? readSecureJson(path) : JSON.parse(readFileSync9(path, "utf8"));
|
|
7042
|
+
return Array.isArray(parsed?.watches) ? parsed.watches : [];
|
|
7043
|
+
} catch {
|
|
7044
|
+
return [];
|
|
7045
|
+
}
|
|
7046
|
+
}
|
|
7047
|
+
function writeStore2(watches) {
|
|
7048
|
+
writePrivateFile(storePath2(), `${JSON.stringify({ watches }, null, 2)}
|
|
7049
|
+
`);
|
|
7050
|
+
return watches;
|
|
7051
|
+
}
|
|
7052
|
+
function pruneWatches(watches, nowMs = Date.now()) {
|
|
7053
|
+
const cutoff = nowMs - WATCH_TTL_MS;
|
|
7054
|
+
const kept = watches.filter((w) => {
|
|
7055
|
+
const posted = Date.parse(w.postedAt);
|
|
7056
|
+
return Number.isFinite(posted) ? posted >= cutoff : true;
|
|
7057
|
+
});
|
|
7058
|
+
if (kept.length <= MAX_WATCHES) return kept;
|
|
7059
|
+
return kept.slice().sort((a, b) => b.postedAt.localeCompare(a.postedAt)).slice(0, MAX_WATCHES);
|
|
7060
|
+
}
|
|
7061
|
+
function recordSlackOutboundWatch(input) {
|
|
7062
|
+
const ts = input.ts.trim();
|
|
7063
|
+
const channelId = input.channelId.trim();
|
|
7064
|
+
const teamId = input.teamId.trim();
|
|
7065
|
+
if (!ts || !channelId || !teamId) return null;
|
|
7066
|
+
const owner = input.ownerUserId?.trim();
|
|
7067
|
+
const toUser = input.toUserId?.trim();
|
|
7068
|
+
if (toUser && owner && toUser === owner) return null;
|
|
7069
|
+
const id = watchId(teamId, channelId, ts);
|
|
7070
|
+
const next = {
|
|
7071
|
+
id,
|
|
7072
|
+
teamId,
|
|
7073
|
+
channelId,
|
|
7074
|
+
ts,
|
|
7075
|
+
threadTs: input.threadTs?.trim() || ts,
|
|
7076
|
+
kind: input.kind === "dm" ? "dm" : "channel",
|
|
7077
|
+
toUserId: toUser,
|
|
7078
|
+
toLabel: input.toLabel.trim() || toUser || channelId,
|
|
7079
|
+
ownerUserId: owner,
|
|
7080
|
+
postedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
7081
|
+
lastSeenTs: ts,
|
|
7082
|
+
unread: false,
|
|
7083
|
+
permalink: slackArchiveUrl(channelId, ts)
|
|
7084
|
+
};
|
|
7085
|
+
const watches = pruneWatches(readStore2().filter((w) => w.id !== id));
|
|
7086
|
+
watches.unshift(next);
|
|
7087
|
+
writeStore2(pruneWatches(watches));
|
|
7088
|
+
return next;
|
|
7089
|
+
}
|
|
7090
|
+
function isHumanReply(msg, watch) {
|
|
7091
|
+
const ts = msg.ts?.trim();
|
|
7092
|
+
if (!ts || ts === watch.ts) return false;
|
|
7093
|
+
if (!tsNewer(ts, watch.lastSeenTs)) return false;
|
|
7094
|
+
if (msg.bot_id) return false;
|
|
7095
|
+
if (msg.subtype) return false;
|
|
7096
|
+
const user = msg.user?.trim();
|
|
7097
|
+
if (!user) return false;
|
|
7098
|
+
if (watch.ownerUserId && user === watch.ownerUserId) return false;
|
|
7099
|
+
return true;
|
|
7100
|
+
}
|
|
7101
|
+
function displayName(user) {
|
|
7102
|
+
const fromProfile = user.profile?.display_name?.trim() || user.profile?.real_name?.trim();
|
|
7103
|
+
return fromProfile || user.real_name?.trim() || user.name?.trim() || "";
|
|
7104
|
+
}
|
|
7105
|
+
async function resolveUserName(token, userId, fallback, fetchImpl) {
|
|
7106
|
+
const cached = nameCache.get(userId);
|
|
7107
|
+
if (cached) return cached;
|
|
7108
|
+
try {
|
|
7109
|
+
const data = await slackApi(token, "users.info", { user: userId }, fetchImpl);
|
|
7110
|
+
const name = displayName(data.user ?? {}) || fallback;
|
|
7111
|
+
nameCache.set(userId, name);
|
|
7112
|
+
return name;
|
|
7113
|
+
} catch {
|
|
7114
|
+
return fallback;
|
|
7115
|
+
}
|
|
7116
|
+
}
|
|
7117
|
+
async function resolvePermalink(token, channelId, ts, fetchImpl) {
|
|
7118
|
+
try {
|
|
7119
|
+
const data = await slackApi(
|
|
7120
|
+
token,
|
|
7121
|
+
"chat.getPermalink",
|
|
7122
|
+
{ channel: channelId, message_ts: ts },
|
|
7123
|
+
fetchImpl
|
|
7124
|
+
);
|
|
7125
|
+
if (data.permalink?.startsWith("http")) return data.permalink;
|
|
7126
|
+
} catch {
|
|
7127
|
+
}
|
|
7128
|
+
return slackArchiveUrl(channelId, ts);
|
|
7129
|
+
}
|
|
7130
|
+
async function fetchMessages(token, watch, fetchImpl) {
|
|
7131
|
+
const out = [];
|
|
7132
|
+
try {
|
|
7133
|
+
const data = await slackApi(
|
|
7134
|
+
token,
|
|
7135
|
+
"conversations.replies",
|
|
7136
|
+
{
|
|
7137
|
+
channel: watch.channelId,
|
|
7138
|
+
ts: watch.threadTs,
|
|
7139
|
+
oldest: watch.lastSeenTs,
|
|
7140
|
+
inclusive: false,
|
|
7141
|
+
limit: 50
|
|
7142
|
+
},
|
|
7143
|
+
fetchImpl
|
|
7144
|
+
);
|
|
7145
|
+
out.push(...data.messages ?? []);
|
|
7146
|
+
} catch {
|
|
7147
|
+
}
|
|
7148
|
+
if (watch.kind === "dm" || watch.channelId.startsWith("D")) {
|
|
7149
|
+
try {
|
|
7150
|
+
const data = await slackApi(
|
|
7151
|
+
token,
|
|
7152
|
+
"conversations.history",
|
|
7153
|
+
{
|
|
7154
|
+
channel: watch.channelId,
|
|
7155
|
+
oldest: watch.lastSeenTs,
|
|
7156
|
+
inclusive: false,
|
|
7157
|
+
limit: 50
|
|
7158
|
+
},
|
|
7159
|
+
fetchImpl
|
|
7160
|
+
);
|
|
7161
|
+
out.push(...data.messages ?? []);
|
|
7162
|
+
} catch {
|
|
7163
|
+
}
|
|
7164
|
+
}
|
|
7165
|
+
return out;
|
|
7166
|
+
}
|
|
7167
|
+
function listSlackReplyBadges() {
|
|
7168
|
+
const unread = readStore2().filter((w) => w.unread && w.replyUserId && w.permalink);
|
|
7169
|
+
const byUser = /* @__PURE__ */ new Map();
|
|
7170
|
+
for (const w of unread) {
|
|
7171
|
+
const id = badgeId(w.teamId, w.replyUserId);
|
|
7172
|
+
const prev = byUser.get(id);
|
|
7173
|
+
if (!prev || tsNewer(w.replyTs || "", prev.replyTs || "")) {
|
|
7174
|
+
byUser.set(id, w);
|
|
7175
|
+
}
|
|
7176
|
+
}
|
|
7177
|
+
return [...byUser.entries()].map(([id, w]) => {
|
|
7178
|
+
const userName = w.replyUserName || w.toLabel || "Slack";
|
|
7179
|
+
return {
|
|
7180
|
+
id,
|
|
7181
|
+
userId: w.replyUserId,
|
|
7182
|
+
userName,
|
|
7183
|
+
initials: initialsFromName(userName),
|
|
7184
|
+
hue: hueFromId(w.replyUserId),
|
|
7185
|
+
permalink: w.permalink || slackArchiveUrl(w.channelId, w.replyTs || w.ts),
|
|
7186
|
+
label: w.toLabel,
|
|
7187
|
+
preview: w.replyPreview,
|
|
7188
|
+
repliedAt: w.replyTs || w.postedAt
|
|
7189
|
+
};
|
|
7190
|
+
}).sort((a, b) => b.repliedAt.localeCompare(a.repliedAt));
|
|
7191
|
+
}
|
|
7192
|
+
function dismissSlackReplyBadge(badgeKey) {
|
|
7193
|
+
const key = badgeKey.trim();
|
|
7194
|
+
const watches = readStore2().map((w) => {
|
|
7195
|
+
if (!w.unread || !w.replyUserId) return w;
|
|
7196
|
+
if (badgeId(w.teamId, w.replyUserId) !== key) return w;
|
|
7197
|
+
return { ...w, unread: false };
|
|
7198
|
+
});
|
|
7199
|
+
writeStore2(watches);
|
|
7200
|
+
return listSlackReplyBadges();
|
|
7201
|
+
}
|
|
7202
|
+
function permalinkForSlackReplyBadge(badgeKey) {
|
|
7203
|
+
return listSlackReplyBadges().find((b) => b.id === badgeKey)?.permalink ?? null;
|
|
7204
|
+
}
|
|
7205
|
+
async function refreshSlackReplyBadges(opts) {
|
|
7206
|
+
const now = opts?.now ?? Date.now();
|
|
7207
|
+
if (!opts?.force && now - lastPollMs < POLL_INTERVAL_MS) {
|
|
7208
|
+
return listSlackReplyBadges();
|
|
7209
|
+
}
|
|
7210
|
+
lastPollMs = now;
|
|
7211
|
+
const existing = readStore2();
|
|
7212
|
+
let watches = pruneWatches(existing, now);
|
|
7213
|
+
let changed = watches.length !== existing.length;
|
|
7214
|
+
for (let i = 0; i < watches.length; i++) {
|
|
7215
|
+
const watch = watches[i];
|
|
7216
|
+
const ws = getSlackWorkspace(watch.teamId);
|
|
7217
|
+
if (!ws) continue;
|
|
7218
|
+
let token;
|
|
7219
|
+
try {
|
|
7220
|
+
token = slackTokenFor(ws, "read");
|
|
7221
|
+
} catch {
|
|
7222
|
+
continue;
|
|
7223
|
+
}
|
|
7224
|
+
const messages = await fetchMessages(token, watch, opts?.fetchImpl);
|
|
7225
|
+
const replies = messages.filter((m) => isHumanReply(m, watch)).sort((a, b) => Number(a.ts) - Number(b.ts));
|
|
7226
|
+
const latest = replies[replies.length - 1];
|
|
7227
|
+
if (!latest?.ts || !latest.user) continue;
|
|
7228
|
+
const fallback = watch.toUserId === latest.user ? watch.toLabel : latest.user;
|
|
7229
|
+
const replyUserName = await resolveUserName(
|
|
7230
|
+
token,
|
|
7231
|
+
latest.user,
|
|
7232
|
+
fallback,
|
|
7233
|
+
opts?.fetchImpl
|
|
7234
|
+
);
|
|
7235
|
+
const permalink = await resolvePermalink(
|
|
7236
|
+
token,
|
|
7237
|
+
watch.channelId,
|
|
7238
|
+
latest.ts,
|
|
7239
|
+
opts?.fetchImpl
|
|
7240
|
+
);
|
|
7241
|
+
watches[i] = {
|
|
7242
|
+
...watch,
|
|
7243
|
+
lastSeenTs: latest.ts,
|
|
7244
|
+
unread: true,
|
|
7245
|
+
replyUserId: latest.user,
|
|
7246
|
+
replyUserName,
|
|
7247
|
+
replyTs: latest.ts,
|
|
7248
|
+
replyPreview: (latest.text ?? "").slice(0, 140),
|
|
7249
|
+
permalink
|
|
7250
|
+
};
|
|
7251
|
+
changed = true;
|
|
7252
|
+
}
|
|
7253
|
+
if (changed) writeStore2(watches);
|
|
7254
|
+
return listSlackReplyBadges();
|
|
7255
|
+
}
|
|
7256
|
+
|
|
7257
|
+
// src/mcp/slack-tools.ts
|
|
7258
|
+
function text(payload, isError = false) {
|
|
7259
|
+
return {
|
|
7260
|
+
content: [{ type: "text", text: JSON.stringify(payload, null, 2) }],
|
|
7261
|
+
...isError ? { isError: true } : {}
|
|
7262
|
+
};
|
|
7263
|
+
}
|
|
7264
|
+
function fail(err) {
|
|
7265
|
+
return text(
|
|
7266
|
+
{ error: err instanceof Error ? err.message : String(err) },
|
|
7267
|
+
true
|
|
7268
|
+
);
|
|
7269
|
+
}
|
|
7270
|
+
function registerSlackTools(server) {
|
|
7271
|
+
server.tool(
|
|
7272
|
+
"list_teams",
|
|
7273
|
+
"List Slack workspaces connected in Sideboard Account settings. Each row is team_id + name. Pass team_id to slack_list_channels, slack_list_users, slack_search, slack_read, and slack_post.",
|
|
7274
|
+
{},
|
|
7275
|
+
async () => {
|
|
7276
|
+
const teams = listSlackWorkspaces();
|
|
7277
|
+
if (teams.length === 0) {
|
|
7278
|
+
return text({
|
|
7279
|
+
teams: [],
|
|
7280
|
+
hint: "No Slack workspaces connected. Add one in Account \u2192 Slack workspaces (paste xoxb-/xoxp- or browser sign-in)."
|
|
7281
|
+
});
|
|
7282
|
+
}
|
|
7283
|
+
return text({
|
|
7284
|
+
teams: teams.map((t) => ({
|
|
7285
|
+
team_id: t.team_id,
|
|
7286
|
+
name: t.team_name,
|
|
7287
|
+
search: t.has_user_token
|
|
7288
|
+
}))
|
|
7289
|
+
});
|
|
6508
7290
|
}
|
|
6509
7291
|
);
|
|
6510
7292
|
server.tool(
|
|
6511
|
-
"
|
|
6512
|
-
"
|
|
7293
|
+
"slack_list_channels",
|
|
7294
|
+
"List channels in a connected Slack workspace. Pass team_id from list_teams.",
|
|
6513
7295
|
{
|
|
6514
|
-
|
|
6515
|
-
|
|
6516
|
-
datasource: z.enum(["brightsy", "inline"]).optional().describe("brightsy resolves via login; inline uses embedded resource/records"),
|
|
6517
|
-
resource_id: z.string().optional().describe("Brightsy record type UUID (or generic resource id)"),
|
|
6518
|
-
record_id: z.string().optional().describe("Record id when opening form mode"),
|
|
6519
|
-
resource: z.record(z.unknown()).optional().describe("Inline { id, title, schema, schemaUi?, slug? }"),
|
|
6520
|
-
record: z.record(z.unknown()).optional().describe("Inline record { id, data, published_at? }"),
|
|
6521
|
-
records: z.array(z.record(z.unknown())).optional().describe("Inline records for table mode"),
|
|
6522
|
-
pane_id: z.string().optional().describe("Stable pane id across updates")
|
|
7296
|
+
team_id: z.string(),
|
|
7297
|
+
limit: z.number().optional()
|
|
6523
7298
|
},
|
|
6524
|
-
async (
|
|
6525
|
-
|
|
6526
|
-
|
|
6527
|
-
|
|
6528
|
-
|
|
6529
|
-
|
|
6530
|
-
|
|
6531
|
-
|
|
6532
|
-
|
|
6533
|
-
|
|
6534
|
-
|
|
6535
|
-
|
|
6536
|
-
|
|
7299
|
+
async ({ team_id, limit }) => {
|
|
7300
|
+
try {
|
|
7301
|
+
const ws = requireSlackWorkspace(team_id);
|
|
7302
|
+
const token = slackTokenFor(ws, "read");
|
|
7303
|
+
const data = await slackApi(token, "conversations.list", {
|
|
7304
|
+
types: "public_channel,private_channel",
|
|
7305
|
+
exclude_archived: true,
|
|
7306
|
+
limit: Math.min(200, Math.max(1, limit ?? 100))
|
|
7307
|
+
});
|
|
7308
|
+
return text({
|
|
7309
|
+
team_id: ws.team_id,
|
|
7310
|
+
team_name: ws.team_name,
|
|
7311
|
+
channels: (data.channels ?? []).map((c) => ({
|
|
7312
|
+
id: c.id,
|
|
7313
|
+
name: c.name,
|
|
7314
|
+
private: Boolean(c.is_private),
|
|
7315
|
+
member: Boolean(c.is_member)
|
|
7316
|
+
}))
|
|
7317
|
+
});
|
|
7318
|
+
} catch (err) {
|
|
7319
|
+
return fail(err);
|
|
7320
|
+
}
|
|
6537
7321
|
}
|
|
6538
7322
|
);
|
|
6539
7323
|
server.tool(
|
|
6540
|
-
"
|
|
6541
|
-
"
|
|
7324
|
+
"slack_list_users",
|
|
7325
|
+
"List or search people in a connected Slack workspace (for DMs / @mentions). Pass team_id from list_teams. Optional query matches name, display name, real name, or email.",
|
|
6542
7326
|
{
|
|
6543
|
-
|
|
6544
|
-
|
|
6545
|
-
|
|
6546
|
-
pane_id: z.string().optional().describe("Stable pane id across updates")
|
|
7327
|
+
team_id: z.string(),
|
|
7328
|
+
query: z.string().optional(),
|
|
7329
|
+
limit: z.number().optional()
|
|
6547
7330
|
},
|
|
6548
|
-
async (
|
|
6549
|
-
|
|
6550
|
-
|
|
6551
|
-
|
|
6552
|
-
|
|
6553
|
-
|
|
6554
|
-
|
|
6555
|
-
|
|
6556
|
-
|
|
6557
|
-
|
|
6558
|
-
|
|
7331
|
+
async ({ team_id, query, limit }) => {
|
|
7332
|
+
try {
|
|
7333
|
+
const ws = requireSlackWorkspace(team_id);
|
|
7334
|
+
const token = slackTokenFor(ws, "read");
|
|
7335
|
+
const users = await listSlackUsers(token, { query, limit });
|
|
7336
|
+
return text({
|
|
7337
|
+
team_id: ws.team_id,
|
|
7338
|
+
team_name: ws.team_name,
|
|
7339
|
+
query: query?.trim() || void 0,
|
|
7340
|
+
users
|
|
7341
|
+
});
|
|
7342
|
+
} catch (err) {
|
|
7343
|
+
return fail(err);
|
|
7344
|
+
}
|
|
6559
7345
|
}
|
|
6560
7346
|
);
|
|
6561
|
-
|
|
6562
|
-
|
|
6563
|
-
|
|
6564
|
-
|
|
6565
|
-
|
|
6566
|
-
|
|
6567
|
-
|
|
6568
|
-
|
|
6569
|
-
|
|
6570
|
-
|
|
6571
|
-
|
|
6572
|
-
|
|
6573
|
-
|
|
6574
|
-
|
|
6575
|
-
|
|
6576
|
-
|
|
6577
|
-
|
|
6578
|
-
|
|
6579
|
-
|
|
6580
|
-
|
|
6581
|
-
|
|
6582
|
-
|
|
6583
|
-
|
|
6584
|
-
|
|
6585
|
-
|
|
6586
|
-
|
|
6587
|
-
|
|
6588
|
-
|
|
6589
|
-
|
|
6590
|
-
|
|
6591
|
-
|
|
6592
|
-
|
|
6593
|
-
|
|
7347
|
+
server.tool(
|
|
7348
|
+
"slack_search",
|
|
7349
|
+
"Search messages in a connected Slack workspace (needs a user token). Pass team_id from list_teams.",
|
|
7350
|
+
{
|
|
7351
|
+
team_id: z.string(),
|
|
7352
|
+
query: z.string(),
|
|
7353
|
+
count: z.number().optional()
|
|
7354
|
+
},
|
|
7355
|
+
async ({ team_id, query, count }) => {
|
|
7356
|
+
try {
|
|
7357
|
+
const ws = requireSlackWorkspace(team_id);
|
|
7358
|
+
const token = slackTokenFor(ws, "search");
|
|
7359
|
+
const data = await slackApi(token, "search.messages", {
|
|
7360
|
+
query,
|
|
7361
|
+
count: Math.min(50, Math.max(1, count ?? 10)),
|
|
7362
|
+
sort: "timestamp"
|
|
7363
|
+
});
|
|
7364
|
+
return text({
|
|
7365
|
+
team_id: ws.team_id,
|
|
7366
|
+
team_name: ws.team_name,
|
|
7367
|
+
query,
|
|
7368
|
+
matches: (data.messages?.matches ?? []).map((m) => ({
|
|
7369
|
+
channel: m.channel?.name || m.channel?.id,
|
|
7370
|
+
user: m.username || m.user,
|
|
7371
|
+
ts: m.ts,
|
|
7372
|
+
text: m.text,
|
|
7373
|
+
permalink: m.permalink
|
|
7374
|
+
}))
|
|
7375
|
+
});
|
|
7376
|
+
} catch (err) {
|
|
7377
|
+
return fail(err);
|
|
7378
|
+
}
|
|
7379
|
+
}
|
|
7380
|
+
);
|
|
7381
|
+
server.tool(
|
|
7382
|
+
"slack_read",
|
|
7383
|
+
"Read recent messages in a Slack channel or DM, or a thread when ts is set. Pass team_id from list_teams and #channel, @user, or a C\u2026/D\u2026/U\u2026 id.",
|
|
7384
|
+
{
|
|
7385
|
+
team_id: z.string(),
|
|
7386
|
+
channel: z.string(),
|
|
7387
|
+
ts: z.string().optional(),
|
|
7388
|
+
limit: z.number().optional()
|
|
7389
|
+
},
|
|
7390
|
+
async ({ team_id, channel, ts, limit }) => {
|
|
7391
|
+
try {
|
|
7392
|
+
const ws = requireSlackWorkspace(team_id);
|
|
7393
|
+
const token = slackTokenFor(ws, "read");
|
|
7394
|
+
const dest = await resolveSlackDestination(token, channel, { forRead: true });
|
|
7395
|
+
const n = Math.min(100, Math.max(1, limit ?? 20));
|
|
7396
|
+
if (ts?.trim()) {
|
|
7397
|
+
const data2 = await slackApi(token, "conversations.replies", {
|
|
7398
|
+
channel: dest.channelId,
|
|
7399
|
+
ts: ts.trim(),
|
|
7400
|
+
limit: n
|
|
7401
|
+
});
|
|
7402
|
+
return text({
|
|
7403
|
+
team_id: ws.team_id,
|
|
7404
|
+
channel: dest.channelId,
|
|
7405
|
+
label: dest.label,
|
|
7406
|
+
thread_ts: ts.trim(),
|
|
7407
|
+
messages: data2.messages ?? []
|
|
7408
|
+
});
|
|
6594
7409
|
}
|
|
6595
|
-
|
|
6596
|
-
|
|
6597
|
-
|
|
6598
|
-
|
|
7410
|
+
const data = await slackApi(token, "conversations.history", {
|
|
7411
|
+
channel: dest.channelId,
|
|
7412
|
+
limit: n
|
|
7413
|
+
});
|
|
7414
|
+
return text({
|
|
7415
|
+
team_id: ws.team_id,
|
|
7416
|
+
channel: dest.channelId,
|
|
7417
|
+
label: dest.label,
|
|
7418
|
+
messages: data.messages ?? []
|
|
7419
|
+
});
|
|
7420
|
+
} catch (err) {
|
|
7421
|
+
return fail(err);
|
|
7422
|
+
}
|
|
7423
|
+
}
|
|
7424
|
+
);
|
|
7425
|
+
server.tool(
|
|
7426
|
+
"slack_post",
|
|
7427
|
+
"Post a message to a Slack channel or DM (as the Sideboard bot). Pass team_id from list_teams. Use to or channel for #name, @user, or C\u2026/D\u2026/U\u2026 ids. Optional github_url appends a PR / code / comment link. Only notify when the user asks. Thread with thread_ts when set.",
|
|
7428
|
+
{
|
|
7429
|
+
team_id: z.string(),
|
|
7430
|
+
channel: z.string().optional(),
|
|
7431
|
+
to: z.string().optional(),
|
|
7432
|
+
text: z.string(),
|
|
7433
|
+
github_url: z.string().optional(),
|
|
7434
|
+
thread_ts: z.string().optional()
|
|
7435
|
+
},
|
|
7436
|
+
async ({ team_id, channel, to, text: message, github_url, thread_ts }) => {
|
|
7437
|
+
try {
|
|
7438
|
+
const destRaw = (to ?? channel)?.trim();
|
|
7439
|
+
if (!destRaw) {
|
|
7440
|
+
return fail(new Error("slack_post needs `to` or `channel` (#name, @user, or Slack id)"));
|
|
6599
7441
|
}
|
|
6600
|
-
|
|
6601
|
-
|
|
6602
|
-
|
|
6603
|
-
|
|
6604
|
-
|
|
6605
|
-
|
|
6606
|
-
|
|
6607
|
-
|
|
6608
|
-
|
|
6609
|
-
|
|
6610
|
-
|
|
6611
|
-
|
|
7442
|
+
const ws = requireSlackWorkspace(team_id);
|
|
7443
|
+
const token = slackTokenFor(ws, "write");
|
|
7444
|
+
const dest = await resolveSlackDestination(token, destRaw);
|
|
7445
|
+
const body = appendGithubLink(message, github_url);
|
|
7446
|
+
const data = await slackApi(
|
|
7447
|
+
token,
|
|
7448
|
+
"chat.postMessage",
|
|
7449
|
+
{
|
|
7450
|
+
channel: dest.channelId,
|
|
7451
|
+
text: body,
|
|
7452
|
+
thread_ts: thread_ts?.trim() || void 0,
|
|
7453
|
+
unfurl_links: true,
|
|
7454
|
+
unfurl_media: true
|
|
7455
|
+
}
|
|
7456
|
+
);
|
|
7457
|
+
const postedTs = data.ts?.trim();
|
|
7458
|
+
const postedChannel = (data.channel || dest.channelId).trim();
|
|
7459
|
+
if (postedTs && postedChannel) {
|
|
7460
|
+
try {
|
|
7461
|
+
recordSlackOutboundWatch({
|
|
7462
|
+
teamId: ws.team_id,
|
|
7463
|
+
channelId: postedChannel,
|
|
7464
|
+
ts: postedTs,
|
|
7465
|
+
threadTs: thread_ts?.trim() || postedTs,
|
|
7466
|
+
kind: dest.kind === "channel" ? "channel" : "dm",
|
|
7467
|
+
toUserId: dest.userId,
|
|
7468
|
+
toLabel: dest.label,
|
|
7469
|
+
ownerUserId: ws.user_id
|
|
7470
|
+
});
|
|
7471
|
+
} catch {
|
|
6612
7472
|
}
|
|
6613
7473
|
}
|
|
6614
|
-
|
|
6615
|
-
|
|
6616
|
-
|
|
6617
|
-
|
|
6618
|
-
|
|
6619
|
-
|
|
6620
|
-
|
|
6621
|
-
|
|
6622
|
-
const opts = resolveNewThreadOptions2({
|
|
6623
|
-
agent: agentArg,
|
|
6624
|
-
model: args.model
|
|
7474
|
+
return text({
|
|
7475
|
+
ok: true,
|
|
7476
|
+
team_id: ws.team_id,
|
|
7477
|
+
channel: postedChannel,
|
|
7478
|
+
label: dest.label,
|
|
7479
|
+
kind: dest.kind,
|
|
7480
|
+
user_id: dest.userId,
|
|
7481
|
+
ts: postedTs
|
|
6625
7482
|
});
|
|
6626
|
-
|
|
6627
|
-
|
|
6628
|
-
|
|
6629
|
-
|
|
6630
|
-
|
|
6631
|
-
|
|
6632
|
-
|
|
6633
|
-
|
|
6634
|
-
|
|
6635
|
-
|
|
6636
|
-
|
|
6637
|
-
|
|
6638
|
-
|
|
6639
|
-
|
|
6640
|
-
|
|
6641
|
-
|
|
6642
|
-
|
|
6643
|
-
|
|
6644
|
-
|
|
6645
|
-
|
|
6646
|
-
|
|
6647
|
-
|
|
6648
|
-
|
|
6649
|
-
|
|
6650
|
-
|
|
6651
|
-
|
|
6652
|
-
|
|
6653
|
-
|
|
6654
|
-
|
|
6655
|
-
|
|
6656
|
-
|
|
6657
|
-
|
|
6658
|
-
|
|
6659
|
-
|
|
6660
|
-
|
|
6661
|
-
|
|
6662
|
-
|
|
6663
|
-
|
|
6664
|
-
|
|
6665
|
-
|
|
6666
|
-
|
|
6667
|
-
|
|
7483
|
+
} catch (err) {
|
|
7484
|
+
return fail(err);
|
|
7485
|
+
}
|
|
7486
|
+
}
|
|
7487
|
+
);
|
|
7488
|
+
}
|
|
7489
|
+
|
|
7490
|
+
// src/mcp/server.ts
|
|
7491
|
+
var MAX_ORCH_THREADS = 5;
|
|
7492
|
+
var CREATE_THREAD_TIMEOUT_MS = 9e4;
|
|
7493
|
+
function withTimeout(promise, ms, label) {
|
|
7494
|
+
return new Promise((resolve, reject) => {
|
|
7495
|
+
const timer = setTimeout(() => {
|
|
7496
|
+
reject(new Error(`${label} timed out after ${Math.round(ms / 1e3)}s`));
|
|
7497
|
+
}, ms);
|
|
7498
|
+
promise.then(
|
|
7499
|
+
(value) => {
|
|
7500
|
+
clearTimeout(timer);
|
|
7501
|
+
resolve(value);
|
|
7502
|
+
},
|
|
7503
|
+
(err) => {
|
|
7504
|
+
clearTimeout(timer);
|
|
7505
|
+
reject(err);
|
|
7506
|
+
}
|
|
7507
|
+
);
|
|
7508
|
+
});
|
|
7509
|
+
}
|
|
7510
|
+
async function startMcpServer() {
|
|
7511
|
+
const orch = getOrchestrator();
|
|
7512
|
+
try {
|
|
7513
|
+
const { maxConcurrentAgents: maxConcurrentAgents2 } = await import("./app-settings-3TLA2EJ7.js");
|
|
7514
|
+
orch.setMaxConcurrent(maxConcurrentAgents2());
|
|
7515
|
+
} catch {
|
|
7516
|
+
}
|
|
7517
|
+
try {
|
|
7518
|
+
await orch.reconcile(void 0, { reclaimStaleTurns: false, drainQueues: false });
|
|
7519
|
+
} catch (err) {
|
|
7520
|
+
console.error(
|
|
7521
|
+
"[sideboard-mcp] reconcile on boot failed (continuing):",
|
|
7522
|
+
err instanceof Error ? err.message : err
|
|
7523
|
+
);
|
|
7524
|
+
}
|
|
7525
|
+
const server = new McpServer({
|
|
7526
|
+
name: "sideboard",
|
|
7527
|
+
version: "0.1.0"
|
|
7528
|
+
});
|
|
7529
|
+
server.tool(
|
|
7530
|
+
"list_workspaces",
|
|
7531
|
+
"List registered Sideboard workspaces (repos). Each line is name, path, and github:owner/repo when resolvable \u2014 use path as repoPath for list_branches/list_prs/list_issues/create_thread.",
|
|
7532
|
+
{},
|
|
7533
|
+
async () => {
|
|
7534
|
+
const workspaces = orch.listWorkspaces();
|
|
7535
|
+
const lines = await Promise.all(
|
|
7536
|
+
workspaces.map(async (w) => {
|
|
7537
|
+
const slug = await resolveGithubRepoSlug(w.path).catch(() => null);
|
|
7538
|
+
return slug ? `${w.name} ${w.path} github:${slug}` : `${w.name} ${w.path}`;
|
|
7539
|
+
})
|
|
7540
|
+
);
|
|
7541
|
+
return {
|
|
7542
|
+
content: [
|
|
7543
|
+
{ type: "text", text: lines.join("\n") || "(no workspaces)" }
|
|
7544
|
+
]
|
|
7545
|
+
};
|
|
7546
|
+
}
|
|
7547
|
+
);
|
|
7548
|
+
server.tool(
|
|
7549
|
+
"list_threads",
|
|
7550
|
+
"List Sideboard threads across all workspaces (one summary line each \u2014 token-frugal). Each line ends with sideboard://thread/<id> \u2014 use that URL in markdown links so the UI can open the chat.",
|
|
7551
|
+
{},
|
|
7552
|
+
async () => {
|
|
7553
|
+
const threads = orch.getThreads(true);
|
|
7554
|
+
const lines = threads.map((t) => {
|
|
7555
|
+
const repo = t.repoPath === GLOBAL_WORKSPACE_ID ? "Orchestration" : basename4(t.repoPath) || t.repoPath;
|
|
7556
|
+
return `${t.id.slice(0, 8)} ${t.status.padEnd(9)} ${t.agent.padEnd(8)} ${repo} ${t.sourceType}:${t.sourceRef} ${t.title} sideboard://thread/${t.id}${t.devPort ? ` http://localhost:${t.devPort}` : ""}`;
|
|
7557
|
+
});
|
|
7558
|
+
return {
|
|
7559
|
+
content: [{ type: "text", text: lines.join("\n") || "(no threads)" }]
|
|
7560
|
+
};
|
|
7561
|
+
}
|
|
7562
|
+
);
|
|
7563
|
+
server.tool(
|
|
7564
|
+
"get_thread",
|
|
7565
|
+
"Get a compact thread summary by id/ref",
|
|
7566
|
+
{ ref: z2.string() },
|
|
7567
|
+
async ({ ref }) => {
|
|
7568
|
+
const t = orch.getThread(ref);
|
|
7569
|
+
if (!t) {
|
|
7570
|
+
return { content: [{ type: "text", text: `Thread not found: ${ref}` }], isError: true };
|
|
7571
|
+
}
|
|
7572
|
+
const summary = {
|
|
7573
|
+
id: t.id,
|
|
7574
|
+
title: t.title,
|
|
7575
|
+
status: t.status,
|
|
7576
|
+
agent: t.agent,
|
|
7577
|
+
sourceType: t.sourceType,
|
|
7578
|
+
sourceRef: t.sourceRef,
|
|
7579
|
+
branchName: t.branchName,
|
|
7580
|
+
worktreePath: t.worktreePath,
|
|
7581
|
+
sessionId: t.sessionId,
|
|
7582
|
+
queueLength: t.queue.length,
|
|
7583
|
+
messageCount: t.messages.length,
|
|
7584
|
+
devPort: t.devPort,
|
|
7585
|
+
prUrl: t.prUrl,
|
|
7586
|
+
lastError: t.lastError ?? null
|
|
7587
|
+
};
|
|
7588
|
+
return { content: [{ type: "text", text: JSON.stringify(summary, null, 2) }] };
|
|
7589
|
+
}
|
|
7590
|
+
);
|
|
7591
|
+
server.tool(
|
|
7592
|
+
"present_artifact",
|
|
7593
|
+
"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.",
|
|
7594
|
+
{
|
|
7595
|
+
title: z2.string().describe("Short title shown in the artifact pane header"),
|
|
7596
|
+
type: z2.enum(["html", "svg", "markdown", "react"]).describe(
|
|
7597
|
+
"Artifact kind \u2014 html opens an iframe preview; react transpiles and renders a default-exported component in a sandboxed iframe"
|
|
7598
|
+
),
|
|
7599
|
+
content: z2.string().describe(
|
|
7600
|
+
"Full document body (complete HTML page, SVG markup, markdown, or a React component module with a default export)"
|
|
7601
|
+
),
|
|
7602
|
+
artifact_id: z2.string().optional().describe("Stable id when updating the same artifact across turns")
|
|
7603
|
+
},
|
|
7604
|
+
async ({ title, type, artifact_id }) => {
|
|
7605
|
+
const id = artifact_id?.trim() || `artifact_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 8)}`;
|
|
7606
|
+
const payload = {
|
|
7607
|
+
ok: true,
|
|
7608
|
+
artifact_id: id,
|
|
7609
|
+
title,
|
|
7610
|
+
type,
|
|
7611
|
+
message: "Artifact accepted. Sideboard desktop opens it in the side column beside chat."
|
|
7612
|
+
};
|
|
7613
|
+
return { content: [{ type: "text", text: JSON.stringify(payload) }] };
|
|
7614
|
+
}
|
|
7615
|
+
);
|
|
7616
|
+
server.tool(
|
|
7617
|
+
"ask_user",
|
|
7618
|
+
"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.",
|
|
7619
|
+
{
|
|
7620
|
+
questions: z2.array(
|
|
7621
|
+
z2.object({
|
|
7622
|
+
question: z2.string().describe("Full question text ending with ?"),
|
|
7623
|
+
header: z2.string().max(24).optional().describe("Short label shown above the question"),
|
|
7624
|
+
multiSelect: z2.boolean().optional().describe("Allow selecting multiple options"),
|
|
7625
|
+
options: z2.array(
|
|
7626
|
+
z2.object({
|
|
7627
|
+
label: z2.string(),
|
|
7628
|
+
description: z2.string().optional().describe("What this option means / when to choose it (strongly preferred)")
|
|
7629
|
+
})
|
|
7630
|
+
).min(2).max(6).describe("2\u20136 choices (Sideboard also offers Other)")
|
|
7631
|
+
})
|
|
7632
|
+
).min(1).max(4).describe("1\u20134 questions")
|
|
7633
|
+
},
|
|
7634
|
+
async ({ questions }) => {
|
|
7635
|
+
const payload = {
|
|
7636
|
+
ok: true,
|
|
7637
|
+
questions,
|
|
7638
|
+
message: "Questions shown in Sideboard\u2019s composer. Wait for the user\u2019s next message with their answers before continuing."
|
|
7639
|
+
};
|
|
7640
|
+
return { content: [{ type: "text", text: JSON.stringify(payload) }] };
|
|
7641
|
+
}
|
|
7642
|
+
);
|
|
7643
|
+
server.tool(
|
|
7644
|
+
"present_plan",
|
|
7645
|
+
"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.",
|
|
7646
|
+
{
|
|
7647
|
+
title: z2.string().optional().describe("Short plan title (defaults to Plan)"),
|
|
7648
|
+
content: z2.string().min(1).describe("Full plan markdown (headings, steps, risks, open questions)"),
|
|
7649
|
+
thread_id: z2.string().optional().describe("Sideboard thread id when cwd is not the worktree")
|
|
7650
|
+
},
|
|
7651
|
+
async ({ title, content, thread_id }) => {
|
|
7652
|
+
const { writePlanFile: writePlanFile2 } = await import("./plan-file-6O7G4VPQ.js");
|
|
7653
|
+
let root = process.cwd();
|
|
7654
|
+
if (thread_id?.trim()) {
|
|
7655
|
+
const t = orch.getThread(thread_id.trim());
|
|
7656
|
+
if (t?.worktreePath?.trim()) root = t.worktreePath;
|
|
7657
|
+
}
|
|
7658
|
+
const path = writePlanFile2(root, content);
|
|
7659
|
+
const payload = {
|
|
7660
|
+
ok: true,
|
|
7661
|
+
path,
|
|
7662
|
+
title: title?.trim() || "Plan",
|
|
7663
|
+
message: "Plan saved to .context/attachments/plan.md and shown in Sideboard chat for approval."
|
|
7664
|
+
};
|
|
7665
|
+
return { content: [{ type: "text", text: JSON.stringify(payload) }] };
|
|
7666
|
+
}
|
|
7667
|
+
);
|
|
7668
|
+
server.tool(
|
|
7669
|
+
"present_schema",
|
|
7670
|
+
"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.",
|
|
7671
|
+
{
|
|
7672
|
+
title: z2.string().describe("Pane title"),
|
|
7673
|
+
mode: z2.enum(["table", "form"]).optional().describe("table = list/filter records; form = edit one record"),
|
|
7674
|
+
datasource: z2.enum(["brightsy", "inline"]).optional().describe("brightsy resolves via login; inline uses embedded resource/records"),
|
|
7675
|
+
resource_id: z2.string().optional().describe("Brightsy record type UUID (or generic resource id)"),
|
|
7676
|
+
record_id: z2.string().optional().describe("Record id when opening form mode"),
|
|
7677
|
+
resource: z2.record(z2.unknown()).optional().describe("Inline { id, title, schema, schemaUi?, slug? }"),
|
|
7678
|
+
record: z2.record(z2.unknown()).optional().describe("Inline record { id, data, published_at? }"),
|
|
7679
|
+
records: z2.array(z2.record(z2.unknown())).optional().describe("Inline records for table mode"),
|
|
7680
|
+
pane_id: z2.string().optional().describe("Stable pane id across updates")
|
|
7681
|
+
},
|
|
7682
|
+
async (args) => {
|
|
7683
|
+
const id = args.pane_id?.trim() || `schema_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 8)}`;
|
|
7684
|
+
const payload = {
|
|
7685
|
+
ok: true,
|
|
7686
|
+
pane_id: id,
|
|
7687
|
+
title: args.title,
|
|
7688
|
+
mode: args.mode ?? (args.record_id || args.record ? "form" : "table"),
|
|
7689
|
+
datasource: args.datasource ?? (args.resource ? "inline" : "brightsy"),
|
|
7690
|
+
resource_id: args.resource_id,
|
|
7691
|
+
record_id: args.record_id,
|
|
7692
|
+
message: "Schema pane accepted. Sideboard desktop opens the CMS column beside chat."
|
|
7693
|
+
};
|
|
7694
|
+
return { content: [{ type: "text", text: JSON.stringify(payload) }] };
|
|
7695
|
+
}
|
|
7696
|
+
);
|
|
7697
|
+
server.tool(
|
|
7698
|
+
"present_files",
|
|
7699
|
+
"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.",
|
|
7700
|
+
{
|
|
7701
|
+
title: z2.string().optional().describe("Pane title (default: Files)"),
|
|
7702
|
+
datasource: z2.enum(["brightsy", "memory"]).optional().describe("brightsy = account storage via login; memory = session demo store"),
|
|
7703
|
+
path: z2.string().optional().describe("Initial folder path (e.g. public)"),
|
|
7704
|
+
pane_id: z2.string().optional().describe("Stable pane id across updates")
|
|
7705
|
+
},
|
|
7706
|
+
async (args) => {
|
|
7707
|
+
const id = args.pane_id?.trim() || `files_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 8)}`;
|
|
7708
|
+
const payload = {
|
|
7709
|
+
ok: true,
|
|
7710
|
+
pane_id: id,
|
|
7711
|
+
title: args.title?.trim() || "Files",
|
|
7712
|
+
datasource: args.datasource ?? "brightsy",
|
|
7713
|
+
path: args.path,
|
|
7714
|
+
message: "Files pane accepted. Sideboard desktop opens the Files column beside chat."
|
|
7715
|
+
};
|
|
7716
|
+
return { content: [{ type: "text", text: JSON.stringify(payload) }] };
|
|
7717
|
+
}
|
|
7718
|
+
);
|
|
7719
|
+
registerSlackTools(server);
|
|
7720
|
+
if (sideboardMcpProfile() !== "worktree") {
|
|
7721
|
+
const { getCaffeinateHold: getCaffeinateHold2, setCaffeinateHold: setCaffeinateHold2 } = await import("./caffeinate-hold-KLC6SABD.js");
|
|
7722
|
+
const { resolveNewThreadOptions: resolveNewThreadOptions2, resolveThreadDefaults: resolveThreadDefaults2 } = await import("./app-settings-3TLA2EJ7.js");
|
|
7723
|
+
const accountDefaults = resolveThreadDefaults2();
|
|
7724
|
+
const accountDefaultsHint = `Account defaults: agent=${accountDefaults.agent}, model=${accountDefaults.model?.trim() || "Auto"}, effort=${accountDefaults.effort}`;
|
|
7725
|
+
server.tool(
|
|
7726
|
+
"set_caffeinate",
|
|
7727
|
+
"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.",
|
|
7728
|
+
{
|
|
7729
|
+
enabled: z2.boolean().describe("true = hold caffeinate on; false = release and let the Mac sleep")
|
|
7730
|
+
},
|
|
7731
|
+
async ({ enabled }) => {
|
|
7732
|
+
const state = setCaffeinateHold2(enabled);
|
|
7733
|
+
if (enabled && !state.held) {
|
|
6668
7734
|
return {
|
|
6669
7735
|
content: [
|
|
6670
7736
|
{
|
|
6671
7737
|
type: "text",
|
|
6672
|
-
text:
|
|
7738
|
+
text: JSON.stringify({
|
|
7739
|
+
...state,
|
|
7740
|
+
ok: false,
|
|
7741
|
+
message: state.platform === "darwin" ? "Could not start caffeinate." : "Caffeinate is macOS only."
|
|
7742
|
+
})
|
|
6673
7743
|
}
|
|
6674
7744
|
],
|
|
6675
7745
|
isError: true
|
|
6676
7746
|
};
|
|
6677
7747
|
}
|
|
6678
|
-
}
|
|
6679
|
-
);
|
|
6680
|
-
server.tool(
|
|
6681
|
-
"send_to_thread",
|
|
6682
|
-
"Queue a prompt on a worktree thread chat (runs under concurrency cap). Use after create_thread to start or continue a conversation. Set force_stop=true to interrupt an in-flight/queued turn (kill + clear queue) before queueing this prompt \u2014 use when the thread is mid-turn or has stale queued prompts you need to replace.",
|
|
6683
|
-
{
|
|
6684
|
-
ref: z.string(),
|
|
6685
|
-
prompt: z.string(),
|
|
6686
|
-
force_stop: z.boolean().optional()
|
|
6687
|
-
},
|
|
6688
|
-
async ({ ref, prompt, force_stop }) => {
|
|
6689
|
-
if (force_stop) {
|
|
6690
|
-
const existing = orch.getThread(ref);
|
|
6691
|
-
if (existing) {
|
|
6692
|
-
orch.stop(ref, { clearQueue: true });
|
|
6693
|
-
}
|
|
6694
|
-
}
|
|
6695
|
-
const thread = await orch.send(ref, prompt);
|
|
6696
7748
|
return {
|
|
6697
7749
|
content: [
|
|
6698
7750
|
{
|
|
6699
7751
|
type: "text",
|
|
6700
7752
|
text: JSON.stringify({
|
|
6701
|
-
|
|
6702
|
-
|
|
6703
|
-
|
|
6704
|
-
forceStopped: Boolean(force_stop)
|
|
7753
|
+
...getCaffeinateHold2(),
|
|
7754
|
+
ok: true,
|
|
7755
|
+
message: state.held ? "Mac will stay awake until you call set_caffeinate with enabled=false (or the user says they are done)." : "Caffeinate hold released. The Mac can sleep (unless Settings caffeinate toggles are on)."
|
|
6705
7756
|
})
|
|
6706
7757
|
}
|
|
6707
7758
|
]
|
|
@@ -6709,16 +7760,162 @@ async function startMcpServer() {
|
|
|
6709
7760
|
}
|
|
6710
7761
|
);
|
|
6711
7762
|
server.tool(
|
|
6712
|
-
"
|
|
6713
|
-
|
|
7763
|
+
"create_thread",
|
|
7764
|
+
`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.`,
|
|
6714
7765
|
{
|
|
6715
|
-
|
|
6716
|
-
|
|
7766
|
+
sourceType: z2.enum(["branch", "pr", "ticket"]),
|
|
7767
|
+
sourceRef: z2.string(),
|
|
7768
|
+
agent: z2.enum(["claude", "codex", "opencode", "brightsy", "cursor"]).optional().describe(`Omit to use Account default agent (${accountDefaults.agent})`),
|
|
7769
|
+
model: z2.string().nullable().optional().describe(
|
|
7770
|
+
`Usually omit to use Account default model (${accountDefaults.model?.trim() || "Auto"}). Pass null only to force Auto / agent-default.`
|
|
7771
|
+
),
|
|
7772
|
+
repoPath: z2.string(),
|
|
7773
|
+
title: z2.string().optional(),
|
|
7774
|
+
parentThreadId: z2.string().optional().describe(
|
|
7775
|
+
"Orchestration: omit (preferred) or pass YOUR chat id from the turn reminder / AGENTS.md. Do not invent uuids."
|
|
7776
|
+
)
|
|
6717
7777
|
},
|
|
6718
|
-
async (
|
|
6719
|
-
const
|
|
6720
|
-
|
|
6721
|
-
|
|
7778
|
+
async (args) => {
|
|
7779
|
+
const envParentId = process.env.SIDEBOARD_ORCHESTRATOR_THREAD_ID?.trim() || "";
|
|
7780
|
+
let parentId = args.parentThreadId?.trim() || "";
|
|
7781
|
+
let parent = parentId ? orch.getThread(parentId) : null;
|
|
7782
|
+
let parentCorrectedFrom;
|
|
7783
|
+
if (envParentId) {
|
|
7784
|
+
const envParent = orch.getThread(envParentId);
|
|
7785
|
+
if (envParent) {
|
|
7786
|
+
if (parentId && parentId !== envParentId) parentCorrectedFrom = parentId;
|
|
7787
|
+
else if (!parentId) parentCorrectedFrom = void 0;
|
|
7788
|
+
parentId = envParentId;
|
|
7789
|
+
parent = envParent;
|
|
7790
|
+
}
|
|
7791
|
+
}
|
|
7792
|
+
if (parentId && !parent) {
|
|
7793
|
+
parentCorrectedFrom = parentId;
|
|
7794
|
+
parentId = "";
|
|
7795
|
+
parent = null;
|
|
7796
|
+
}
|
|
7797
|
+
if (parentId) {
|
|
7798
|
+
const children = orch.getThreads(false).filter((t) => t.parentThreadId === parentId);
|
|
7799
|
+
if (children.length >= MAX_ORCH_THREADS) {
|
|
7800
|
+
return {
|
|
7801
|
+
content: [
|
|
7802
|
+
{
|
|
7803
|
+
type: "text",
|
|
7804
|
+
text: `Thread-creation cap (${MAX_ORCH_THREADS}) reached for this orchestration session`
|
|
7805
|
+
}
|
|
7806
|
+
],
|
|
7807
|
+
isError: true
|
|
7808
|
+
};
|
|
7809
|
+
}
|
|
7810
|
+
}
|
|
7811
|
+
let agentArg = args.agent;
|
|
7812
|
+
let agentCoercedFrom;
|
|
7813
|
+
const resolvedProbe = resolveNewThreadOptions2({ agent: agentArg }).agent;
|
|
7814
|
+
if (resolvedProbe === "codex") {
|
|
7815
|
+
agentCoercedFrom = agentArg ?? "codex";
|
|
7816
|
+
const accountAgent = resolveNewThreadOptions2({}).agent;
|
|
7817
|
+
agentArg = accountAgent !== "codex" ? accountAgent : "cursor";
|
|
7818
|
+
}
|
|
7819
|
+
const opts = resolveNewThreadOptions2({
|
|
7820
|
+
agent: agentArg,
|
|
7821
|
+
model: args.model
|
|
7822
|
+
});
|
|
7823
|
+
try {
|
|
7824
|
+
const thread = await withTimeout(
|
|
7825
|
+
orch.createThread({
|
|
7826
|
+
sourceType: args.sourceType,
|
|
7827
|
+
sourceRef: args.sourceRef,
|
|
7828
|
+
agent: opts.agent,
|
|
7829
|
+
model: opts.model,
|
|
7830
|
+
effort: opts.effort,
|
|
7831
|
+
fast: opts.fast,
|
|
7832
|
+
repoPath: args.repoPath,
|
|
7833
|
+
title: args.title,
|
|
7834
|
+
parentThreadId: parentId || null
|
|
7835
|
+
}),
|
|
7836
|
+
CREATE_THREAD_TIMEOUT_MS,
|
|
7837
|
+
"create_thread"
|
|
7838
|
+
);
|
|
7839
|
+
const parentNote = parentCorrectedFrom ? parentId ? `Ignored unknown/stale parentThreadId ${parentCorrectedFrom}; nested under ${parentId}` : `Ignored unknown/stale parentThreadId ${parentCorrectedFrom}; created without parent` : void 0;
|
|
7840
|
+
return {
|
|
7841
|
+
content: [
|
|
7842
|
+
{
|
|
7843
|
+
type: "text",
|
|
7844
|
+
text: JSON.stringify({
|
|
7845
|
+
id: thread.id,
|
|
7846
|
+
title: thread.title,
|
|
7847
|
+
branchName: thread.branchName,
|
|
7848
|
+
worktreePath: thread.worktreePath,
|
|
7849
|
+
agent: thread.agent,
|
|
7850
|
+
model: thread.model,
|
|
7851
|
+
status: thread.status,
|
|
7852
|
+
link: `sideboard://thread/${thread.id}`,
|
|
7853
|
+
parentThreadId: thread.parentThreadId,
|
|
7854
|
+
...agentCoercedFrom ? {
|
|
7855
|
+
agentCoercedFrom,
|
|
7856
|
+
note: `Avoid nested Codex under a Codex orchestrator \u2014 used Account default agent=${thread.agent}`
|
|
7857
|
+
} : {},
|
|
7858
|
+
...parentCorrectedFrom ? { parentCorrectedFrom, parentNote } : {}
|
|
7859
|
+
})
|
|
7860
|
+
}
|
|
7861
|
+
]
|
|
7862
|
+
};
|
|
7863
|
+
} catch (err) {
|
|
7864
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
7865
|
+
return {
|
|
7866
|
+
content: [
|
|
7867
|
+
{
|
|
7868
|
+
type: "text",
|
|
7869
|
+
text: `create_thread failed: ${message}`
|
|
7870
|
+
}
|
|
7871
|
+
],
|
|
7872
|
+
isError: true
|
|
7873
|
+
};
|
|
7874
|
+
}
|
|
7875
|
+
}
|
|
7876
|
+
);
|
|
7877
|
+
server.tool(
|
|
7878
|
+
"send_to_thread",
|
|
7879
|
+
"Queue a prompt on a worktree thread chat (runs under concurrency cap). Use after create_thread to start or continue a conversation. Set force_stop=true to interrupt an in-flight/queued turn (kill + clear queue) before queueing this prompt \u2014 use when the thread is mid-turn or has stale queued prompts you need to replace.",
|
|
7880
|
+
{
|
|
7881
|
+
ref: z2.string(),
|
|
7882
|
+
prompt: z2.string(),
|
|
7883
|
+
force_stop: z2.boolean().optional()
|
|
7884
|
+
},
|
|
7885
|
+
async ({ ref, prompt, force_stop }) => {
|
|
7886
|
+
if (force_stop) {
|
|
7887
|
+
const existing = orch.getThread(ref);
|
|
7888
|
+
if (existing) {
|
|
7889
|
+
orch.stop(ref, { clearQueue: true });
|
|
7890
|
+
}
|
|
7891
|
+
}
|
|
7892
|
+
const thread = await orch.send(ref, prompt);
|
|
7893
|
+
return {
|
|
7894
|
+
content: [
|
|
7895
|
+
{
|
|
7896
|
+
type: "text",
|
|
7897
|
+
text: JSON.stringify({
|
|
7898
|
+
id: thread.id,
|
|
7899
|
+
status: thread.status,
|
|
7900
|
+
queueLength: thread.queue.length,
|
|
7901
|
+
forceStopped: Boolean(force_stop)
|
|
7902
|
+
})
|
|
7903
|
+
}
|
|
7904
|
+
]
|
|
7905
|
+
};
|
|
7906
|
+
}
|
|
7907
|
+
);
|
|
7908
|
+
server.tool(
|
|
7909
|
+
"wait_for_turn",
|
|
7910
|
+
"Block until the thread finishes its current/queued turn (avoids polling). Use after send_to_thread to read the agent reply.",
|
|
7911
|
+
{
|
|
7912
|
+
ref: z2.string(),
|
|
7913
|
+
timeoutMs: z2.number().optional()
|
|
7914
|
+
},
|
|
7915
|
+
async ({ ref, timeoutMs }) => {
|
|
7916
|
+
const thread = await orch.waitForTurn(ref, timeoutMs ?? 6e5);
|
|
7917
|
+
const result = orch.getTurnResult(thread.id);
|
|
7918
|
+
return {
|
|
6722
7919
|
content: [
|
|
6723
7920
|
{
|
|
6724
7921
|
type: "text",
|
|
@@ -6735,7 +7932,7 @@ async function startMcpServer() {
|
|
|
6735
7932
|
server.tool(
|
|
6736
7933
|
"get_turn_result",
|
|
6737
7934
|
"Final assistant message only (not full transcript)",
|
|
6738
|
-
{ ref:
|
|
7935
|
+
{ ref: z2.string() },
|
|
6739
7936
|
async ({ ref }) => {
|
|
6740
7937
|
const result = orch.getTurnResult(ref);
|
|
6741
7938
|
return { content: [{ type: "text", text: JSON.stringify(result) }] };
|
|
@@ -6745,8 +7942,8 @@ async function startMcpServer() {
|
|
|
6745
7942
|
"stop_thread",
|
|
6746
7943
|
"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.",
|
|
6747
7944
|
{
|
|
6748
|
-
ref:
|
|
6749
|
-
force:
|
|
7945
|
+
ref: z2.string(),
|
|
7946
|
+
force: z2.boolean().optional()
|
|
6750
7947
|
},
|
|
6751
7948
|
async ({ ref, force }) => {
|
|
6752
7949
|
const t = orch.getThread(ref);
|
|
@@ -6775,8 +7972,8 @@ async function startMcpServer() {
|
|
|
6775
7972
|
);
|
|
6776
7973
|
server.tool(
|
|
6777
7974
|
"archive_thread",
|
|
6778
|
-
"Archive a thread (stops agent/dev, runs archive script, removes worktree when last chat tab).
|
|
6779
|
-
{ ref:
|
|
7975
|
+
"Archive a thread (stops agent/dev, runs archive script, removes worktree when last chat tab). Coordinators open PRs only by asking the worktree agent.",
|
|
7976
|
+
{ ref: z2.string() },
|
|
6780
7977
|
async ({ ref }) => {
|
|
6781
7978
|
const t = orch.getThread(ref);
|
|
6782
7979
|
if (!t) {
|
|
@@ -6809,7 +8006,7 @@ async function startMcpServer() {
|
|
|
6809
8006
|
server.tool(
|
|
6810
8007
|
"restore_thread",
|
|
6811
8008
|
"Restore an archived thread (recreates worktree from branch when needed)",
|
|
6812
|
-
{ ref:
|
|
8009
|
+
{ ref: z2.string() },
|
|
6813
8010
|
async ({ ref }) => {
|
|
6814
8011
|
try {
|
|
6815
8012
|
const restored = await orch.restore(ref);
|
|
@@ -6835,8 +8032,8 @@ async function startMcpServer() {
|
|
|
6835
8032
|
"get_diff",
|
|
6836
8033
|
"Compact diff summary (capped hunks, paginated)",
|
|
6837
8034
|
{
|
|
6838
|
-
ref:
|
|
6839
|
-
maxFiles:
|
|
8035
|
+
ref: z2.string(),
|
|
8036
|
+
maxFiles: z2.number().optional()
|
|
6840
8037
|
},
|
|
6841
8038
|
async ({ ref, maxFiles }) => {
|
|
6842
8039
|
const summary = await orch.diffSummary(ref);
|
|
@@ -6856,7 +8053,7 @@ async function startMcpServer() {
|
|
|
6856
8053
|
server.tool(
|
|
6857
8054
|
"request_review",
|
|
6858
8055
|
'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.',
|
|
6859
|
-
{ ref:
|
|
8056
|
+
{ ref: z2.string().describe("Worktree thread id/ref to review") },
|
|
6860
8057
|
async ({ ref }) => {
|
|
6861
8058
|
try {
|
|
6862
8059
|
const tab = await orch.requestReview(ref);
|
|
@@ -6881,7 +8078,7 @@ async function startMcpServer() {
|
|
|
6881
8078
|
}
|
|
6882
8079
|
}
|
|
6883
8080
|
);
|
|
6884
|
-
const agentEnum =
|
|
8081
|
+
const agentEnum = z2.enum(["claude", "codex", "opencode", "brightsy", "cursor"]);
|
|
6885
8082
|
server.tool(
|
|
6886
8083
|
"list_models",
|
|
6887
8084
|
"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.",
|
|
@@ -6904,11 +8101,11 @@ async function startMcpServer() {
|
|
|
6904
8101
|
"fork_worktree",
|
|
6905
8102
|
"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.",
|
|
6906
8103
|
{
|
|
6907
|
-
ref:
|
|
6908
|
-
through_index:
|
|
8104
|
+
ref: z2.string().describe("Worktree thread id/ref to fork"),
|
|
8105
|
+
through_index: z2.number().optional().describe("Inclusive message index to include in the transcript (default: all)"),
|
|
6909
8106
|
agent: agentEnum.optional().describe("Agent for the forked chat (default: same as source)"),
|
|
6910
|
-
model:
|
|
6911
|
-
title:
|
|
8107
|
+
model: z2.string().nullable().optional().describe("Usually omit (Auto). Only set from list_models when you need a specific model"),
|
|
8108
|
+
title: z2.string().optional()
|
|
6912
8109
|
},
|
|
6913
8110
|
async ({ ref, through_index, agent, model, title }) => {
|
|
6914
8111
|
try {
|
|
@@ -6947,13 +8144,13 @@ async function startMcpServer() {
|
|
|
6947
8144
|
);
|
|
6948
8145
|
server.tool(
|
|
6949
8146
|
"fork_chat",
|
|
6950
|
-
"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).
|
|
8147
|
+
"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.",
|
|
6951
8148
|
{
|
|
6952
|
-
ref:
|
|
6953
|
-
through_index:
|
|
8149
|
+
ref: z2.string().describe("Thread id/ref to fork (worktree agent or orchestration chat)"),
|
|
8150
|
+
through_index: z2.number().optional().describe("Inclusive message index to include in the transcript (default: all)"),
|
|
6954
8151
|
agent: agentEnum.optional().describe("Agent for the forked chat (default: same as source)"),
|
|
6955
|
-
model:
|
|
6956
|
-
title:
|
|
8152
|
+
model: z2.string().nullable().optional().describe("Usually omit (Auto). Only set from list_models when you need a specific model"),
|
|
8153
|
+
title: z2.string().optional()
|
|
6957
8154
|
},
|
|
6958
8155
|
async ({ ref, through_index, agent, model, title }) => {
|
|
6959
8156
|
try {
|
|
@@ -6999,8 +8196,8 @@ async function startMcpServer() {
|
|
|
6999
8196
|
"run_dev_script",
|
|
7000
8197
|
"Start a .sideboard/.conductor run script for a thread (default script if name omitted); returns port",
|
|
7001
8198
|
{
|
|
7002
|
-
ref:
|
|
7003
|
-
name:
|
|
8199
|
+
ref: z2.string(),
|
|
8200
|
+
name: z2.string().optional()
|
|
7004
8201
|
},
|
|
7005
8202
|
async ({ ref, name }) => {
|
|
7006
8203
|
const result = await orch.startDev(ref, name);
|
|
@@ -7022,7 +8219,7 @@ async function startMcpServer() {
|
|
|
7022
8219
|
server.tool(
|
|
7023
8220
|
"list_run_scripts",
|
|
7024
8221
|
"List named run scripts available for a thread",
|
|
7025
|
-
{ ref:
|
|
8222
|
+
{ ref: z2.string() },
|
|
7026
8223
|
async ({ ref }) => {
|
|
7027
8224
|
const scripts = orch.listThreadRunScripts(ref);
|
|
7028
8225
|
const active = orch.getActiveRuns(ref);
|
|
@@ -7040,8 +8237,8 @@ async function startMcpServer() {
|
|
|
7040
8237
|
"stop_dev_script",
|
|
7041
8238
|
"Stop a running script for a thread (all scripts if name omitted)",
|
|
7042
8239
|
{
|
|
7043
|
-
ref:
|
|
7044
|
-
name:
|
|
8240
|
+
ref: z2.string(),
|
|
8241
|
+
name: z2.string().optional()
|
|
7045
8242
|
},
|
|
7046
8243
|
async ({ ref, name }) => {
|
|
7047
8244
|
orch.stopDev(ref, name);
|
|
@@ -7051,7 +8248,7 @@ async function startMcpServer() {
|
|
|
7051
8248
|
server.tool(
|
|
7052
8249
|
"run_setup",
|
|
7053
8250
|
"Re-run workspace setup (Sideboard/Conductor settings or .cursor/worktrees.json)",
|
|
7054
|
-
{ ref:
|
|
8251
|
+
{ ref: z2.string() },
|
|
7055
8252
|
async ({ ref }) => {
|
|
7056
8253
|
const result = await orch.runSetup(ref);
|
|
7057
8254
|
return {
|
|
@@ -7062,7 +8259,7 @@ async function startMcpServer() {
|
|
|
7062
8259
|
server.tool(
|
|
7063
8260
|
"add_workspace",
|
|
7064
8261
|
"Register a git repo as a Sideboard workspace",
|
|
7065
|
-
{ repoPath:
|
|
8262
|
+
{ repoPath: z2.string() },
|
|
7066
8263
|
async ({ repoPath }) => {
|
|
7067
8264
|
const ws = await orch.addWorkspace(repoPath);
|
|
7068
8265
|
return { content: [{ type: "text", text: JSON.stringify(ws) }] };
|
|
@@ -7071,7 +8268,7 @@ async function startMcpServer() {
|
|
|
7071
8268
|
server.tool(
|
|
7072
8269
|
"remove_workspace",
|
|
7073
8270
|
"Unregister a Sideboard workspace (does not archive threads)",
|
|
7074
|
-
{ repoPath:
|
|
8271
|
+
{ repoPath: z2.string() },
|
|
7075
8272
|
async ({ repoPath }) => {
|
|
7076
8273
|
orch.removeWorkspace(repoPath);
|
|
7077
8274
|
return { content: [{ type: "text", text: "ok" }] };
|
|
@@ -7081,12 +8278,12 @@ async function startMcpServer() {
|
|
|
7081
8278
|
"fanout",
|
|
7082
8279
|
"Best-of-n: create one thread per agent with the same prompt (parallel attempts)",
|
|
7083
8280
|
{
|
|
7084
|
-
prompt:
|
|
7085
|
-
agents:
|
|
7086
|
-
repoPath:
|
|
7087
|
-
sourceType:
|
|
7088
|
-
sourceRef:
|
|
7089
|
-
title:
|
|
8281
|
+
prompt: z2.string(),
|
|
8282
|
+
agents: z2.array(z2.enum(["claude", "codex", "opencode", "brightsy", "cursor"])),
|
|
8283
|
+
repoPath: z2.string(),
|
|
8284
|
+
sourceType: z2.enum(["branch", "pr", "ticket"]).optional(),
|
|
8285
|
+
sourceRef: z2.string().optional(),
|
|
8286
|
+
title: z2.string().optional()
|
|
7090
8287
|
},
|
|
7091
8288
|
async (args) => {
|
|
7092
8289
|
const threads = await orch.bestOfN(args);
|
|
@@ -7113,8 +8310,8 @@ async function startMcpServer() {
|
|
|
7113
8310
|
"list_branches",
|
|
7114
8311
|
"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).",
|
|
7115
8312
|
{
|
|
7116
|
-
repoPath:
|
|
7117
|
-
unmergedOnly:
|
|
8313
|
+
repoPath: z2.string(),
|
|
8314
|
+
unmergedOnly: z2.boolean().optional()
|
|
7118
8315
|
},
|
|
7119
8316
|
async ({ repoPath, unmergedOnly }) => {
|
|
7120
8317
|
const root = await resolveRepoRoot(repoPath);
|
|
@@ -7134,7 +8331,7 @@ async function startMcpServer() {
|
|
|
7134
8331
|
server.tool(
|
|
7135
8332
|
"list_prs",
|
|
7136
8333
|
"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.",
|
|
7137
|
-
{ repoPath:
|
|
8334
|
+
{ repoPath: z2.string() },
|
|
7138
8335
|
async ({ repoPath }) => {
|
|
7139
8336
|
const root = await resolveRepoRoot(repoPath);
|
|
7140
8337
|
const prs = await listPrs(root);
|
|
@@ -7153,7 +8350,7 @@ async function startMcpServer() {
|
|
|
7153
8350
|
server.tool(
|
|
7154
8351
|
"get_pr_stack",
|
|
7155
8352
|
"Load the GitHub PR stack for a thread worktree (`gh stack view --json`). Returns null JSON when the branch is not stacked. Prefer this before mergePr on stacked PRs.",
|
|
7156
|
-
{ ref:
|
|
8353
|
+
{ ref: z2.string() },
|
|
7157
8354
|
async ({ ref }) => {
|
|
7158
8355
|
const stack = await orch.getPrStack(ref);
|
|
7159
8356
|
return {
|
|
@@ -7165,8 +8362,8 @@ async function startMcpServer() {
|
|
|
7165
8362
|
"open_pr_stack_layers",
|
|
7166
8363
|
"Materialize one worktree+thread per stack layer (or a single 1-based layer). Pass a thread ref already on the stack.",
|
|
7167
8364
|
{
|
|
7168
|
-
ref:
|
|
7169
|
-
layer:
|
|
8365
|
+
ref: z2.string(),
|
|
8366
|
+
layer: z2.number().int().positive().optional()
|
|
7170
8367
|
},
|
|
7171
8368
|
async ({ ref, layer }) => {
|
|
7172
8369
|
const result = await orch.openPrStackLayers(ref, { layer });
|
|
@@ -7200,9 +8397,9 @@ async function startMcpServer() {
|
|
|
7200
8397
|
"add_stack_layer",
|
|
7201
8398
|
"Add a branch on top of the current stack (`gh stack add`) and open a worktree+thread for it.",
|
|
7202
8399
|
{
|
|
7203
|
-
ref:
|
|
7204
|
-
branchName:
|
|
7205
|
-
title:
|
|
8400
|
+
ref: z2.string(),
|
|
8401
|
+
branchName: z2.string(),
|
|
8402
|
+
title: z2.string().optional()
|
|
7206
8403
|
},
|
|
7207
8404
|
async ({ ref, branchName, title }) => {
|
|
7208
8405
|
const result = await orch.addStackLayer(ref, branchName, { title });
|
|
@@ -7231,11 +8428,11 @@ async function startMcpServer() {
|
|
|
7231
8428
|
"create_pr_stack",
|
|
7232
8429
|
"Create a new GitHub PR stack with one Sideboard worktree per layer (bottom\u2192top branch names). Requires `gh extension install github/gh-stack`.",
|
|
7233
8430
|
{
|
|
7234
|
-
repoPath:
|
|
7235
|
-
branches:
|
|
7236
|
-
agent:
|
|
7237
|
-
base:
|
|
7238
|
-
title:
|
|
8431
|
+
repoPath: z2.string(),
|
|
8432
|
+
branches: z2.array(z2.string()).min(1),
|
|
8433
|
+
agent: z2.enum(["claude", "codex", "opencode", "brightsy", "cursor"]),
|
|
8434
|
+
base: z2.string().optional(),
|
|
8435
|
+
title: z2.string().optional()
|
|
7239
8436
|
},
|
|
7240
8437
|
async (args) => {
|
|
7241
8438
|
const result = await orch.createPrStack({
|
|
@@ -7269,362 +8466,1640 @@ async function startMcpServer() {
|
|
|
7269
8466
|
]
|
|
7270
8467
|
};
|
|
7271
8468
|
}
|
|
7272
|
-
);
|
|
7273
|
-
server.tool(
|
|
7274
|
-
"list_issues",
|
|
7275
|
-
"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.",
|
|
7276
|
-
{ repoPath:
|
|
7277
|
-
async ({ repoPath }) => {
|
|
7278
|
-
const root = await resolveRepoRoot(repoPath);
|
|
7279
|
-
const result = await listIssues(root);
|
|
7280
|
-
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
|
|
8469
|
+
);
|
|
8470
|
+
server.tool(
|
|
8471
|
+
"list_issues",
|
|
8472
|
+
"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.",
|
|
8473
|
+
{ repoPath: z2.string() },
|
|
8474
|
+
async ({ repoPath }) => {
|
|
8475
|
+
const root = await resolveRepoRoot(repoPath);
|
|
8476
|
+
const result = await listIssues(root);
|
|
8477
|
+
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
|
|
8478
|
+
}
|
|
8479
|
+
);
|
|
8480
|
+
server.tool(
|
|
8481
|
+
"list_linear_issues",
|
|
8482
|
+
"Deprecated: prefer list_issues. Lists assigned Linear issues via the chosen agent MCP connector",
|
|
8483
|
+
{
|
|
8484
|
+
agent: z2.enum(["claude", "codex", "opencode"]),
|
|
8485
|
+
repoPath: z2.string()
|
|
8486
|
+
},
|
|
8487
|
+
async ({ agent, repoPath }) => {
|
|
8488
|
+
const issues = await listLinearIssues(agent, repoPath);
|
|
8489
|
+
return { content: [{ type: "text", text: JSON.stringify(issues, null, 2) }] };
|
|
8490
|
+
}
|
|
8491
|
+
);
|
|
8492
|
+
}
|
|
8493
|
+
const transport = new StdioServerTransport();
|
|
8494
|
+
await server.connect(transport);
|
|
8495
|
+
}
|
|
8496
|
+
|
|
8497
|
+
// src/brightsy/api.ts
|
|
8498
|
+
function formatBrightsyFetchError(err, url) {
|
|
8499
|
+
if (!(err instanceof Error)) return `${String(err)} (${url})`;
|
|
8500
|
+
const cause = err.cause;
|
|
8501
|
+
let detail = "";
|
|
8502
|
+
if (cause instanceof Error) {
|
|
8503
|
+
const code = typeof cause.code === "string" ? cause.code : void 0;
|
|
8504
|
+
detail = code ? ` [${code}: ${cause.message}]` : ` [${cause.message}]`;
|
|
8505
|
+
} else if (cause != null) {
|
|
8506
|
+
detail = ` [${String(cause)}]`;
|
|
8507
|
+
}
|
|
8508
|
+
return `${err.message}${detail} (${url})`;
|
|
8509
|
+
}
|
|
8510
|
+
var BrightsySideboardApi = class {
|
|
8511
|
+
cfg;
|
|
8512
|
+
fetchImpl;
|
|
8513
|
+
constructor(cfgOrOpts) {
|
|
8514
|
+
const isOpts = !!cfgOrOpts && typeof cfgOrOpts === "object" && ("fetchImpl" in cfgOrOpts || "cfg" in cfgOrOpts) && !("access_token" in cfgOrOpts);
|
|
8515
|
+
if (isOpts) {
|
|
8516
|
+
const opts = cfgOrOpts;
|
|
8517
|
+
this.cfg = opts.cfg ?? loadBrightsyConfig();
|
|
8518
|
+
this.fetchImpl = opts.fetchImpl ?? globalThis.fetch.bind(globalThis);
|
|
8519
|
+
} else {
|
|
8520
|
+
this.cfg = cfgOrOpts ?? loadBrightsyConfig();
|
|
8521
|
+
this.fetchImpl = globalThis.fetch.bind(globalThis);
|
|
8522
|
+
}
|
|
8523
|
+
}
|
|
8524
|
+
get accountId() {
|
|
8525
|
+
return this.cfg.account_id;
|
|
8526
|
+
}
|
|
8527
|
+
get endpoint() {
|
|
8528
|
+
return (this.cfg.endpoint || "https://brightsy.ai").replace(/\/$/, "");
|
|
8529
|
+
}
|
|
8530
|
+
async request(path, init = {}) {
|
|
8531
|
+
await this.refreshIfNeeded();
|
|
8532
|
+
const url = `${this.endpoint}${path}`;
|
|
8533
|
+
const headers = {
|
|
8534
|
+
Authorization: `Bearer ${this.cfg.access_token}`,
|
|
8535
|
+
Accept: "application/json"
|
|
8536
|
+
};
|
|
8537
|
+
if (init.body != null) headers["Content-Type"] = "application/json";
|
|
8538
|
+
let res;
|
|
8539
|
+
try {
|
|
8540
|
+
res = await this.fetchImpl(url, {
|
|
8541
|
+
method: init.method ?? "GET",
|
|
8542
|
+
headers,
|
|
8543
|
+
body: init.body != null ? JSON.stringify(init.body) : void 0
|
|
8544
|
+
});
|
|
8545
|
+
} catch (err) {
|
|
8546
|
+
throw new Error(formatBrightsyFetchError(err, url));
|
|
8547
|
+
}
|
|
8548
|
+
if (!res.ok) {
|
|
8549
|
+
const err = await res.json().catch(() => ({}));
|
|
8550
|
+
throw new Error(err.error?.message ?? `${res.status} ${res.statusText} (${url})`);
|
|
8551
|
+
}
|
|
8552
|
+
return await res.json();
|
|
8553
|
+
}
|
|
8554
|
+
async refreshIfNeeded() {
|
|
8555
|
+
const expiresAt = this.cfg.expires_at ?? 0;
|
|
8556
|
+
if (!this.cfg.refresh_token || Date.now() < expiresAt - 6e4) return;
|
|
8557
|
+
const clientId = this.cfg.oauth_client_id || "brightsy-cli";
|
|
8558
|
+
const url = `${this.endpoint}/oauth/token`;
|
|
8559
|
+
let res;
|
|
8560
|
+
try {
|
|
8561
|
+
res = await this.fetchImpl(url, {
|
|
8562
|
+
method: "POST",
|
|
8563
|
+
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
8564
|
+
body: new URLSearchParams({
|
|
8565
|
+
grant_type: "refresh_token",
|
|
8566
|
+
refresh_token: this.cfg.refresh_token,
|
|
8567
|
+
client_id: clientId
|
|
8568
|
+
})
|
|
8569
|
+
});
|
|
8570
|
+
} catch (err) {
|
|
8571
|
+
throw new Error(formatBrightsyFetchError(err, url));
|
|
8572
|
+
}
|
|
8573
|
+
if (!res.ok) return;
|
|
8574
|
+
const data = await res.json();
|
|
8575
|
+
if (!data.access_token) return;
|
|
8576
|
+
this.cfg.access_token = data.access_token;
|
|
8577
|
+
if (data.refresh_token) this.cfg.refresh_token = data.refresh_token;
|
|
8578
|
+
if (data.expires_in) {
|
|
8579
|
+
this.cfg.expires_at = Date.now() + data.expires_in * 1e3;
|
|
8580
|
+
}
|
|
8581
|
+
saveBrightsyConfig(this.cfg);
|
|
8582
|
+
}
|
|
8583
|
+
async getAccess() {
|
|
8584
|
+
return this.request(
|
|
8585
|
+
`/api/v1beta/desktop/access?accountId=${encodeURIComponent(this.accountId)}`
|
|
8586
|
+
);
|
|
8587
|
+
}
|
|
8588
|
+
async setAccess(enabled, allow_always = false) {
|
|
8589
|
+
return this.request("/api/v1beta/desktop/access", {
|
|
8590
|
+
method: "PUT",
|
|
8591
|
+
body: {
|
|
8592
|
+
accountId: this.accountId,
|
|
8593
|
+
enabled,
|
|
8594
|
+
allow_always
|
|
8595
|
+
}
|
|
8596
|
+
});
|
|
8597
|
+
}
|
|
8598
|
+
async getTasks(status) {
|
|
8599
|
+
const params = new URLSearchParams();
|
|
8600
|
+
if (status) params.set("status", status);
|
|
8601
|
+
params.set("accountId", this.accountId);
|
|
8602
|
+
const q = `?${params.toString()}`;
|
|
8603
|
+
const data = await this.request(
|
|
8604
|
+
`/api/v1beta/desktop/tasks${q}`
|
|
8605
|
+
);
|
|
8606
|
+
return data.tasks ?? [];
|
|
8607
|
+
}
|
|
8608
|
+
async approveTask(taskId) {
|
|
8609
|
+
await this.request(`/api/v1beta/desktop/tasks/${taskId}`, {
|
|
8610
|
+
method: "PATCH",
|
|
8611
|
+
body: { action: "approve" }
|
|
8612
|
+
});
|
|
8613
|
+
}
|
|
8614
|
+
async markRunning(taskId) {
|
|
8615
|
+
await this.request(`/api/v1beta/desktop/tasks/${taskId}`, {
|
|
8616
|
+
method: "PATCH",
|
|
8617
|
+
body: { task_status: "running" }
|
|
8618
|
+
});
|
|
8619
|
+
}
|
|
8620
|
+
async submitResponse(taskId, response) {
|
|
8621
|
+
await this.request(`/api/v1beta/desktop/tasks/${taskId}/response`, {
|
|
8622
|
+
method: "POST",
|
|
8623
|
+
body: { response }
|
|
8624
|
+
});
|
|
8625
|
+
}
|
|
8626
|
+
};
|
|
8627
|
+
function taskMessageText(task) {
|
|
8628
|
+
const parts = task.message?.parts ?? [];
|
|
8629
|
+
const text2 = parts.find((p) => p.kind === "text")?.text ?? "";
|
|
8630
|
+
return text2.trim();
|
|
8631
|
+
}
|
|
8632
|
+
|
|
8633
|
+
// src/brightsy/cloud-connect.ts
|
|
8634
|
+
var POLL_MS = 5e3;
|
|
8635
|
+
var handleTaskChain = Promise.resolve();
|
|
8636
|
+
function enqueueHandleTask(fn) {
|
|
8637
|
+
const run2 = handleTaskChain.then(fn, fn);
|
|
8638
|
+
handleTaskChain = run2.then(
|
|
8639
|
+
() => void 0,
|
|
8640
|
+
() => void 0
|
|
8641
|
+
);
|
|
8642
|
+
return run2;
|
|
8643
|
+
}
|
|
8644
|
+
function refreshWorkspaces() {
|
|
8645
|
+
const orch = getOrchestrator();
|
|
8646
|
+
return orch.listWorkspaces();
|
|
8647
|
+
}
|
|
8648
|
+
async function handleTask(api, task, opts) {
|
|
8649
|
+
const log = opts.onLog ?? (() => void 0);
|
|
8650
|
+
let message = taskMessageText(task);
|
|
8651
|
+
if (!message) {
|
|
8652
|
+
log(`skip ${task.id.slice(0, 8)}: empty message`);
|
|
8653
|
+
return;
|
|
8654
|
+
}
|
|
8655
|
+
if (task.task_status !== "pending" && task.task_status !== "running" && task.task_status !== "awaiting_confirmation") {
|
|
8656
|
+
return;
|
|
8657
|
+
}
|
|
8658
|
+
const workspaces = refreshWorkspaces();
|
|
8659
|
+
for (const ws of workspaces) {
|
|
8660
|
+
await getOrchestrator().reconcile(ws.path).catch(() => void 0);
|
|
8661
|
+
}
|
|
8662
|
+
const inventory = await enrichWorkspacesWithGithub(workspaces);
|
|
8663
|
+
const coordinator = ensureCloudCoordinator(opts.agent);
|
|
8664
|
+
let fresh = readThread(coordinator.id) ?? coordinator;
|
|
8665
|
+
const parsed = parseForceStopMessage(message);
|
|
8666
|
+
if (parsed.forceStop) {
|
|
8667
|
+
try {
|
|
8668
|
+
getOrchestrator().stop(fresh.id);
|
|
8669
|
+
log(`force-stop ${task.id.slice(0, 8)} \u2192 coordinator ${fresh.id.slice(0, 8)}`);
|
|
8670
|
+
} catch (err) {
|
|
8671
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
8672
|
+
log(`force-stop ${task.id.slice(0, 8)}: ${msg}`);
|
|
8673
|
+
}
|
|
8674
|
+
if (!parsed.remainder) {
|
|
8675
|
+
if (task.task_status === "awaiting_confirmation") {
|
|
8676
|
+
await api.approveTask(task.id).catch(() => void 0);
|
|
8677
|
+
}
|
|
8678
|
+
await api.markRunning(task.id).catch(() => void 0);
|
|
8679
|
+
await api.submitResponse(task.id, CLOUD_COORDINATOR_STOPPED_REPLY);
|
|
8680
|
+
log(`replied stopped ${task.id.slice(0, 8)}`);
|
|
8681
|
+
return;
|
|
8682
|
+
}
|
|
8683
|
+
message = parsed.remainder;
|
|
8684
|
+
fresh = readThread(coordinator.id) ?? ensureCloudCoordinator(opts.agent);
|
|
8685
|
+
}
|
|
8686
|
+
if (fresh.status === "running" || fresh.status === "queued") {
|
|
8687
|
+
log(
|
|
8688
|
+
`busy ${task.id.slice(0, 8)} \u2192 coordinator ${fresh.id.slice(0, 8)} (${fresh.status})`
|
|
8689
|
+
);
|
|
8690
|
+
if (task.task_status === "awaiting_confirmation") {
|
|
8691
|
+
await api.approveTask(task.id).catch(() => void 0);
|
|
8692
|
+
}
|
|
8693
|
+
await api.markRunning(task.id).catch(() => void 0);
|
|
8694
|
+
await api.submitResponse(task.id, CLOUD_COORDINATOR_BUSY_REPLY);
|
|
8695
|
+
log(`replied busy ${task.id.slice(0, 8)}`);
|
|
8696
|
+
return;
|
|
8697
|
+
}
|
|
8698
|
+
if (task.task_status === "awaiting_confirmation") {
|
|
8699
|
+
log(`approve ${task.id.slice(0, 8)}`);
|
|
8700
|
+
await api.approveTask(task.id);
|
|
8701
|
+
task = { ...task, task_status: "pending" };
|
|
8702
|
+
}
|
|
8703
|
+
log(
|
|
8704
|
+
`run ${task.id.slice(0, 8)} \u2192 coordinator ${fresh.id.slice(0, 8)} (${inventory.length} workspace${inventory.length === 1 ? "" : "s"})`
|
|
8705
|
+
);
|
|
8706
|
+
await api.markRunning(task.id).catch(() => void 0);
|
|
8707
|
+
const orch = getOrchestrator();
|
|
8708
|
+
const prompt = [
|
|
8709
|
+
coordinatorSystemPrompt({
|
|
8710
|
+
goal: fresh.sourceRef || CLOUD_ORCHESTRATOR_GOAL,
|
|
8711
|
+
parentId: fresh.id,
|
|
8712
|
+
workspaces: inventory,
|
|
8713
|
+
audience: "cloud"
|
|
8714
|
+
}),
|
|
8715
|
+
"",
|
|
8716
|
+
"--- Cloud agent request ---",
|
|
8717
|
+
message
|
|
8718
|
+
].join("\n");
|
|
8719
|
+
try {
|
|
8720
|
+
await orch.send(fresh.id, prompt);
|
|
8721
|
+
await orch.waitForTurn(fresh.id, 14 * 60 * 1e3);
|
|
8722
|
+
const result = orch.getTurnResult(fresh.id);
|
|
8723
|
+
const reply = result.text.trim() || `(coordinator finished with status ${result.status}, no text)`;
|
|
8724
|
+
await api.submitResponse(task.id, reply);
|
|
8725
|
+
log(`replied ${task.id.slice(0, 8)} (${reply.length} chars)`);
|
|
8726
|
+
} catch (err) {
|
|
8727
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
8728
|
+
const timedOut = /timed out|timeout/i.test(msg);
|
|
8729
|
+
const reply = timedOut ? CLOUD_COORDINATOR_TIMEOUT_REPLY : `Sideboard coordinator error: ${msg}`;
|
|
8730
|
+
await api.submitResponse(task.id, reply).catch(() => void 0);
|
|
8731
|
+
log(`replied error ${task.id.slice(0, 8)}: ${msg}`);
|
|
8732
|
+
if (!timedOut) throw err;
|
|
8733
|
+
}
|
|
8734
|
+
}
|
|
8735
|
+
async function runCloudConnect(opts) {
|
|
8736
|
+
const log = opts.onLog ?? console.log;
|
|
8737
|
+
const api = opts.api ?? new BrightsySideboardApi(
|
|
8738
|
+
opts.fetchImpl ? { fetchImpl: opts.fetchImpl } : void 0
|
|
8739
|
+
);
|
|
8740
|
+
const enableAccess = opts.enableAccess !== false;
|
|
8741
|
+
const allowAlways = opts.allowAlways !== false;
|
|
8742
|
+
const pollMs = opts.pollIntervalMs ?? POLL_MS;
|
|
8743
|
+
const workspaces = refreshWorkspaces();
|
|
8744
|
+
const coordinator = ensureCloudCoordinator(opts.agent);
|
|
8745
|
+
const access = await api.getAccess();
|
|
8746
|
+
if (!access.enabled) {
|
|
8747
|
+
if (!enableAccess) {
|
|
8748
|
+
throw new Error(
|
|
8749
|
+
"Brightsy desktop access is disabled. Enable Cloud messages in Sideboard Settings \u2192 Brightsy, or Brightsy User Settings."
|
|
8750
|
+
);
|
|
8751
|
+
}
|
|
8752
|
+
log("Enabling Brightsy desktop access\u2026");
|
|
8753
|
+
await api.setAccess(true, allowAlways);
|
|
8754
|
+
} else if (allowAlways && !access.allow_always) {
|
|
8755
|
+
await api.setAccess(true, true);
|
|
8756
|
+
}
|
|
8757
|
+
log(
|
|
8758
|
+
`Connected to Brightsy (${api.endpoint}). Polling desktop tasks every ${pollMs / 1e3}s\u2026`
|
|
8759
|
+
);
|
|
8760
|
+
log(
|
|
8761
|
+
`Global coordinator: ${coordinator.id.slice(0, 8)} agent: ${opts.agent} global chats: ${listGlobalThreads().length}`
|
|
8762
|
+
);
|
|
8763
|
+
log(`Workspaces (${workspaces.length}):`);
|
|
8764
|
+
for (const line of formatWorkspaceInventory(workspaces).split("\n")) {
|
|
8765
|
+
log(` ${line}`);
|
|
8766
|
+
}
|
|
8767
|
+
const inFlight = /* @__PURE__ */ new Set();
|
|
8768
|
+
let consecutivePollFailures = 0;
|
|
8769
|
+
while (!opts.signal?.aborted) {
|
|
8770
|
+
try {
|
|
8771
|
+
const hadFailures = consecutivePollFailures > 0;
|
|
8772
|
+
const [pending, awaiting, running] = await Promise.all([
|
|
8773
|
+
api.getTasks("pending"),
|
|
8774
|
+
api.getTasks("awaiting_confirmation"),
|
|
8775
|
+
api.getTasks("running")
|
|
8776
|
+
]);
|
|
8777
|
+
consecutivePollFailures = 0;
|
|
8778
|
+
if (hadFailures) log("poll recovered");
|
|
8779
|
+
const tasks = [...awaiting, ...pending, ...running];
|
|
8780
|
+
for (const task of tasks) {
|
|
8781
|
+
if (inFlight.has(task.id)) continue;
|
|
8782
|
+
if (task.task_status === "running") continue;
|
|
8783
|
+
inFlight.add(task.id);
|
|
8784
|
+
const message = taskMessageText(task);
|
|
8785
|
+
if (message && parseForceStopMessage(message).forceStop) {
|
|
8786
|
+
try {
|
|
8787
|
+
getOrchestrator().stop(ensureCloudCoordinator(opts.agent).id);
|
|
8788
|
+
log(
|
|
8789
|
+
`force-stop interrupt ${task.id.slice(0, 8)} \u2192 coordinator`
|
|
8790
|
+
);
|
|
8791
|
+
} catch (err) {
|
|
8792
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
8793
|
+
log(`force-stop interrupt ${task.id.slice(0, 8)}: ${msg}`);
|
|
8794
|
+
}
|
|
8795
|
+
}
|
|
8796
|
+
void enqueueHandleTask(() => handleTask(api, task, opts)).catch((err) => {
|
|
8797
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
8798
|
+
log(`error ${task.id.slice(0, 8)}: ${msg}`);
|
|
8799
|
+
}).finally(() => inFlight.delete(task.id));
|
|
8800
|
+
}
|
|
8801
|
+
} catch (err) {
|
|
8802
|
+
consecutivePollFailures += 1;
|
|
8803
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
8804
|
+
if (consecutivePollFailures === 1) {
|
|
8805
|
+
log(`poll retry: ${msg}`);
|
|
8806
|
+
} else {
|
|
8807
|
+
log(`poll error: ${msg}`);
|
|
8808
|
+
}
|
|
8809
|
+
}
|
|
8810
|
+
await new Promise((resolve) => {
|
|
8811
|
+
const t = setTimeout(resolve, pollMs);
|
|
8812
|
+
opts.signal?.addEventListener(
|
|
8813
|
+
"abort",
|
|
8814
|
+
() => {
|
|
8815
|
+
clearTimeout(t);
|
|
8816
|
+
resolve();
|
|
8817
|
+
},
|
|
8818
|
+
{ once: true }
|
|
8819
|
+
);
|
|
8820
|
+
});
|
|
8821
|
+
}
|
|
8822
|
+
}
|
|
8823
|
+
|
|
8824
|
+
// src/slack/oauth.ts
|
|
8825
|
+
import { randomBytes as randomBytes2 } from "crypto";
|
|
8826
|
+
import { createServer as createServer3 } from "http";
|
|
8827
|
+
|
|
8828
|
+
// src/slack/baked-app.ts
|
|
8829
|
+
var BAKED_SLACK_CLIENT_ID = "7592788819232.11813536272562";
|
|
8830
|
+
var BAKED_SLACK_CLIENT_SECRET = "10290abf6ad0dc11fe6fc321a6215299";
|
|
8831
|
+
var BAKED_SLACK_RELAY_URL = "wss://slack-relay.sideboard.cloud/desktop";
|
|
8832
|
+
function hasBakedSlackOAuth() {
|
|
8833
|
+
return Boolean(BAKED_SLACK_CLIENT_ID.trim() && BAKED_SLACK_CLIENT_SECRET.trim());
|
|
8834
|
+
}
|
|
8835
|
+
function slackRelayUrl() {
|
|
8836
|
+
return process.env.SIDEBOARD_SLACK_RELAY_URL?.trim() || BAKED_SLACK_RELAY_URL.trim();
|
|
8837
|
+
}
|
|
8838
|
+
|
|
8839
|
+
// src/slack/oauth.ts
|
|
8840
|
+
var SLACK_OAUTH_PORT = 19847;
|
|
8841
|
+
var SLACK_OAUTH_REDIRECT = `http://127.0.0.1:${SLACK_OAUTH_PORT}/callback`;
|
|
8842
|
+
var SLACK_BOT_SCOPES = [
|
|
8843
|
+
"app_mentions:read",
|
|
8844
|
+
"channels:history",
|
|
8845
|
+
"channels:read",
|
|
8846
|
+
"groups:history",
|
|
8847
|
+
"groups:read",
|
|
8848
|
+
"im:history",
|
|
8849
|
+
"im:read",
|
|
8850
|
+
"im:write",
|
|
8851
|
+
"mpim:history",
|
|
8852
|
+
"mpim:read",
|
|
8853
|
+
"chat:write",
|
|
8854
|
+
"chat:write.public",
|
|
8855
|
+
"reactions:write",
|
|
8856
|
+
"users:read",
|
|
8857
|
+
"users:read.email",
|
|
8858
|
+
"team:read"
|
|
8859
|
+
].join(",");
|
|
8860
|
+
var SLACK_USER_SCOPES = [
|
|
8861
|
+
"search:read",
|
|
8862
|
+
"channels:history",
|
|
8863
|
+
"channels:read",
|
|
8864
|
+
"groups:history",
|
|
8865
|
+
"im:history",
|
|
8866
|
+
"mpim:history",
|
|
8867
|
+
"chat:write",
|
|
8868
|
+
"users:read"
|
|
8869
|
+
].join(",");
|
|
8870
|
+
function slackOAuthCredentials() {
|
|
8871
|
+
const settings = loadAppSettings();
|
|
8872
|
+
const clientId = process.env.SIDEBOARD_SLACK_CLIENT_ID?.trim() || settings.integrations.slackClientId?.trim() || BAKED_SLACK_CLIENT_ID.trim();
|
|
8873
|
+
const clientSecret = process.env.SIDEBOARD_SLACK_CLIENT_SECRET?.trim() || settings.integrations.slackClientSecret?.trim() || BAKED_SLACK_CLIENT_SECRET.trim();
|
|
8874
|
+
if (!clientId || !clientSecret) {
|
|
8875
|
+
throw new Error(
|
|
8876
|
+
"Slack browser sign-in needs a Slack app Client ID and Secret (Account \u2192 Slack, or SIDEBOARD_SLACK_CLIENT_ID / SIDEBOARD_SLACK_CLIENT_SECRET). You can still paste an xoxb- or xoxp- token."
|
|
8877
|
+
);
|
|
8878
|
+
}
|
|
8879
|
+
return { clientId, clientSecret };
|
|
8880
|
+
}
|
|
8881
|
+
function slackOAuthAuthorizeUrl(clientId, state) {
|
|
8882
|
+
const params = new URLSearchParams({
|
|
8883
|
+
client_id: clientId,
|
|
8884
|
+
scope: SLACK_BOT_SCOPES,
|
|
8885
|
+
user_scope: SLACK_USER_SCOPES,
|
|
8886
|
+
redirect_uri: SLACK_OAUTH_REDIRECT,
|
|
8887
|
+
state
|
|
8888
|
+
});
|
|
8889
|
+
return `https://slack.com/oauth/v2/authorize?${params.toString()}`;
|
|
8890
|
+
}
|
|
8891
|
+
function htmlPage2(title, body) {
|
|
8892
|
+
return `<!doctype html><html><head><meta charset="utf-8"><title>${title}</title>
|
|
8893
|
+
<style>body{font-family:ui-sans-serif,system-ui,sans-serif;padding:48px 24px;max-width:36rem;margin:0 auto;color:#1a1a1a}
|
|
8894
|
+
h1{font-size:1.25rem}p{line-height:1.5;color:#444}</style></head><body>${body}</body></html>`;
|
|
8895
|
+
}
|
|
8896
|
+
async function startSlackOAuth(opts) {
|
|
8897
|
+
const { clientId, clientSecret } = slackOAuthCredentials();
|
|
8898
|
+
const state = randomBytes2(16).toString("hex");
|
|
8899
|
+
const authorizeUrl = slackOAuthAuthorizeUrl(clientId, state);
|
|
8900
|
+
const timeoutMs = opts?.timeoutMs ?? 5 * 6e4;
|
|
8901
|
+
const code = await new Promise((resolve, reject) => {
|
|
8902
|
+
const server = createServer3((req, res2) => {
|
|
8903
|
+
try {
|
|
8904
|
+
const url = new URL(req.url || "/", `http://127.0.0.1:${SLACK_OAUTH_PORT}`);
|
|
8905
|
+
if (url.pathname !== "/callback") {
|
|
8906
|
+
res2.writeHead(404);
|
|
8907
|
+
res2.end("Not found");
|
|
8908
|
+
return;
|
|
8909
|
+
}
|
|
8910
|
+
const err = url.searchParams.get("error");
|
|
8911
|
+
const gotState = url.searchParams.get("state");
|
|
8912
|
+
const gotCode = url.searchParams.get("code");
|
|
8913
|
+
if (err) {
|
|
8914
|
+
res2.writeHead(400, { "Content-Type": "text/html" });
|
|
8915
|
+
res2.end(htmlPage2("Slack", `<h1>Authorization cancelled</h1><p>${err}</p>`));
|
|
8916
|
+
cleanup();
|
|
8917
|
+
reject(new Error(`Slack OAuth: ${err}`));
|
|
8918
|
+
return;
|
|
8919
|
+
}
|
|
8920
|
+
if (gotState !== state || !gotCode) {
|
|
8921
|
+
res2.writeHead(400, { "Content-Type": "text/html" });
|
|
8922
|
+
res2.end(htmlPage2("Slack", "<h1>Invalid callback</h1><p>State or code missing.</p>"));
|
|
8923
|
+
cleanup();
|
|
8924
|
+
reject(new Error("Slack OAuth callback was invalid"));
|
|
8925
|
+
return;
|
|
8926
|
+
}
|
|
8927
|
+
res2.writeHead(200, { "Content-Type": "text/html" });
|
|
8928
|
+
res2.end(
|
|
8929
|
+
htmlPage2(
|
|
8930
|
+
"Slack connected",
|
|
8931
|
+
"<h1>Slack workspace connected</h1><p>You can close this tab and return to Sideboard.</p>"
|
|
8932
|
+
)
|
|
8933
|
+
);
|
|
8934
|
+
cleanup();
|
|
8935
|
+
resolve(gotCode);
|
|
8936
|
+
} catch (e) {
|
|
8937
|
+
cleanup();
|
|
8938
|
+
reject(e instanceof Error ? e : new Error(String(e)));
|
|
8939
|
+
}
|
|
8940
|
+
});
|
|
8941
|
+
const timer = setTimeout(() => {
|
|
8942
|
+
cleanup();
|
|
8943
|
+
reject(new Error("Slack sign-in timed out \u2014 try again"));
|
|
8944
|
+
}, timeoutMs);
|
|
8945
|
+
const cleanup = () => {
|
|
8946
|
+
clearTimeout(timer);
|
|
8947
|
+
server.close();
|
|
8948
|
+
};
|
|
8949
|
+
server.on("error", (e) => {
|
|
8950
|
+
cleanup();
|
|
8951
|
+
reject(
|
|
8952
|
+
e instanceof Error && e.code === "EADDRINUSE" ? new Error(
|
|
8953
|
+
`Port ${SLACK_OAUTH_PORT} is in use. Close whatever is bound there, or paste a Slack token instead.`
|
|
8954
|
+
) : e
|
|
8955
|
+
);
|
|
8956
|
+
});
|
|
8957
|
+
server.listen(SLACK_OAUTH_PORT, "127.0.0.1", () => {
|
|
8958
|
+
void Promise.resolve(opts?.openUrl?.(authorizeUrl)).catch((e) => {
|
|
8959
|
+
cleanup();
|
|
8960
|
+
reject(e instanceof Error ? e : new Error(String(e)));
|
|
8961
|
+
});
|
|
8962
|
+
});
|
|
8963
|
+
});
|
|
8964
|
+
const body = new URLSearchParams({
|
|
8965
|
+
client_id: clientId,
|
|
8966
|
+
client_secret: clientSecret,
|
|
8967
|
+
code,
|
|
8968
|
+
redirect_uri: SLACK_OAUTH_REDIRECT
|
|
8969
|
+
});
|
|
8970
|
+
const res = await fetch("https://slack.com/api/oauth.v2.access", {
|
|
8971
|
+
method: "POST",
|
|
8972
|
+
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
8973
|
+
body
|
|
8974
|
+
});
|
|
8975
|
+
const data = await res.json();
|
|
8976
|
+
if (!data.ok) {
|
|
8977
|
+
throw new Error(`Slack OAuth exchange failed: ${data.error || res.status}`);
|
|
8978
|
+
}
|
|
8979
|
+
const teamId = data.team?.id?.trim();
|
|
8980
|
+
if (!teamId) throw new Error("Slack OAuth did not return a team id");
|
|
8981
|
+
return upsertSlackWorkspace({
|
|
8982
|
+
team_id: teamId,
|
|
8983
|
+
team_name: data.team?.name?.trim() || teamId,
|
|
8984
|
+
user_id: data.authed_user?.id,
|
|
8985
|
+
bot_token: data.access_token,
|
|
8986
|
+
user_token: data.authed_user?.access_token,
|
|
8987
|
+
scopes: [data.scope, data.authed_user?.scope].filter(Boolean).join(","),
|
|
8988
|
+
connected_at: (/* @__PURE__ */ new Date()).toISOString()
|
|
8989
|
+
});
|
|
8990
|
+
}
|
|
8991
|
+
|
|
8992
|
+
// src/slack/socket-mode.ts
|
|
8993
|
+
import { WebSocket as WsWebSocket } from "ws";
|
|
8994
|
+
var BACKOFF_START_MS = 1e3;
|
|
8995
|
+
var BACKOFF_MAX_MS = 3e4;
|
|
8996
|
+
function stripSlackMentions(text2) {
|
|
8997
|
+
return text2.replace(/<@[^>]+>/g, "").replace(/\s+/g, " ").trim();
|
|
8998
|
+
}
|
|
8999
|
+
function isSlackStopCommand(text2) {
|
|
9000
|
+
const t = stripSlackMentions(text2).toLowerCase();
|
|
9001
|
+
return t === "stop" || t === "sideboard_force_stop";
|
|
9002
|
+
}
|
|
9003
|
+
function parseSlackSocketFrame(raw) {
|
|
9004
|
+
try {
|
|
9005
|
+
const parsed = JSON.parse(raw);
|
|
9006
|
+
if (!parsed || typeof parsed !== "object") return null;
|
|
9007
|
+
return parsed;
|
|
9008
|
+
} catch {
|
|
9009
|
+
return null;
|
|
9010
|
+
}
|
|
9011
|
+
}
|
|
9012
|
+
function inboundFromSocketFrame(frame) {
|
|
9013
|
+
if (frame.type !== "events_api") return null;
|
|
9014
|
+
const event = frame.payload?.event;
|
|
9015
|
+
if (!event) return null;
|
|
9016
|
+
if (event.bot_id) return null;
|
|
9017
|
+
if (event.subtype) return null;
|
|
9018
|
+
const channelId = event.channel?.trim();
|
|
9019
|
+
const ts = event.ts?.trim();
|
|
9020
|
+
if (!channelId || !ts) return null;
|
|
9021
|
+
const rawText = event.text?.trim() ?? "";
|
|
9022
|
+
const text2 = stripSlackMentions(rawText);
|
|
9023
|
+
if (!text2) return null;
|
|
9024
|
+
const teamId = (frame.payload?.team_id || event.team || "").trim();
|
|
9025
|
+
if (!teamId) return null;
|
|
9026
|
+
const isDm = event.type === "message" && (event.channel_type === "im" || event.channel_type === "mpim" || !event.channel_type && channelId.startsWith("D"));
|
|
9027
|
+
const isMention = event.type === "app_mention";
|
|
9028
|
+
if (!isDm && !isMention) return null;
|
|
9029
|
+
return {
|
|
9030
|
+
teamId,
|
|
9031
|
+
channelId,
|
|
9032
|
+
ts,
|
|
9033
|
+
threadTs: event.thread_ts?.trim() || void 0,
|
|
9034
|
+
userId: event.user?.trim(),
|
|
9035
|
+
text: text2,
|
|
9036
|
+
kind: isMention ? "mention" : "dm"
|
|
9037
|
+
};
|
|
9038
|
+
}
|
|
9039
|
+
async function openSlackSocketUrl(appToken, fetchImpl) {
|
|
9040
|
+
const token = appToken.trim();
|
|
9041
|
+
if (!token.startsWith("xapp-")) {
|
|
9042
|
+
throw new Error(
|
|
9043
|
+
"Socket Mode needs an app-level token (xapp-\u2026) with connections:write. Create one on the Slack app \u2192 Basic Information \u2192 App-Level Tokens."
|
|
9044
|
+
);
|
|
9045
|
+
}
|
|
9046
|
+
const doFetch = fetchImpl ?? fetch;
|
|
9047
|
+
const res = await doFetch("https://slack.com/api/apps.connections.open", {
|
|
9048
|
+
method: "POST",
|
|
9049
|
+
headers: {
|
|
9050
|
+
Authorization: `Bearer ${token}`,
|
|
9051
|
+
"Content-Type": "application/x-www-form-urlencoded"
|
|
9052
|
+
},
|
|
9053
|
+
body: new URLSearchParams()
|
|
9054
|
+
});
|
|
9055
|
+
const json = await res.json();
|
|
9056
|
+
if (!json.ok) {
|
|
9057
|
+
throw new SlackApiError("apps.connections.open", json.error || `HTTP ${res.status}`);
|
|
9058
|
+
}
|
|
9059
|
+
const url = json.url?.trim();
|
|
9060
|
+
if (!url) throw new Error("Slack apps.connections.open did not return a WebSocket URL");
|
|
9061
|
+
return url;
|
|
9062
|
+
}
|
|
9063
|
+
function messageToString(data) {
|
|
9064
|
+
if (typeof data === "string") return data;
|
|
9065
|
+
if (Buffer.isBuffer(data)) return data.toString("utf8");
|
|
9066
|
+
if (Array.isArray(data)) return Buffer.concat(data).toString("utf8");
|
|
9067
|
+
if (data instanceof ArrayBuffer) return Buffer.from(data).toString("utf8");
|
|
9068
|
+
return String(data ?? "");
|
|
9069
|
+
}
|
|
9070
|
+
function nodeWebSocketCtor() {
|
|
9071
|
+
return class NodeSlackWebSocket {
|
|
9072
|
+
socket;
|
|
9073
|
+
constructor(url) {
|
|
9074
|
+
this.socket = new WsWebSocket(url);
|
|
9075
|
+
}
|
|
9076
|
+
send(data) {
|
|
9077
|
+
this.socket.send(data);
|
|
9078
|
+
}
|
|
9079
|
+
close(code, reason) {
|
|
9080
|
+
this.socket.close(code, reason);
|
|
9081
|
+
}
|
|
9082
|
+
addEventListener(type, listener) {
|
|
9083
|
+
if (type === "message") {
|
|
9084
|
+
this.socket.on("message", (data) => listener({ data: messageToString(data) }));
|
|
9085
|
+
return;
|
|
9086
|
+
}
|
|
9087
|
+
this.socket.on(type, () => listener({}));
|
|
9088
|
+
}
|
|
9089
|
+
};
|
|
9090
|
+
}
|
|
9091
|
+
function resolveWebSocket(ctor) {
|
|
9092
|
+
if (ctor) return ctor;
|
|
9093
|
+
const globalWs = globalThis.WebSocket;
|
|
9094
|
+
if (globalWs) return globalWs;
|
|
9095
|
+
return nodeWebSocketCtor();
|
|
9096
|
+
}
|
|
9097
|
+
function wait(ms, signal) {
|
|
9098
|
+
return new Promise((resolve, reject) => {
|
|
9099
|
+
if (signal?.aborted) {
|
|
9100
|
+
reject(new Error("aborted"));
|
|
9101
|
+
return;
|
|
9102
|
+
}
|
|
9103
|
+
const t = setTimeout(resolve, ms);
|
|
9104
|
+
signal?.addEventListener(
|
|
9105
|
+
"abort",
|
|
9106
|
+
() => {
|
|
9107
|
+
clearTimeout(t);
|
|
9108
|
+
reject(new Error("aborted"));
|
|
9109
|
+
},
|
|
9110
|
+
{ once: true }
|
|
9111
|
+
);
|
|
9112
|
+
});
|
|
9113
|
+
}
|
|
9114
|
+
async function runSlackSocketMode(opts) {
|
|
9115
|
+
const log = opts.onLog ?? (() => void 0);
|
|
9116
|
+
const Ws = resolveWebSocket(opts.WebSocketImpl);
|
|
9117
|
+
let backoff = BACKOFF_START_MS;
|
|
9118
|
+
while (!opts.signal?.aborted) {
|
|
9119
|
+
let ws = null;
|
|
9120
|
+
try {
|
|
9121
|
+
const url = await openSlackSocketUrl(opts.appToken, opts.fetchImpl);
|
|
9122
|
+
ws = new Ws(url);
|
|
9123
|
+
const session = connectSession(ws, opts, log);
|
|
9124
|
+
backoff = BACKOFF_START_MS;
|
|
9125
|
+
await session;
|
|
9126
|
+
} catch (err) {
|
|
9127
|
+
if (opts.signal?.aborted) return;
|
|
9128
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
9129
|
+
if (msg === "aborted") return;
|
|
9130
|
+
log(`socket error: ${msg}`);
|
|
9131
|
+
} finally {
|
|
9132
|
+
try {
|
|
9133
|
+
ws?.close();
|
|
9134
|
+
} catch {
|
|
9135
|
+
}
|
|
9136
|
+
}
|
|
9137
|
+
if (opts.signal?.aborted) return;
|
|
9138
|
+
log(`reconnect in ${Math.round(backoff / 1e3)}s`);
|
|
9139
|
+
try {
|
|
9140
|
+
await wait(backoff, opts.signal);
|
|
9141
|
+
} catch {
|
|
9142
|
+
return;
|
|
9143
|
+
}
|
|
9144
|
+
backoff = Math.min(backoff * 2, BACKOFF_MAX_MS);
|
|
9145
|
+
}
|
|
9146
|
+
}
|
|
9147
|
+
function connectSession(ws, opts, log) {
|
|
9148
|
+
return new Promise((resolve, reject) => {
|
|
9149
|
+
let settled = false;
|
|
9150
|
+
const finish = (err) => {
|
|
9151
|
+
if (settled) return;
|
|
9152
|
+
settled = true;
|
|
9153
|
+
opts.signal?.removeEventListener("abort", onAbort);
|
|
9154
|
+
if (err) reject(err);
|
|
9155
|
+
else resolve();
|
|
9156
|
+
};
|
|
9157
|
+
const onAbort = () => {
|
|
9158
|
+
try {
|
|
9159
|
+
ws.close();
|
|
9160
|
+
} catch {
|
|
9161
|
+
}
|
|
9162
|
+
finish();
|
|
9163
|
+
};
|
|
9164
|
+
opts.signal?.addEventListener("abort", onAbort, { once: true });
|
|
9165
|
+
ws.addEventListener("open", () => {
|
|
9166
|
+
log("Socket Mode connected");
|
|
9167
|
+
});
|
|
9168
|
+
ws.addEventListener("error", () => {
|
|
9169
|
+
finish(new Error("WebSocket error"));
|
|
9170
|
+
});
|
|
9171
|
+
ws.addEventListener("close", () => {
|
|
9172
|
+
finish();
|
|
9173
|
+
});
|
|
9174
|
+
ws.addEventListener("message", (ev) => {
|
|
9175
|
+
const raw = typeof ev.data === "string" ? ev.data : String(ev.data ?? "");
|
|
9176
|
+
const frame = parseSlackSocketFrame(raw);
|
|
9177
|
+
if (!frame) return;
|
|
9178
|
+
if (frame.envelope_id) {
|
|
9179
|
+
try {
|
|
9180
|
+
ws.send(JSON.stringify({ envelope_id: frame.envelope_id }));
|
|
9181
|
+
} catch {
|
|
9182
|
+
}
|
|
9183
|
+
}
|
|
9184
|
+
if (frame.type === "hello") {
|
|
9185
|
+
log("Socket Mode hello");
|
|
9186
|
+
return;
|
|
7281
9187
|
}
|
|
7282
|
-
|
|
7283
|
-
|
|
7284
|
-
|
|
7285
|
-
|
|
7286
|
-
|
|
7287
|
-
|
|
7288
|
-
|
|
7289
|
-
},
|
|
7290
|
-
async ({ agent, repoPath }) => {
|
|
7291
|
-
const issues = await listLinearIssues(agent, repoPath);
|
|
7292
|
-
return { content: [{ type: "text", text: JSON.stringify(issues, null, 2) }] };
|
|
9188
|
+
if (frame.type === "disconnect") {
|
|
9189
|
+
log(`socket disconnect: ${frame.reason || "refresh"}`);
|
|
9190
|
+
try {
|
|
9191
|
+
ws.close();
|
|
9192
|
+
} catch {
|
|
9193
|
+
}
|
|
9194
|
+
return;
|
|
7293
9195
|
}
|
|
7294
|
-
|
|
7295
|
-
|
|
7296
|
-
|
|
7297
|
-
|
|
9196
|
+
const inbound = inboundFromSocketFrame(frame);
|
|
9197
|
+
if (!inbound) return;
|
|
9198
|
+
void Promise.resolve(opts.onEvent(inbound)).catch((err) => {
|
|
9199
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
9200
|
+
log(`event error: ${msg}`);
|
|
9201
|
+
});
|
|
9202
|
+
});
|
|
9203
|
+
});
|
|
7298
9204
|
}
|
|
7299
9205
|
|
|
7300
|
-
// src/
|
|
7301
|
-
|
|
7302
|
-
|
|
7303
|
-
|
|
7304
|
-
|
|
7305
|
-
|
|
7306
|
-
|
|
7307
|
-
|
|
7308
|
-
|
|
7309
|
-
|
|
9206
|
+
// src/slack/reply-target.ts
|
|
9207
|
+
import { existsSync as existsSync15, readFileSync as readFileSync10 } from "fs";
|
|
9208
|
+
import { join as join13 } from "path";
|
|
9209
|
+
function storePath3() {
|
|
9210
|
+
return join13(appDataDir(), "slack-reply-to.json");
|
|
9211
|
+
}
|
|
9212
|
+
function readStore3() {
|
|
9213
|
+
const path = storePath3();
|
|
9214
|
+
if (!existsSync15(path)) return {};
|
|
9215
|
+
try {
|
|
9216
|
+
const parsed = isSecureFileEncrypted(path) ? readSecureJson(path) : JSON.parse(readFileSync10(path, "utf8"));
|
|
9217
|
+
return parsed?.targets && typeof parsed.targets === "object" ? parsed.targets : {};
|
|
9218
|
+
} catch {
|
|
9219
|
+
return {};
|
|
7310
9220
|
}
|
|
7311
|
-
return `${err.message}${detail} (${url})`;
|
|
7312
9221
|
}
|
|
7313
|
-
|
|
7314
|
-
|
|
7315
|
-
|
|
7316
|
-
|
|
7317
|
-
|
|
7318
|
-
|
|
7319
|
-
|
|
7320
|
-
|
|
7321
|
-
|
|
7322
|
-
|
|
7323
|
-
|
|
7324
|
-
|
|
9222
|
+
function setSlackReplyTarget(target) {
|
|
9223
|
+
const targets = readStore3();
|
|
9224
|
+
targets[target.threadId] = target;
|
|
9225
|
+
writePrivateFile(storePath3(), `${JSON.stringify({ targets }, null, 2)}
|
|
9226
|
+
`);
|
|
9227
|
+
}
|
|
9228
|
+
function getSlackReplyTarget(threadId) {
|
|
9229
|
+
return readStore3()[threadId] ?? null;
|
|
9230
|
+
}
|
|
9231
|
+
|
|
9232
|
+
// src/slack/relay-protocol.ts
|
|
9233
|
+
import { randomBytes as randomBytes3 } from "crypto";
|
|
9234
|
+
function slackRelaySessionKey(teamId, userId, deviceId) {
|
|
9235
|
+
return `${teamId.trim()}:${userId.trim()}:${deviceId.trim()}`;
|
|
9236
|
+
}
|
|
9237
|
+
function slackRelayUserKey(teamId, userId) {
|
|
9238
|
+
return `${teamId.trim()}:${userId.trim()}`;
|
|
9239
|
+
}
|
|
9240
|
+
function newSlackRelayEventId() {
|
|
9241
|
+
return randomBytes3(12).toString("hex");
|
|
9242
|
+
}
|
|
9243
|
+
function parseSlackRelayClientMessage(raw) {
|
|
9244
|
+
try {
|
|
9245
|
+
const parsed = JSON.parse(raw);
|
|
9246
|
+
if (!parsed || typeof parsed !== "object" || !("type" in parsed)) return null;
|
|
9247
|
+
if (parsed.type === "ping") return { type: "ping" };
|
|
9248
|
+
if (parsed.type === "claim") {
|
|
9249
|
+
const eventId = typeof parsed.eventId === "string" ? parsed.eventId.trim() : "";
|
|
9250
|
+
if (!eventId) return null;
|
|
9251
|
+
return { type: "claim", eventId };
|
|
9252
|
+
}
|
|
9253
|
+
if (parsed.type === "register") {
|
|
9254
|
+
const teamId = typeof parsed.teamId === "string" ? parsed.teamId.trim() : "";
|
|
9255
|
+
const userId = typeof parsed.userId === "string" ? parsed.userId.trim() : "";
|
|
9256
|
+
const deviceId = typeof parsed.deviceId === "string" ? parsed.deviceId.trim() : "";
|
|
9257
|
+
const deviceLabel = typeof parsed.deviceLabel === "string" ? parsed.deviceLabel.trim() : void 0;
|
|
9258
|
+
const botToken = typeof parsed.botToken === "string" ? parsed.botToken.trim() : "";
|
|
9259
|
+
const userToken = typeof parsed.userToken === "string" ? parsed.userToken.trim() : "";
|
|
9260
|
+
if (!teamId || !userId || !deviceId || !botToken || !userToken) return null;
|
|
9261
|
+
return {
|
|
9262
|
+
type: "register",
|
|
9263
|
+
teamId,
|
|
9264
|
+
userId,
|
|
9265
|
+
deviceId,
|
|
9266
|
+
deviceLabel: deviceLabel || void 0,
|
|
9267
|
+
botToken,
|
|
9268
|
+
userToken
|
|
9269
|
+
};
|
|
7325
9270
|
}
|
|
9271
|
+
return null;
|
|
9272
|
+
} catch {
|
|
9273
|
+
return null;
|
|
7326
9274
|
}
|
|
7327
|
-
|
|
7328
|
-
|
|
7329
|
-
|
|
7330
|
-
|
|
7331
|
-
|
|
7332
|
-
|
|
7333
|
-
|
|
7334
|
-
|
|
7335
|
-
|
|
7336
|
-
|
|
7337
|
-
|
|
7338
|
-
|
|
7339
|
-
}
|
|
7340
|
-
if (
|
|
7341
|
-
|
|
7342
|
-
|
|
7343
|
-
|
|
7344
|
-
|
|
7345
|
-
|
|
7346
|
-
|
|
7347
|
-
}
|
|
7348
|
-
}
|
|
7349
|
-
|
|
9275
|
+
}
|
|
9276
|
+
function parseSlackRelayServerMessage(raw) {
|
|
9277
|
+
try {
|
|
9278
|
+
const parsed = JSON.parse(raw);
|
|
9279
|
+
if (!parsed || typeof parsed !== "object" || !("type" in parsed)) return null;
|
|
9280
|
+
if (parsed.type === "pong") return { type: "pong" };
|
|
9281
|
+
if (parsed.type === "registered") {
|
|
9282
|
+
const teamId = typeof parsed.teamId === "string" ? parsed.teamId.trim() : "";
|
|
9283
|
+
const userId = typeof parsed.userId === "string" ? parsed.userId.trim() : "";
|
|
9284
|
+
const deviceId = typeof parsed.deviceId === "string" ? parsed.deviceId.trim() : "";
|
|
9285
|
+
if (!teamId || !userId) return null;
|
|
9286
|
+
return { type: "registered", teamId, userId, deviceId: deviceId || "unknown" };
|
|
9287
|
+
}
|
|
9288
|
+
if (parsed.type === "claim_ok" || parsed.type === "claim_denied") {
|
|
9289
|
+
const eventId = typeof parsed.eventId === "string" ? parsed.eventId.trim() : "";
|
|
9290
|
+
if (!eventId) return null;
|
|
9291
|
+
return { type: parsed.type, eventId };
|
|
9292
|
+
}
|
|
9293
|
+
if (parsed.type === "error") {
|
|
9294
|
+
const message = typeof parsed.message === "string" ? parsed.message : "error";
|
|
9295
|
+
return { type: "error", message };
|
|
9296
|
+
}
|
|
9297
|
+
if (parsed.type === "event") {
|
|
9298
|
+
const eventId = typeof parsed.eventId === "string" && parsed.eventId.trim() ? parsed.eventId.trim() : `legacy-${Date.now()}`;
|
|
9299
|
+
const msg = parsed.message;
|
|
9300
|
+
if (!msg || typeof msg !== "object") return null;
|
|
9301
|
+
if (typeof msg.teamId !== "string" || typeof msg.channelId !== "string" || typeof msg.ts !== "string" || typeof msg.text !== "string" || msg.kind !== "dm" && msg.kind !== "mention") {
|
|
9302
|
+
return null;
|
|
9303
|
+
}
|
|
9304
|
+
return { type: "event", eventId, message: msg };
|
|
7350
9305
|
}
|
|
7351
|
-
|
|
7352
|
-
|
|
7353
|
-
|
|
9306
|
+
return null;
|
|
9307
|
+
} catch {
|
|
9308
|
+
return null;
|
|
9309
|
+
}
|
|
9310
|
+
}
|
|
9311
|
+
|
|
9312
|
+
// src/slack/relay-client.ts
|
|
9313
|
+
var BACKOFF_START_MS2 = 1e3;
|
|
9314
|
+
var BACKOFF_MAX_MS2 = 3e4;
|
|
9315
|
+
function wait2(ms, signal) {
|
|
9316
|
+
return new Promise((resolve, reject) => {
|
|
9317
|
+
if (signal?.aborted) {
|
|
9318
|
+
reject(new Error("aborted"));
|
|
9319
|
+
return;
|
|
7354
9320
|
}
|
|
7355
|
-
|
|
9321
|
+
const t = setTimeout(resolve, ms);
|
|
9322
|
+
signal?.addEventListener(
|
|
9323
|
+
"abort",
|
|
9324
|
+
() => {
|
|
9325
|
+
clearTimeout(t);
|
|
9326
|
+
reject(new Error("aborted"));
|
|
9327
|
+
},
|
|
9328
|
+
{ once: true }
|
|
9329
|
+
);
|
|
9330
|
+
});
|
|
9331
|
+
}
|
|
9332
|
+
function send(ws, msg) {
|
|
9333
|
+
ws.send(JSON.stringify(msg));
|
|
9334
|
+
}
|
|
9335
|
+
async function runSlackRelayClient(opts) {
|
|
9336
|
+
const log = opts.onLog ?? (() => void 0);
|
|
9337
|
+
const Ws = resolveWebSocket(opts.WebSocketImpl);
|
|
9338
|
+
const url = opts.url.trim();
|
|
9339
|
+
const deviceId = opts.deviceId.trim();
|
|
9340
|
+
if (!url) throw new Error("Slack relay URL is empty");
|
|
9341
|
+
if (!deviceId) throw new Error("Slack relay needs a deviceId for this Mac");
|
|
9342
|
+
if (opts.workspaces.length === 0) {
|
|
9343
|
+
throw new Error(
|
|
9344
|
+
"Slack relay needs a connected workspace with bot + user tokens (Add via browser)."
|
|
9345
|
+
);
|
|
7356
9346
|
}
|
|
7357
|
-
|
|
7358
|
-
|
|
7359
|
-
|
|
7360
|
-
const clientId = this.cfg.oauth_client_id || "brightsy-cli";
|
|
7361
|
-
const url = `${this.endpoint}/oauth/token`;
|
|
7362
|
-
let res;
|
|
9347
|
+
let backoff = BACKOFF_START_MS2;
|
|
9348
|
+
while (!opts.signal?.aborted) {
|
|
9349
|
+
let ws = null;
|
|
7363
9350
|
try {
|
|
7364
|
-
|
|
7365
|
-
|
|
7366
|
-
|
|
7367
|
-
body: new URLSearchParams({
|
|
7368
|
-
grant_type: "refresh_token",
|
|
7369
|
-
refresh_token: this.cfg.refresh_token,
|
|
7370
|
-
client_id: clientId
|
|
7371
|
-
})
|
|
7372
|
-
});
|
|
9351
|
+
ws = new Ws(url);
|
|
9352
|
+
await connectSession2(ws, opts, log);
|
|
9353
|
+
backoff = BACKOFF_START_MS2;
|
|
7373
9354
|
} catch (err) {
|
|
7374
|
-
|
|
9355
|
+
if (opts.signal?.aborted) return;
|
|
9356
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
9357
|
+
if (msg === "aborted") return;
|
|
9358
|
+
log(`relay error: ${msg}`);
|
|
9359
|
+
} finally {
|
|
9360
|
+
try {
|
|
9361
|
+
ws?.close();
|
|
9362
|
+
} catch {
|
|
9363
|
+
}
|
|
7375
9364
|
}
|
|
7376
|
-
if (
|
|
7377
|
-
|
|
7378
|
-
|
|
7379
|
-
|
|
7380
|
-
|
|
7381
|
-
|
|
7382
|
-
this.cfg.expires_at = Date.now() + data.expires_in * 1e3;
|
|
9365
|
+
if (opts.signal?.aborted) return;
|
|
9366
|
+
log(`relay reconnect in ${Math.round(backoff / 1e3)}s`);
|
|
9367
|
+
try {
|
|
9368
|
+
await wait2(backoff, opts.signal);
|
|
9369
|
+
} catch {
|
|
9370
|
+
return;
|
|
7383
9371
|
}
|
|
7384
|
-
|
|
9372
|
+
backoff = Math.min(backoff * 2, BACKOFF_MAX_MS2);
|
|
7385
9373
|
}
|
|
7386
|
-
|
|
7387
|
-
|
|
7388
|
-
|
|
9374
|
+
}
|
|
9375
|
+
function connectSession2(ws, opts, log) {
|
|
9376
|
+
return new Promise((resolve, reject) => {
|
|
9377
|
+
let settled = false;
|
|
9378
|
+
const pending = /* @__PURE__ */ new Map();
|
|
9379
|
+
const allowedUsers = new Set(
|
|
9380
|
+
opts.workspaces.map((w) => `${w.teamId.trim()}:${w.userId.trim()}`)
|
|
7389
9381
|
);
|
|
7390
|
-
|
|
7391
|
-
|
|
7392
|
-
|
|
7393
|
-
|
|
7394
|
-
|
|
7395
|
-
|
|
7396
|
-
|
|
7397
|
-
|
|
9382
|
+
const finish = (err) => {
|
|
9383
|
+
if (settled) return;
|
|
9384
|
+
settled = true;
|
|
9385
|
+
pending.clear();
|
|
9386
|
+
opts.signal?.removeEventListener("abort", onAbort);
|
|
9387
|
+
if (err) reject(err);
|
|
9388
|
+
else resolve();
|
|
9389
|
+
};
|
|
9390
|
+
const onAbort = () => {
|
|
9391
|
+
try {
|
|
9392
|
+
ws.close();
|
|
9393
|
+
} catch {
|
|
9394
|
+
}
|
|
9395
|
+
finish();
|
|
9396
|
+
};
|
|
9397
|
+
opts.signal?.addEventListener("abort", onAbort, { once: true });
|
|
9398
|
+
ws.addEventListener("open", () => {
|
|
9399
|
+
const label = opts.deviceLabel?.trim() || opts.deviceId.trim();
|
|
9400
|
+
log(`Relay connected \xB7 ${label}`);
|
|
9401
|
+
for (const wsInfo of opts.workspaces) {
|
|
9402
|
+
send(ws, {
|
|
9403
|
+
type: "register",
|
|
9404
|
+
teamId: wsInfo.teamId,
|
|
9405
|
+
userId: wsInfo.userId,
|
|
9406
|
+
deviceId: opts.deviceId.trim(),
|
|
9407
|
+
deviceLabel: opts.deviceLabel?.trim() || void 0,
|
|
9408
|
+
botToken: wsInfo.botToken,
|
|
9409
|
+
userToken: wsInfo.userToken
|
|
9410
|
+
});
|
|
7398
9411
|
}
|
|
7399
9412
|
});
|
|
7400
|
-
|
|
7401
|
-
|
|
7402
|
-
const params = new URLSearchParams();
|
|
7403
|
-
if (status) params.set("status", status);
|
|
7404
|
-
params.set("accountId", this.accountId);
|
|
7405
|
-
const q = `?${params.toString()}`;
|
|
7406
|
-
const data = await this.request(
|
|
7407
|
-
`/api/v1beta/desktop/tasks${q}`
|
|
7408
|
-
);
|
|
7409
|
-
return data.tasks ?? [];
|
|
7410
|
-
}
|
|
7411
|
-
async approveTask(taskId) {
|
|
7412
|
-
await this.request(`/api/v1beta/desktop/tasks/${taskId}`, {
|
|
7413
|
-
method: "PATCH",
|
|
7414
|
-
body: { action: "approve" }
|
|
9413
|
+
ws.addEventListener("error", () => {
|
|
9414
|
+
finish(new Error("WebSocket error"));
|
|
7415
9415
|
});
|
|
7416
|
-
|
|
7417
|
-
|
|
7418
|
-
await this.request(`/api/v1beta/desktop/tasks/${taskId}`, {
|
|
7419
|
-
method: "PATCH",
|
|
7420
|
-
body: { task_status: "running" }
|
|
9416
|
+
ws.addEventListener("close", () => {
|
|
9417
|
+
finish();
|
|
7421
9418
|
});
|
|
7422
|
-
|
|
7423
|
-
|
|
7424
|
-
|
|
7425
|
-
|
|
7426
|
-
|
|
9419
|
+
ws.addEventListener("message", (ev) => {
|
|
9420
|
+
const raw = typeof ev.data === "string" ? ev.data : String(ev.data ?? "");
|
|
9421
|
+
const msg = parseSlackRelayServerMessage(raw);
|
|
9422
|
+
if (!msg) return;
|
|
9423
|
+
if (msg.type === "pong") return;
|
|
9424
|
+
if (msg.type === "error") {
|
|
9425
|
+
log(`relay error: ${msg.message}`);
|
|
9426
|
+
return;
|
|
9427
|
+
}
|
|
9428
|
+
if (msg.type === "registered") {
|
|
9429
|
+
log(
|
|
9430
|
+
msg.deviceId && msg.deviceId !== "unknown" ? `Relay registered ${msg.teamId}/${msg.userId} \xB7 device ${msg.deviceId.slice(0, 8)}\u2026` : `Relay registered ${msg.teamId}/${msg.userId}`
|
|
9431
|
+
);
|
|
9432
|
+
return;
|
|
9433
|
+
}
|
|
9434
|
+
if (msg.type === "event") {
|
|
9435
|
+
const key = `${msg.message.teamId.trim()}:${(msg.message.userId ?? "").trim()}`;
|
|
9436
|
+
if (!allowedUsers.has(key)) {
|
|
9437
|
+
log(`relay skip: event for ${key} not owned by this desktop`);
|
|
9438
|
+
return;
|
|
9439
|
+
}
|
|
9440
|
+
pending.set(msg.eventId, msg.message);
|
|
9441
|
+
send(ws, { type: "claim", eventId: msg.eventId });
|
|
9442
|
+
const eventId = msg.eventId;
|
|
9443
|
+
setTimeout(() => {
|
|
9444
|
+
const inbound = pending.get(eventId);
|
|
9445
|
+
if (!inbound) return;
|
|
9446
|
+
pending.delete(eventId);
|
|
9447
|
+
void Promise.resolve(opts.onEvent(inbound)).catch((err) => {
|
|
9448
|
+
const errMsg = err instanceof Error ? err.message : String(err);
|
|
9449
|
+
log(`event error: ${errMsg}`);
|
|
9450
|
+
});
|
|
9451
|
+
}, 400);
|
|
9452
|
+
return;
|
|
9453
|
+
}
|
|
9454
|
+
if (msg.type === "claim_denied") {
|
|
9455
|
+
pending.delete(msg.eventId);
|
|
9456
|
+
return;
|
|
9457
|
+
}
|
|
9458
|
+
if (msg.type === "claim_ok") {
|
|
9459
|
+
const inbound = pending.get(msg.eventId);
|
|
9460
|
+
pending.delete(msg.eventId);
|
|
9461
|
+
if (!inbound) return;
|
|
9462
|
+
void Promise.resolve(opts.onEvent(inbound)).catch((err) => {
|
|
9463
|
+
const errMsg = err instanceof Error ? err.message : String(err);
|
|
9464
|
+
log(`event error: ${errMsg}`);
|
|
9465
|
+
});
|
|
9466
|
+
}
|
|
7427
9467
|
});
|
|
7428
|
-
}
|
|
7429
|
-
};
|
|
7430
|
-
function taskMessageText(task) {
|
|
7431
|
-
const parts = task.message?.parts ?? [];
|
|
7432
|
-
const text = parts.find((p) => p.kind === "text")?.text ?? "";
|
|
7433
|
-
return text.trim();
|
|
9468
|
+
});
|
|
7434
9469
|
}
|
|
7435
9470
|
|
|
7436
|
-
// src/
|
|
7437
|
-
var
|
|
7438
|
-
|
|
7439
|
-
|
|
7440
|
-
|
|
7441
|
-
|
|
9471
|
+
// src/slack/listen.ts
|
|
9472
|
+
var SLACK_LISTEN_BUSY_REPLY = [
|
|
9473
|
+
"Sideboard is busy with an in-progress turn.",
|
|
9474
|
+
"I did not start a new request.",
|
|
9475
|
+
'Send "stop" to interrupt, or try again when idle.'
|
|
9476
|
+
].join(" ");
|
|
9477
|
+
var SLACK_LISTEN_STOPPED_REPLY = "Sideboard stopped the in-progress turn. Send another message when you want to continue.";
|
|
9478
|
+
var SLACK_LISTEN_TIMEOUT_REPLY = [
|
|
9479
|
+
"Sideboard timed out waiting for the local agent turn to finish.",
|
|
9480
|
+
'Send "stop" to interrupt, or try again.'
|
|
9481
|
+
].join(" ");
|
|
9482
|
+
var handleChain = Promise.resolve();
|
|
9483
|
+
function enqueueHandle(fn) {
|
|
9484
|
+
const run2 = handleChain.then(fn, fn);
|
|
9485
|
+
handleChain = run2.then(
|
|
7442
9486
|
() => void 0,
|
|
7443
9487
|
() => void 0
|
|
7444
9488
|
);
|
|
7445
9489
|
return run2;
|
|
7446
9490
|
}
|
|
7447
|
-
function
|
|
7448
|
-
|
|
7449
|
-
|
|
9491
|
+
function refreshWorkspaces2() {
|
|
9492
|
+
return getOrchestrator().listWorkspaces();
|
|
9493
|
+
}
|
|
9494
|
+
function formatSlackInboundPrompt(msg) {
|
|
9495
|
+
const kind = msg.kind === "mention" ? "Slack @mention" : "Slack DM";
|
|
9496
|
+
return `${kind}
|
|
9497
|
+
|
|
9498
|
+
${msg.text}`;
|
|
9499
|
+
}
|
|
9500
|
+
function isSlackInboundUserPrompt(text2) {
|
|
9501
|
+
return text2.startsWith("Slack DM\n") || text2.startsWith("Slack @mention\n");
|
|
9502
|
+
}
|
|
9503
|
+
function formatSlackSignedReply(deviceLabel, text2) {
|
|
9504
|
+
const label = deviceLabel.trim();
|
|
9505
|
+
const body = text2.trim();
|
|
9506
|
+
if (!body) return body;
|
|
9507
|
+
if (!label) return body;
|
|
9508
|
+
const head = `${label}:`;
|
|
9509
|
+
if (body.slice(0, head.length).toLowerCase() === head.toLowerCase()) {
|
|
9510
|
+
return body;
|
|
9511
|
+
}
|
|
9512
|
+
return `${label}: ${body}`;
|
|
9513
|
+
}
|
|
9514
|
+
function signForThisMac(text2) {
|
|
9515
|
+
return formatSlackSignedReply(ensureSlackDeviceIdentity().deviceLabel, text2);
|
|
9516
|
+
}
|
|
9517
|
+
function slackReplyThreadTs(msg) {
|
|
9518
|
+
if (msg.threadTs && msg.threadTs !== msg.ts) return msg.threadTs;
|
|
9519
|
+
if (msg.kind === "mention") return msg.ts;
|
|
9520
|
+
return void 0;
|
|
9521
|
+
}
|
|
9522
|
+
var SLACK_SEEN_REACTION = "+1";
|
|
9523
|
+
function writeTokenForTeam(teamId) {
|
|
9524
|
+
const workspaces = listSlackWorkspacesRaw();
|
|
9525
|
+
const ws = workspaces.find((w) => w.team_id === teamId);
|
|
9526
|
+
if (!ws) {
|
|
9527
|
+
const connected = workspaces.map((w) => `${w.team_name} (${w.team_id})`).join(", ");
|
|
9528
|
+
throw new Error(
|
|
9529
|
+
connected ? `No connected Slack workspace for team ${teamId}. Connected: ${connected}` : `No connected Slack workspace for team ${teamId}`
|
|
9530
|
+
);
|
|
9531
|
+
}
|
|
9532
|
+
return slackTokenFor(ws, "write");
|
|
9533
|
+
}
|
|
9534
|
+
async function ackSlackInboundSeen(msg, opts) {
|
|
9535
|
+
const log = opts.onLog ?? (() => void 0);
|
|
9536
|
+
try {
|
|
9537
|
+
if (opts.addReaction) {
|
|
9538
|
+
await opts.addReaction(msg, SLACK_SEEN_REACTION);
|
|
9539
|
+
return;
|
|
9540
|
+
}
|
|
9541
|
+
await slackApi(
|
|
9542
|
+
writeTokenForTeam(msg.teamId),
|
|
9543
|
+
"reactions.add",
|
|
9544
|
+
{
|
|
9545
|
+
channel: msg.channelId,
|
|
9546
|
+
timestamp: msg.ts,
|
|
9547
|
+
name: SLACK_SEEN_REACTION
|
|
9548
|
+
},
|
|
9549
|
+
opts.fetchImpl
|
|
9550
|
+
);
|
|
9551
|
+
} catch (err) {
|
|
9552
|
+
if (err instanceof SlackApiError && err.slackError === "already_reacted") return;
|
|
9553
|
+
const errMsg = err instanceof Error ? err.message : String(err);
|
|
9554
|
+
const hint = err instanceof SlackApiError && err.slackError === "missing_scope" ? " \u2014 reconnect Slack in Account to grant reactions:write" : "";
|
|
9555
|
+
log(`react error: ${errMsg}${hint}`);
|
|
9556
|
+
}
|
|
9557
|
+
}
|
|
9558
|
+
async function postSlackText(target, text2, opts) {
|
|
9559
|
+
const stub = {
|
|
9560
|
+
teamId: target.teamId,
|
|
9561
|
+
channelId: target.channelId,
|
|
9562
|
+
ts: target.threadTs ?? "",
|
|
9563
|
+
threadTs: target.threadTs,
|
|
9564
|
+
text: "",
|
|
9565
|
+
kind: "dm"
|
|
9566
|
+
};
|
|
9567
|
+
if (opts.postReply) {
|
|
9568
|
+
await opts.postReply(stub, text2);
|
|
9569
|
+
return;
|
|
9570
|
+
}
|
|
9571
|
+
const token = writeTokenForTeam(target.teamId);
|
|
9572
|
+
await slackApi(
|
|
9573
|
+
token,
|
|
9574
|
+
"chat.postMessage",
|
|
9575
|
+
{
|
|
9576
|
+
channel: target.channelId,
|
|
9577
|
+
text: text2,
|
|
9578
|
+
thread_ts: target.threadTs
|
|
9579
|
+
},
|
|
9580
|
+
opts.fetchImpl
|
|
9581
|
+
);
|
|
9582
|
+
}
|
|
9583
|
+
async function postSlackReply(msg, text2, opts) {
|
|
9584
|
+
await postSlackText(
|
|
9585
|
+
{
|
|
9586
|
+
teamId: msg.teamId,
|
|
9587
|
+
channelId: msg.channelId,
|
|
9588
|
+
threadTs: slackReplyThreadTs(msg)
|
|
9589
|
+
},
|
|
9590
|
+
signForThisMac(text2),
|
|
9591
|
+
opts
|
|
9592
|
+
);
|
|
9593
|
+
}
|
|
9594
|
+
var lastRelayed = /* @__PURE__ */ new Map();
|
|
9595
|
+
function markRelayed(threadId, text2) {
|
|
9596
|
+
lastRelayed.set(threadId, `${threadId}:${text2}`);
|
|
7450
9597
|
}
|
|
7451
|
-
|
|
9598
|
+
function alreadyRelayed(threadId, text2) {
|
|
9599
|
+
return lastRelayed.get(threadId) === `${threadId}:${text2}`;
|
|
9600
|
+
}
|
|
9601
|
+
async function relayCoordinatorReplyToSlack(threadId, opts) {
|
|
9602
|
+
const target = getSlackReplyTarget(threadId);
|
|
9603
|
+
if (!target) return;
|
|
9604
|
+
const thread = readThread(threadId);
|
|
9605
|
+
const lastUser = thread ? [...thread.messages].reverse().find((m) => m.role === "user") : void 0;
|
|
9606
|
+
if (lastUser && isSlackInboundUserPrompt(lastUser.text)) return;
|
|
9607
|
+
const result = getOrchestrator().getTurnResult(threadId);
|
|
9608
|
+
const text2 = result.text.trim();
|
|
9609
|
+
if (!text2) return;
|
|
9610
|
+
if (alreadyRelayed(threadId, text2)) return;
|
|
9611
|
+
markRelayed(threadId, text2);
|
|
7452
9612
|
const log = opts.onLog ?? (() => void 0);
|
|
7453
|
-
|
|
7454
|
-
|
|
7455
|
-
log(`
|
|
7456
|
-
|
|
7457
|
-
|
|
7458
|
-
|
|
7459
|
-
|
|
9613
|
+
try {
|
|
9614
|
+
await postSlackText(target, signForThisMac(text2), opts);
|
|
9615
|
+
log(`replied ${target.threadTs ?? target.channelId} (${text2.length} chars)`);
|
|
9616
|
+
} catch (err) {
|
|
9617
|
+
lastRelayed.delete(threadId);
|
|
9618
|
+
const errMsg = err instanceof Error ? err.message : String(err);
|
|
9619
|
+
log(`relay error: ${errMsg}`);
|
|
7460
9620
|
}
|
|
7461
|
-
|
|
9621
|
+
}
|
|
9622
|
+
async function handleSlackInbound(msg, opts) {
|
|
9623
|
+
const log = opts.onLog ?? (() => void 0);
|
|
9624
|
+
await ackSlackInboundSeen(msg, opts);
|
|
9625
|
+
const agent = coerceOrchestratorAgent(opts.agent ?? getDefaultAgent());
|
|
9626
|
+
const workspaces = refreshWorkspaces2();
|
|
7462
9627
|
for (const ws of workspaces) {
|
|
7463
9628
|
await getOrchestrator().reconcile(ws.path).catch(() => void 0);
|
|
7464
9629
|
}
|
|
7465
9630
|
const inventory = await enrichWorkspacesWithGithub(workspaces);
|
|
7466
|
-
const
|
|
7467
|
-
|
|
7468
|
-
|
|
7469
|
-
|
|
9631
|
+
const userId = msg.userId?.trim();
|
|
9632
|
+
if (!userId) {
|
|
9633
|
+
log(`skip ${msg.kind} ${msg.ts}: missing Slack user id`);
|
|
9634
|
+
return;
|
|
9635
|
+
}
|
|
9636
|
+
const coordinator = ensureSlackCoordinator(msg.teamId, userId, agent);
|
|
9637
|
+
const fresh = readThread(coordinator.id) ?? coordinator;
|
|
9638
|
+
if (isSlackStopCommand(msg.text)) {
|
|
7470
9639
|
try {
|
|
7471
9640
|
getOrchestrator().stop(fresh.id);
|
|
7472
|
-
log(`
|
|
9641
|
+
log(`stop ${msg.kind} ${msg.ts} \u2192 coordinator ${fresh.id.slice(0, 8)}`);
|
|
7473
9642
|
} catch (err) {
|
|
7474
|
-
const
|
|
7475
|
-
log(`
|
|
7476
|
-
}
|
|
7477
|
-
if (!parsed.remainder) {
|
|
7478
|
-
if (task.task_status === "awaiting_confirmation") {
|
|
7479
|
-
await api.approveTask(task.id).catch(() => void 0);
|
|
7480
|
-
}
|
|
7481
|
-
await api.markRunning(task.id).catch(() => void 0);
|
|
7482
|
-
await api.submitResponse(task.id, CLOUD_COORDINATOR_STOPPED_REPLY);
|
|
7483
|
-
log(`replied stopped ${task.id.slice(0, 8)}`);
|
|
7484
|
-
return;
|
|
9643
|
+
const errMsg = err instanceof Error ? err.message : String(err);
|
|
9644
|
+
log(`stop ${msg.ts}: ${errMsg}`);
|
|
7485
9645
|
}
|
|
7486
|
-
|
|
7487
|
-
|
|
9646
|
+
await postSlackReply(msg, SLACK_LISTEN_STOPPED_REPLY, opts);
|
|
9647
|
+
log(`replied stopped ${msg.ts}`);
|
|
9648
|
+
return;
|
|
7488
9649
|
}
|
|
7489
9650
|
if (fresh.status === "running" || fresh.status === "queued") {
|
|
7490
|
-
log(
|
|
7491
|
-
|
|
7492
|
-
);
|
|
7493
|
-
if (task.task_status === "awaiting_confirmation") {
|
|
7494
|
-
await api.approveTask(task.id).catch(() => void 0);
|
|
7495
|
-
}
|
|
7496
|
-
await api.markRunning(task.id).catch(() => void 0);
|
|
7497
|
-
await api.submitResponse(task.id, CLOUD_COORDINATOR_BUSY_REPLY);
|
|
7498
|
-
log(`replied busy ${task.id.slice(0, 8)}`);
|
|
9651
|
+
log(`busy ${msg.kind} ${msg.ts} \u2192 coordinator ${fresh.id.slice(0, 8)} (${fresh.status})`);
|
|
9652
|
+
await postSlackReply(msg, SLACK_LISTEN_BUSY_REPLY, opts);
|
|
9653
|
+
log(`replied busy ${msg.ts}`);
|
|
7499
9654
|
return;
|
|
7500
9655
|
}
|
|
7501
|
-
if (task.task_status === "awaiting_confirmation") {
|
|
7502
|
-
log(`approve ${task.id.slice(0, 8)}`);
|
|
7503
|
-
await api.approveTask(task.id);
|
|
7504
|
-
task = { ...task, task_status: "pending" };
|
|
7505
|
-
}
|
|
7506
9656
|
log(
|
|
7507
|
-
`run ${
|
|
9657
|
+
`run ${msg.kind} ${msg.ts} user ${userId} \u2192 coordinator ${fresh.id.slice(0, 8)} (${inventory.length} workspace${inventory.length === 1 ? "" : "s"})`
|
|
7508
9658
|
);
|
|
7509
|
-
|
|
9659
|
+
const prompt = formatSlackInboundPrompt(msg);
|
|
9660
|
+
setSlackReplyTarget({
|
|
9661
|
+
threadId: fresh.id,
|
|
9662
|
+
teamId: msg.teamId,
|
|
9663
|
+
channelId: msg.channelId,
|
|
9664
|
+
threadTs: slackReplyThreadTs(msg)
|
|
9665
|
+
});
|
|
7510
9666
|
const orch = getOrchestrator();
|
|
7511
|
-
|
|
7512
|
-
coordinatorSystemPrompt({
|
|
7513
|
-
goal: fresh.sourceRef || CLOUD_ORCHESTRATOR_GOAL,
|
|
7514
|
-
parentId: fresh.id,
|
|
7515
|
-
workspaces: inventory,
|
|
7516
|
-
audience: "cloud"
|
|
7517
|
-
}),
|
|
7518
|
-
"",
|
|
7519
|
-
"--- Cloud agent request ---",
|
|
7520
|
-
message
|
|
7521
|
-
].join("\n");
|
|
9667
|
+
let reply;
|
|
7522
9668
|
try {
|
|
7523
9669
|
await orch.send(fresh.id, prompt);
|
|
7524
9670
|
await orch.waitForTurn(fresh.id, 14 * 60 * 1e3);
|
|
7525
|
-
|
|
7526
|
-
const reply = result.text.trim() || `(coordinator finished with status ${result.status}, no text)`;
|
|
7527
|
-
await api.submitResponse(task.id, reply);
|
|
7528
|
-
log(`replied ${task.id.slice(0, 8)} (${reply.length} chars)`);
|
|
9671
|
+
reply = orch.getTurnResult(fresh.id).text.trim();
|
|
7529
9672
|
} catch (err) {
|
|
7530
|
-
const
|
|
7531
|
-
const timedOut = /timed out|timeout/i.test(
|
|
7532
|
-
const
|
|
7533
|
-
await
|
|
7534
|
-
|
|
9673
|
+
const errMsg = err instanceof Error ? err.message : String(err);
|
|
9674
|
+
const timedOut = /timed out|timeout/i.test(errMsg);
|
|
9675
|
+
const errorReply = timedOut ? SLACK_LISTEN_TIMEOUT_REPLY : `Sideboard coordinator error: ${errMsg}`;
|
|
9676
|
+
await postSlackReply(msg, errorReply, opts).catch((postErr) => {
|
|
9677
|
+
const postMsg = postErr instanceof Error ? postErr.message : String(postErr);
|
|
9678
|
+
log(`post error: ${postMsg}`);
|
|
9679
|
+
});
|
|
9680
|
+
log(`replied error ${msg.ts}: ${errMsg}`);
|
|
7535
9681
|
if (!timedOut) throw err;
|
|
9682
|
+
return;
|
|
9683
|
+
}
|
|
9684
|
+
if (!reply) {
|
|
9685
|
+
log(`post error: no agent text to send for ${msg.ts}`);
|
|
9686
|
+
return;
|
|
9687
|
+
}
|
|
9688
|
+
if (alreadyRelayed(fresh.id, reply)) {
|
|
9689
|
+
log(`turn finished ${msg.ts} (already posted)`);
|
|
9690
|
+
return;
|
|
9691
|
+
}
|
|
9692
|
+
try {
|
|
9693
|
+
await postSlackReply(msg, reply, opts);
|
|
9694
|
+
markRelayed(fresh.id, reply);
|
|
9695
|
+
log(`replied ${msg.ts} (${reply.length} chars)`);
|
|
9696
|
+
} catch (err) {
|
|
9697
|
+
const errMsg = err instanceof Error ? err.message : String(err);
|
|
9698
|
+
log(`post error: ${errMsg}`);
|
|
7536
9699
|
}
|
|
7537
9700
|
}
|
|
7538
|
-
|
|
9701
|
+
function ownedSlackUserKeys() {
|
|
9702
|
+
const keys = /* @__PURE__ */ new Set();
|
|
9703
|
+
for (const w of listSlackWorkspacesRaw()) {
|
|
9704
|
+
const team = w.team_id?.trim();
|
|
9705
|
+
const user = w.user_id?.trim();
|
|
9706
|
+
if (team && user) keys.add(`${team}:${user}`);
|
|
9707
|
+
}
|
|
9708
|
+
return keys;
|
|
9709
|
+
}
|
|
9710
|
+
function isInboundForThisDesktop(msg) {
|
|
9711
|
+
const userId = msg.userId?.trim();
|
|
9712
|
+
if (!userId) return false;
|
|
9713
|
+
const owned = ownedSlackUserKeys();
|
|
9714
|
+
return owned.has(`${msg.teamId.trim()}:${userId}`);
|
|
9715
|
+
}
|
|
9716
|
+
async function runSlackListen(opts = {}) {
|
|
7539
9717
|
const log = opts.onLog ?? console.log;
|
|
7540
|
-
const
|
|
7541
|
-
|
|
7542
|
-
)
|
|
7543
|
-
|
|
7544
|
-
|
|
7545
|
-
|
|
7546
|
-
const
|
|
7547
|
-
|
|
7548
|
-
|
|
7549
|
-
|
|
7550
|
-
|
|
9718
|
+
const connected = listSlackWorkspacesRaw();
|
|
9719
|
+
const orch = getOrchestrator();
|
|
9720
|
+
const off = orch.on((event) => {
|
|
9721
|
+
if (event.type !== "turn_finished") return;
|
|
9722
|
+
void relayCoordinatorReplyToSlack(event.threadId, opts);
|
|
9723
|
+
});
|
|
9724
|
+
const onInbound = (msg) => enqueueHandle(async () => {
|
|
9725
|
+
if (!isInboundForThisDesktop(msg)) {
|
|
9726
|
+
log(
|
|
9727
|
+
`skip ${msg.kind} ${msg.ts}: Slack user ${msg.userId ?? "?"} is not connected on this Mac`
|
|
9728
|
+
);
|
|
9729
|
+
return;
|
|
9730
|
+
}
|
|
9731
|
+
await handleSlackInbound(msg, opts);
|
|
9732
|
+
});
|
|
9733
|
+
try {
|
|
9734
|
+
const relay = (opts.relayUrl ?? slackRelayUrl()).trim();
|
|
9735
|
+
if (!relay) {
|
|
9736
|
+
throw new Error("Slack Listen needs the hosted relay URL.");
|
|
9737
|
+
}
|
|
9738
|
+
const withIdentity = connected.filter(
|
|
9739
|
+
(w) => w.bot_token?.trim() && w.user_token?.trim() && w.user_id?.trim()
|
|
9740
|
+
);
|
|
9741
|
+
if (withIdentity.length === 0) {
|
|
7551
9742
|
throw new Error(
|
|
7552
|
-
"
|
|
9743
|
+
"Slack Listen needs Add via browser (bot token + your Slack user id). Paste-only bot tokens cannot prove which Slack user owns this Mac."
|
|
7553
9744
|
);
|
|
7554
9745
|
}
|
|
7555
|
-
|
|
7556
|
-
|
|
7557
|
-
|
|
7558
|
-
|
|
9746
|
+
const device = ensureSlackDeviceIdentity();
|
|
9747
|
+
log(
|
|
9748
|
+
`Relay listen \xB7 ${device.deviceLabel} \xB7 ${withIdentity.length} workspace${withIdentity.length === 1 ? "" : "s"} as Slack user: ${withIdentity.map((w) => `${w.team_name}/${w.user_id}`).join(", ")}`
|
|
9749
|
+
);
|
|
9750
|
+
await runSlackRelayClient({
|
|
9751
|
+
url: relay,
|
|
9752
|
+
signal: opts.signal,
|
|
9753
|
+
onLog: log,
|
|
9754
|
+
WebSocketImpl: opts.WebSocketImpl,
|
|
9755
|
+
deviceId: device.deviceId,
|
|
9756
|
+
deviceLabel: device.deviceLabel,
|
|
9757
|
+
workspaces: withIdentity.map((w) => ({
|
|
9758
|
+
teamId: w.team_id,
|
|
9759
|
+
userId: w.user_id.trim(),
|
|
9760
|
+
botToken: w.bot_token.trim(),
|
|
9761
|
+
userToken: w.user_token.trim()
|
|
9762
|
+
})),
|
|
9763
|
+
onEvent: onInbound
|
|
9764
|
+
});
|
|
9765
|
+
} finally {
|
|
9766
|
+
off();
|
|
7559
9767
|
}
|
|
7560
|
-
|
|
7561
|
-
|
|
7562
|
-
);
|
|
7563
|
-
|
|
7564
|
-
|
|
7565
|
-
);
|
|
7566
|
-
|
|
7567
|
-
|
|
7568
|
-
|
|
9768
|
+
}
|
|
9769
|
+
function resolveSlackListenMode(opts) {
|
|
9770
|
+
const relay = (opts?.relayUrl ?? slackRelayUrl()).trim();
|
|
9771
|
+
const count = opts?.workspaceCount ?? listSlackWorkspacesRaw().filter(
|
|
9772
|
+
(w) => w.bot_token?.trim() && w.user_token?.trim() && w.user_id?.trim()
|
|
9773
|
+
).length;
|
|
9774
|
+
if (relay && count > 0) return "relay";
|
|
9775
|
+
return null;
|
|
9776
|
+
}
|
|
9777
|
+
|
|
9778
|
+
// src/slack/relay-hub.ts
|
|
9779
|
+
function parseSlackDeviceDestination(text2) {
|
|
9780
|
+
const m = text2.match(/^\s*(?:to\s+)?(?:@|#)?([A-Za-z][\w-]{0,63})\s*[::]\s*/);
|
|
9781
|
+
if (!m) return { label: null, rest: text2 };
|
|
9782
|
+
return { label: m[1], rest: text2.slice(m[0].length) };
|
|
9783
|
+
}
|
|
9784
|
+
var SlackRelayHub = class {
|
|
9785
|
+
sessions = /* @__PURE__ */ new Map();
|
|
9786
|
+
bySocket = /* @__PURE__ */ new Map();
|
|
9787
|
+
pendingClaims = /* @__PURE__ */ new Map();
|
|
9788
|
+
seq = 0;
|
|
9789
|
+
fetchImpl;
|
|
9790
|
+
authTest;
|
|
9791
|
+
log;
|
|
9792
|
+
constructor(opts = {}) {
|
|
9793
|
+
this.fetchImpl = opts.fetchImpl;
|
|
9794
|
+
this.log = opts.onLog ?? (() => void 0);
|
|
9795
|
+
this.authTest = opts.authTest ?? (async (token) => slackApi(token, "auth.test", void 0, this.fetchImpl));
|
|
9796
|
+
}
|
|
9797
|
+
sessionFor(teamId, userId, deviceId) {
|
|
9798
|
+
return this.sessions.get(slackRelaySessionKey(teamId, userId, deviceId)) ?? null;
|
|
9799
|
+
}
|
|
9800
|
+
sessionsForUser(teamId, userId) {
|
|
9801
|
+
const t = teamId.trim();
|
|
9802
|
+
const u = userId.trim();
|
|
9803
|
+
return [...this.sessions.values()].filter((s) => s.teamId === t && s.userId === u);
|
|
9804
|
+
}
|
|
9805
|
+
listSessions() {
|
|
9806
|
+
return [...this.sessions.values()];
|
|
9807
|
+
}
|
|
9808
|
+
detachSocket(socket) {
|
|
9809
|
+
const key = this.bySocket.get(socket);
|
|
9810
|
+
this.bySocket.delete(socket);
|
|
9811
|
+
if (!key) return;
|
|
9812
|
+
const current = this.sessions.get(key);
|
|
9813
|
+
if (current?.socket === socket) {
|
|
9814
|
+
this.sessions.delete(key);
|
|
9815
|
+
this.log(`desktop disconnected ${key}`);
|
|
9816
|
+
}
|
|
9817
|
+
}
|
|
9818
|
+
async handleClientMessage(socket, msg) {
|
|
9819
|
+
if (msg.type === "ping") {
|
|
9820
|
+
this.send(socket, { type: "pong" });
|
|
9821
|
+
return;
|
|
9822
|
+
}
|
|
9823
|
+
if (msg.type === "claim") {
|
|
9824
|
+
this.handleClaim(socket, msg.eventId);
|
|
9825
|
+
return;
|
|
9826
|
+
}
|
|
9827
|
+
if (msg.type === "register") {
|
|
9828
|
+
await this.register(
|
|
9829
|
+
socket,
|
|
9830
|
+
msg.teamId,
|
|
9831
|
+
msg.userId,
|
|
9832
|
+
msg.deviceId,
|
|
9833
|
+
msg.deviceLabel,
|
|
9834
|
+
msg.botToken,
|
|
9835
|
+
msg.userToken
|
|
9836
|
+
);
|
|
9837
|
+
}
|
|
7569
9838
|
}
|
|
7570
|
-
|
|
7571
|
-
|
|
7572
|
-
|
|
9839
|
+
async register(socket, teamId, userId, deviceId, deviceLabel, botToken, userToken) {
|
|
9840
|
+
const claimedTeam = teamId.trim();
|
|
9841
|
+
const claimedUser = userId.trim();
|
|
9842
|
+
const claimedDevice = deviceId.trim();
|
|
9843
|
+
const label = (deviceLabel?.trim() || claimedDevice).slice(0, 64);
|
|
9844
|
+
const bot = botToken.trim();
|
|
9845
|
+
const user = userToken.trim();
|
|
9846
|
+
if (!claimedTeam || !claimedUser || !claimedDevice || !bot || !user) {
|
|
9847
|
+
this.send(socket, {
|
|
9848
|
+
type: "error",
|
|
9849
|
+
message: "register needs teamId, userId, deviceId, botToken, and userToken"
|
|
9850
|
+
});
|
|
9851
|
+
return;
|
|
9852
|
+
}
|
|
7573
9853
|
try {
|
|
7574
|
-
const
|
|
7575
|
-
const
|
|
7576
|
-
|
|
7577
|
-
|
|
7578
|
-
|
|
7579
|
-
|
|
7580
|
-
|
|
7581
|
-
|
|
7582
|
-
|
|
7583
|
-
|
|
7584
|
-
|
|
7585
|
-
|
|
7586
|
-
|
|
7587
|
-
|
|
7588
|
-
|
|
7589
|
-
|
|
7590
|
-
|
|
7591
|
-
|
|
7592
|
-
|
|
7593
|
-
|
|
7594
|
-
|
|
7595
|
-
|
|
7596
|
-
|
|
7597
|
-
|
|
7598
|
-
|
|
7599
|
-
|
|
7600
|
-
|
|
7601
|
-
|
|
7602
|
-
|
|
9854
|
+
const userAuth = await this.authTest(user);
|
|
9855
|
+
const userTeam = userAuth.team_id?.trim();
|
|
9856
|
+
const userAuthId = userAuth.user_id?.trim();
|
|
9857
|
+
if (!userTeam || !userAuthId) {
|
|
9858
|
+
this.send(socket, {
|
|
9859
|
+
type: "error",
|
|
9860
|
+
message: "user token auth.test did not return team_id/user_id"
|
|
9861
|
+
});
|
|
9862
|
+
return;
|
|
9863
|
+
}
|
|
9864
|
+
if (userTeam !== claimedTeam || userAuthId !== claimedUser) {
|
|
9865
|
+
this.send(socket, {
|
|
9866
|
+
type: "error",
|
|
9867
|
+
message: `User token identity ${userTeam}/${userAuthId} does not match claimed ${claimedTeam}/${claimedUser}`
|
|
9868
|
+
});
|
|
9869
|
+
this.log(
|
|
9870
|
+
`register rejected: claimed ${claimedTeam}/${claimedUser} user-token ${userTeam}/${userAuthId}`
|
|
9871
|
+
);
|
|
9872
|
+
return;
|
|
9873
|
+
}
|
|
9874
|
+
const botAuth = await this.authTest(bot);
|
|
9875
|
+
const botTeam = botAuth.team_id?.trim();
|
|
9876
|
+
if (!botTeam) {
|
|
9877
|
+
this.send(socket, {
|
|
9878
|
+
type: "error",
|
|
9879
|
+
message: "bot token auth.test did not return team_id"
|
|
9880
|
+
});
|
|
9881
|
+
return;
|
|
9882
|
+
}
|
|
9883
|
+
if (botTeam !== claimedTeam) {
|
|
9884
|
+
this.send(socket, {
|
|
9885
|
+
type: "error",
|
|
9886
|
+
message: `Bot token team_id ${botTeam} does not match claimed ${claimedTeam}`
|
|
9887
|
+
});
|
|
9888
|
+
this.log(`register rejected: claimed team ${claimedTeam} bot ${botTeam}`);
|
|
9889
|
+
return;
|
|
7603
9890
|
}
|
|
7604
9891
|
} catch (err) {
|
|
7605
|
-
|
|
7606
|
-
|
|
7607
|
-
|
|
7608
|
-
|
|
7609
|
-
|
|
7610
|
-
|
|
9892
|
+
const errMsg = err instanceof Error ? err.message : String(err);
|
|
9893
|
+
this.send(socket, { type: "error", message: `auth.test failed: ${errMsg}` });
|
|
9894
|
+
return;
|
|
9895
|
+
}
|
|
9896
|
+
const key = slackRelaySessionKey(claimedTeam, claimedUser, claimedDevice);
|
|
9897
|
+
const prev = this.sessions.get(key);
|
|
9898
|
+
if (prev && prev.socket !== socket) {
|
|
9899
|
+
try {
|
|
9900
|
+
prev.socket.close(4e3, "replaced");
|
|
9901
|
+
} catch {
|
|
9902
|
+
}
|
|
9903
|
+
this.bySocket.delete(prev.socket);
|
|
9904
|
+
}
|
|
9905
|
+
const session = {
|
|
9906
|
+
id: `s${++this.seq}`,
|
|
9907
|
+
teamId: claimedTeam,
|
|
9908
|
+
userId: claimedUser,
|
|
9909
|
+
deviceId: claimedDevice,
|
|
9910
|
+
deviceLabel: label,
|
|
9911
|
+
socket,
|
|
9912
|
+
connectedAt: Date.now()
|
|
9913
|
+
};
|
|
9914
|
+
this.sessions.set(key, session);
|
|
9915
|
+
this.bySocket.set(socket, key);
|
|
9916
|
+
this.send(socket, {
|
|
9917
|
+
type: "registered",
|
|
9918
|
+
teamId: claimedTeam,
|
|
9919
|
+
userId: claimedUser,
|
|
9920
|
+
deviceId: claimedDevice
|
|
9921
|
+
});
|
|
9922
|
+
this.log(`desktop registered ${key} (${label})`);
|
|
9923
|
+
}
|
|
9924
|
+
/**
|
|
9925
|
+
* Fan-out an inbound event to matching device(s) for that Slack user.
|
|
9926
|
+
* Optional leading `Personal:` / `Work:` selects by device label.
|
|
9927
|
+
* Desktops must claim; first claim wins.
|
|
9928
|
+
*/
|
|
9929
|
+
routeEvent(message) {
|
|
9930
|
+
const userId = message.userId?.trim();
|
|
9931
|
+
if (!userId) {
|
|
9932
|
+
this.log(`skip event ${message.ts}: missing user id`);
|
|
9933
|
+
return false;
|
|
9934
|
+
}
|
|
9935
|
+
const online = this.sessionsForUser(message.teamId, userId);
|
|
9936
|
+
if (online.length === 0) {
|
|
9937
|
+
this.log(`no desktop for ${slackRelayUserKey(message.teamId, userId)} \u2014 skip`);
|
|
9938
|
+
return false;
|
|
9939
|
+
}
|
|
9940
|
+
const { label, rest } = parseSlackDeviceDestination(message.text);
|
|
9941
|
+
let targets = online;
|
|
9942
|
+
let outbound = message;
|
|
9943
|
+
if (label) {
|
|
9944
|
+
const needle = label.toLowerCase();
|
|
9945
|
+
targets = online.filter((s) => s.deviceLabel.toLowerCase() === needle);
|
|
9946
|
+
if (targets.length === 0) {
|
|
9947
|
+
this.log(
|
|
9948
|
+
`no device labeled "${label}" for ${slackRelayUserKey(message.teamId, userId)} \u2014 skip (online: ${online.map((s) => s.deviceLabel).join(", ")})`
|
|
9949
|
+
);
|
|
9950
|
+
return false;
|
|
7611
9951
|
}
|
|
9952
|
+
outbound = { ...message, text: rest };
|
|
7612
9953
|
}
|
|
7613
|
-
|
|
7614
|
-
|
|
7615
|
-
|
|
7616
|
-
|
|
7617
|
-
() =>
|
|
7618
|
-
|
|
7619
|
-
resolve();
|
|
7620
|
-
},
|
|
7621
|
-
{ once: true }
|
|
7622
|
-
);
|
|
9954
|
+
const eventId = newSlackRelayEventId();
|
|
9955
|
+
this.pendingClaims.set(eventId, {
|
|
9956
|
+
winnerKey: null,
|
|
9957
|
+
offered: new Set(
|
|
9958
|
+
targets.map((s) => slackRelaySessionKey(s.teamId, s.userId, s.deviceId))
|
|
9959
|
+
)
|
|
7623
9960
|
});
|
|
9961
|
+
setTimeout(() => this.pendingClaims.delete(eventId), 6e4).unref?.();
|
|
9962
|
+
for (const session of targets) {
|
|
9963
|
+
this.send(session.socket, { type: "event", eventId, message: outbound });
|
|
9964
|
+
}
|
|
9965
|
+
this.log(
|
|
9966
|
+
`fan-out ${eventId} \u2192 ${targets.map((s) => s.deviceLabel || s.deviceId).join(", ")}${label ? ` (dest ${label})` : ""}`
|
|
9967
|
+
);
|
|
9968
|
+
return true;
|
|
9969
|
+
}
|
|
9970
|
+
handleClaim(socket, eventId) {
|
|
9971
|
+
const key = this.bySocket.get(socket);
|
|
9972
|
+
if (!key) {
|
|
9973
|
+
this.send(socket, { type: "error", message: "claim before register" });
|
|
9974
|
+
return;
|
|
9975
|
+
}
|
|
9976
|
+
const pending = this.pendingClaims.get(eventId);
|
|
9977
|
+
if (!pending || !pending.offered.has(key)) {
|
|
9978
|
+
this.send(socket, { type: "claim_denied", eventId });
|
|
9979
|
+
return;
|
|
9980
|
+
}
|
|
9981
|
+
if (!pending.winnerKey) {
|
|
9982
|
+
pending.winnerKey = key;
|
|
9983
|
+
this.send(socket, { type: "claim_ok", eventId });
|
|
9984
|
+
this.log(`claim_ok ${eventId} \u2192 ${key}`);
|
|
9985
|
+
return;
|
|
9986
|
+
}
|
|
9987
|
+
this.send(socket, { type: "claim_denied", eventId });
|
|
9988
|
+
}
|
|
9989
|
+
send(socket, msg) {
|
|
9990
|
+
try {
|
|
9991
|
+
socket.send(JSON.stringify(msg));
|
|
9992
|
+
} catch (err) {
|
|
9993
|
+
const errMsg = err instanceof Error ? err.message : String(err);
|
|
9994
|
+
this.log(`send error: ${errMsg}`);
|
|
9995
|
+
}
|
|
9996
|
+
}
|
|
9997
|
+
};
|
|
9998
|
+
|
|
9999
|
+
// src/slack/relay-server.ts
|
|
10000
|
+
import { createServer as createServer4 } from "http";
|
|
10001
|
+
import { WebSocketServer } from "ws";
|
|
10002
|
+
async function startSlackRelayServer(opts) {
|
|
10003
|
+
const log = opts.onLog ?? console.log;
|
|
10004
|
+
const appToken = opts.appToken.trim();
|
|
10005
|
+
if (!appToken.startsWith("xapp-")) {
|
|
10006
|
+
throw new Error("SIDEBOARD_SLACK_APP_TOKEN must be an xapp-\u2026 app-level token");
|
|
7624
10007
|
}
|
|
10008
|
+
const hub = opts.hub ?? new SlackRelayHub({
|
|
10009
|
+
fetchImpl: opts.fetchImpl,
|
|
10010
|
+
onLog: log
|
|
10011
|
+
});
|
|
10012
|
+
const httpServer = createServer4((req, res) => {
|
|
10013
|
+
if (req.url === "/health" || req.url === "/") {
|
|
10014
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
10015
|
+
res.end(
|
|
10016
|
+
JSON.stringify({
|
|
10017
|
+
ok: true,
|
|
10018
|
+
service: "sideboard-slack-relay",
|
|
10019
|
+
sessions: hub.listSessions().length
|
|
10020
|
+
})
|
|
10021
|
+
);
|
|
10022
|
+
return;
|
|
10023
|
+
}
|
|
10024
|
+
res.writeHead(404);
|
|
10025
|
+
res.end("Not found");
|
|
10026
|
+
});
|
|
10027
|
+
const wss = new WebSocketServer({ server: httpServer, path: "/desktop" });
|
|
10028
|
+
wss.on("connection", (ws, _req) => {
|
|
10029
|
+
const socket = {
|
|
10030
|
+
send: (data) => {
|
|
10031
|
+
if (ws.readyState === ws.OPEN) ws.send(data);
|
|
10032
|
+
},
|
|
10033
|
+
close: (code, reason) => {
|
|
10034
|
+
try {
|
|
10035
|
+
ws.close(code, reason);
|
|
10036
|
+
} catch {
|
|
10037
|
+
}
|
|
10038
|
+
}
|
|
10039
|
+
};
|
|
10040
|
+
log("desktop connected");
|
|
10041
|
+
ws.on("message", (data) => {
|
|
10042
|
+
const raw = typeof data === "string" ? data : data.toString("utf8");
|
|
10043
|
+
const msg = parseSlackRelayClientMessage(raw);
|
|
10044
|
+
if (!msg) {
|
|
10045
|
+
socket.send(JSON.stringify({ type: "error", message: "invalid message" }));
|
|
10046
|
+
return;
|
|
10047
|
+
}
|
|
10048
|
+
void hub.handleClientMessage(socket, msg);
|
|
10049
|
+
});
|
|
10050
|
+
ws.on("close", () => {
|
|
10051
|
+
hub.detachSocket(socket);
|
|
10052
|
+
});
|
|
10053
|
+
ws.on("error", () => {
|
|
10054
|
+
hub.detachSocket(socket);
|
|
10055
|
+
});
|
|
10056
|
+
});
|
|
10057
|
+
const port = opts.port ?? 0;
|
|
10058
|
+
const host = opts.host ?? "0.0.0.0";
|
|
10059
|
+
await new Promise((resolve, reject) => {
|
|
10060
|
+
httpServer.once("error", reject);
|
|
10061
|
+
httpServer.listen(port, host, () => resolve());
|
|
10062
|
+
});
|
|
10063
|
+
const address = httpServer.address();
|
|
10064
|
+
const boundPort = typeof address === "object" && address ? address.port : typeof port === "number" ? port : 0;
|
|
10065
|
+
const url = `ws://${host === "0.0.0.0" ? "127.0.0.1" : host}:${boundPort}/desktop`;
|
|
10066
|
+
log(`relay listening on ${url}`);
|
|
10067
|
+
const ac = new AbortController();
|
|
10068
|
+
const onAbort = () => ac.abort();
|
|
10069
|
+
opts.signal?.addEventListener("abort", onAbort, { once: true });
|
|
10070
|
+
const socketModeDone = runSlackSocketMode({
|
|
10071
|
+
appToken,
|
|
10072
|
+
signal: ac.signal,
|
|
10073
|
+
fetchImpl: opts.fetchImpl,
|
|
10074
|
+
onLog: (line) => log(line),
|
|
10075
|
+
onEvent: (msg) => {
|
|
10076
|
+
hub.routeEvent(msg);
|
|
10077
|
+
}
|
|
10078
|
+
}).catch((err) => {
|
|
10079
|
+
if (ac.signal.aborted) return;
|
|
10080
|
+
const errMsg = err instanceof Error ? err.message : String(err);
|
|
10081
|
+
log(`socket mode ended: ${errMsg}`);
|
|
10082
|
+
});
|
|
10083
|
+
return {
|
|
10084
|
+
port: boundPort,
|
|
10085
|
+
url,
|
|
10086
|
+
hub,
|
|
10087
|
+
close: async () => {
|
|
10088
|
+
opts.signal?.removeEventListener("abort", onAbort);
|
|
10089
|
+
ac.abort();
|
|
10090
|
+
await new Promise((resolve) => {
|
|
10091
|
+
wss.close(() => resolve());
|
|
10092
|
+
});
|
|
10093
|
+
await new Promise((resolve, reject) => {
|
|
10094
|
+
httpServer.close((err) => err ? reject(err) : resolve());
|
|
10095
|
+
});
|
|
10096
|
+
await socketModeDone.catch(() => void 0);
|
|
10097
|
+
}
|
|
10098
|
+
};
|
|
7625
10099
|
}
|
|
7626
10100
|
export {
|
|
7627
10101
|
ATTACHMENTS_DIR,
|
|
10102
|
+
BAKED_SLACK_RELAY_URL,
|
|
7628
10103
|
BRIGHTSY_MCP_ALLOWED_TOOLS,
|
|
7629
10104
|
BrightsySideboardApi,
|
|
7630
10105
|
CLAUDE_MODEL_CATALOG,
|
|
@@ -7643,6 +10118,9 @@ export {
|
|
|
7643
10118
|
LEGACY_ATTACHMENTS_DIR,
|
|
7644
10119
|
LEGACY_PLAN_FILE_REL,
|
|
7645
10120
|
LEGACY_REVIEW_REQUEST_PATH,
|
|
10121
|
+
LINEAR_OAUTH_PORT,
|
|
10122
|
+
LINEAR_OAUTH_REDIRECT,
|
|
10123
|
+
LINEAR_OAUTH_SCOPES,
|
|
7646
10124
|
MAX_ANTHROPIC_CACHE_CONTROL_BLOCKS,
|
|
7647
10125
|
ORCHESTRATOR_AGENT_KINDS,
|
|
7648
10126
|
Orchestrator,
|
|
@@ -7659,7 +10137,16 @@ export {
|
|
|
7659
10137
|
SIDEBOARD_FORCE_STOP,
|
|
7660
10138
|
SIDEBOARD_MCP_ALLOWED_TOOLS,
|
|
7661
10139
|
SIDEBOARD_MCP_PROFILE_ENV,
|
|
10140
|
+
SLACK_LISTEN_BUSY_REPLY,
|
|
10141
|
+
SLACK_LISTEN_STOPPED_REPLY,
|
|
10142
|
+
SLACK_LISTEN_TIMEOUT_REPLY,
|
|
10143
|
+
SLACK_OAUTH_PORT,
|
|
10144
|
+
SLACK_OAUTH_REDIRECT,
|
|
10145
|
+
SLACK_REPLY_FORMATTING,
|
|
10146
|
+
SLACK_SEEN_REACTION,
|
|
10147
|
+
SlackRelayHub,
|
|
7662
10148
|
THINKING_EFFORTS,
|
|
10149
|
+
ackSlackInboundSeen,
|
|
7663
10150
|
addPrStackLayer,
|
|
7664
10151
|
addStackLayerFromThread,
|
|
7665
10152
|
addWorkspace,
|
|
@@ -7701,8 +10188,10 @@ export {
|
|
|
7701
10188
|
buildReviewRequestAttachment,
|
|
7702
10189
|
buildSessionSeed,
|
|
7703
10190
|
buildWorkspaceScriptEnv,
|
|
10191
|
+
caffeinateHoldPath,
|
|
7704
10192
|
caffeinateWhileCloudConnectEnabled,
|
|
7705
10193
|
caffeinateWhileRunningEnabled,
|
|
10194
|
+
caffeinateWhileSlackListenEnabled,
|
|
7706
10195
|
captureLoginEnv,
|
|
7707
10196
|
captureTurnBaseline,
|
|
7708
10197
|
checkoutPrStackLayer,
|
|
@@ -7720,6 +10209,7 @@ export {
|
|
|
7720
10209
|
conductorDbPath,
|
|
7721
10210
|
confirmLand,
|
|
7722
10211
|
connectBrightsyTeam,
|
|
10212
|
+
connectSlackToken,
|
|
7723
10213
|
coordinatorSystemPrompt,
|
|
7724
10214
|
coordinatorTurnReminder,
|
|
7725
10215
|
copyConfiguredFiles,
|
|
@@ -7728,6 +10218,7 @@ export {
|
|
|
7728
10218
|
createEmptyThread,
|
|
7729
10219
|
createExistingBranchWorktree,
|
|
7730
10220
|
createGlobalChat,
|
|
10221
|
+
createLinearPkce,
|
|
7731
10222
|
createOrUpdatePr,
|
|
7732
10223
|
createPrStack,
|
|
7733
10224
|
createThread,
|
|
@@ -7742,7 +10233,11 @@ export {
|
|
|
7742
10233
|
detectGhStack,
|
|
7743
10234
|
detectLocalMergeConflicts,
|
|
7744
10235
|
disconnectBrightsyTeam,
|
|
10236
|
+
disconnectLinear,
|
|
10237
|
+
disconnectLinearConnection,
|
|
10238
|
+
disconnectSlackWorkspace,
|
|
7745
10239
|
discoverSkills,
|
|
10240
|
+
dismissSlackReplyBadge,
|
|
7746
10241
|
dropCachedPrefixOnResume,
|
|
7747
10242
|
encodeBrightsyTarget,
|
|
7748
10243
|
enrichPathWithNpmGlobalBin,
|
|
@@ -7752,6 +10247,8 @@ export {
|
|
|
7752
10247
|
ensureGhPreferOrigin,
|
|
7753
10248
|
ensureGlobalCoordinatorCwd,
|
|
7754
10249
|
ensureReviewRequestFile,
|
|
10250
|
+
ensureSlackCoordinator,
|
|
10251
|
+
ensureSlackDeviceIdentity,
|
|
7755
10252
|
ensureWorkspace,
|
|
7756
10253
|
estimateMessageChars,
|
|
7757
10254
|
estimateThreadChars,
|
|
@@ -7780,6 +10277,8 @@ export {
|
|
|
7780
10277
|
formatPlanQuestionsForChat,
|
|
7781
10278
|
formatRateLimitResetHint,
|
|
7782
10279
|
formatRenameBranchDirective,
|
|
10280
|
+
formatSlackInboundPrompt,
|
|
10281
|
+
formatSlackSignedReply,
|
|
7783
10282
|
formatTranscriptMarkdown,
|
|
7784
10283
|
formatWorkspaceInventory,
|
|
7785
10284
|
formatWorktreeDirective,
|
|
@@ -7787,6 +10286,7 @@ export {
|
|
|
7787
10286
|
getAdapter,
|
|
7788
10287
|
getAgentSetupInfo,
|
|
7789
10288
|
getBrightsySession,
|
|
10289
|
+
getCaffeinateHold,
|
|
7790
10290
|
getDefaultAgent,
|
|
7791
10291
|
getDefaultEffort,
|
|
7792
10292
|
getDefaultFast,
|
|
@@ -7797,6 +10297,7 @@ export {
|
|
|
7797
10297
|
getGitHubStatus,
|
|
7798
10298
|
getIssueSource,
|
|
7799
10299
|
getLinearApiKey,
|
|
10300
|
+
getLinearAuthToken,
|
|
7800
10301
|
getOrchestrator,
|
|
7801
10302
|
getPr,
|
|
7802
10303
|
getPrChecks,
|
|
@@ -7806,12 +10307,16 @@ export {
|
|
|
7806
10307
|
getRepoSetupInfo,
|
|
7807
10308
|
getRunMode,
|
|
7808
10309
|
getRunScript,
|
|
10310
|
+
getSlackWorkspace,
|
|
7809
10311
|
gh,
|
|
7810
10312
|
ghHeadRef,
|
|
7811
10313
|
ghRepoSelectArgs,
|
|
7812
10314
|
git,
|
|
7813
10315
|
globalAgentCwd,
|
|
10316
|
+
handleSlackInbound,
|
|
7814
10317
|
harnessEnvKey,
|
|
10318
|
+
hasBakedLinearOAuth,
|
|
10319
|
+
hasBakedSlackOAuth,
|
|
7815
10320
|
hasConductorHook,
|
|
7816
10321
|
hasCursorWorktreeSetup,
|
|
7817
10322
|
hasRepoHook,
|
|
@@ -7836,6 +10341,7 @@ export {
|
|
|
7836
10341
|
isGlobalThread,
|
|
7837
10342
|
isImageFilePath,
|
|
7838
10343
|
isInPrStack,
|
|
10344
|
+
isInboundForThisDesktop,
|
|
7839
10345
|
isLinearConnected,
|
|
7840
10346
|
isOrchestratorCapableAgent,
|
|
7841
10347
|
isOrchestratorThread,
|
|
@@ -7844,8 +10350,12 @@ export {
|
|
|
7844
10350
|
isPresentPlanToolName,
|
|
7845
10351
|
isSessionQuotaLimit,
|
|
7846
10352
|
isSideboardScratchPath,
|
|
10353
|
+
isSlackCoordinatorThread,
|
|
7847
10354
|
isThinkingEffort,
|
|
7848
10355
|
isWorkspaceScratchPath,
|
|
10356
|
+
linearAuthorizationHeader,
|
|
10357
|
+
linearOAuthAuthorizeUrl,
|
|
10358
|
+
linearOAuthCredentials,
|
|
7849
10359
|
listAgentSetupInfo,
|
|
7850
10360
|
listBranchCommits,
|
|
7851
10361
|
listBranches,
|
|
@@ -7864,6 +10374,8 @@ export {
|
|
|
7864
10374
|
listOpencodeModels,
|
|
7865
10375
|
listPrs,
|
|
7866
10376
|
listRunScripts,
|
|
10377
|
+
listSlackReplyBadges,
|
|
10378
|
+
listSlackWorkspaces,
|
|
7867
10379
|
listThreads,
|
|
7868
10380
|
listWorkspaces,
|
|
7869
10381
|
listWorktreeFiles,
|
|
@@ -7907,9 +10419,13 @@ export {
|
|
|
7907
10419
|
parseMcpList,
|
|
7908
10420
|
parsePlanQuestionsInput,
|
|
7909
10421
|
parseSessionQuotaResetAt,
|
|
10422
|
+
parseSlackRelayClientMessage,
|
|
10423
|
+
parseSlackRelayServerMessage,
|
|
7910
10424
|
partsToAssistantText,
|
|
7911
10425
|
pastedTextStats,
|
|
10426
|
+
permalinkForSlackReplyBadge,
|
|
7912
10427
|
permissionMode,
|
|
10428
|
+
persistVaultKeyInKeychain,
|
|
7913
10429
|
planFileAbs,
|
|
7914
10430
|
posixShellSingleQuote,
|
|
7915
10431
|
prepareTerminalCommand,
|
|
@@ -7917,13 +10433,16 @@ export {
|
|
|
7917
10433
|
promptMentionsBrightsy,
|
|
7918
10434
|
pushBranch,
|
|
7919
10435
|
readExistingReviewRequestFile,
|
|
10436
|
+
readKeychainVaultKey,
|
|
7920
10437
|
readPlanFile,
|
|
7921
10438
|
readSkillBody,
|
|
7922
10439
|
readThread,
|
|
7923
10440
|
readWorktreeFile,
|
|
7924
10441
|
readWorktreeFileForUpload,
|
|
7925
10442
|
readWorktreeInclude,
|
|
10443
|
+
recordSlackOutboundWatch,
|
|
7926
10444
|
refreshGitHubAuth,
|
|
10445
|
+
refreshSlackReplyBadges,
|
|
7927
10446
|
removeWorkspace,
|
|
7928
10447
|
removeWorktree,
|
|
7929
10448
|
repoSlug,
|
|
@@ -7948,18 +10467,26 @@ export {
|
|
|
7948
10467
|
resolveQuotaFallbackAgent,
|
|
7949
10468
|
resolveRepoRoot,
|
|
7950
10469
|
resolveReviewGuidelines,
|
|
10470
|
+
resolveSlackListenMode,
|
|
7951
10471
|
resolveThreadDefaults,
|
|
7952
10472
|
resolveThreadEffort,
|
|
10473
|
+
resolveVaultKey,
|
|
7953
10474
|
resolveWorktreeStartPoint,
|
|
7954
10475
|
run,
|
|
7955
10476
|
runArchiveScript,
|
|
7956
10477
|
runCloudConnect,
|
|
7957
10478
|
runCursorWorktreeSetup,
|
|
7958
10479
|
runSetupScript,
|
|
10480
|
+
runSlackListen,
|
|
10481
|
+
runSlackRelayClient,
|
|
7959
10482
|
sameWorktreePath,
|
|
7960
10483
|
sanitizeMcpServerName,
|
|
7961
10484
|
saveAppSettings,
|
|
10485
|
+
saveLinearOAuth,
|
|
10486
|
+
secureFileUnlocksWith,
|
|
10487
|
+
setCaffeinateHold,
|
|
7962
10488
|
setStatus,
|
|
10489
|
+
setVaultMasterKey,
|
|
7963
10490
|
settingsSourceLabel,
|
|
7964
10491
|
shouldAttachPastedText,
|
|
7965
10492
|
shouldCompactContext,
|
|
@@ -7970,6 +10497,12 @@ export {
|
|
|
7970
10497
|
sideboardMcpProfile,
|
|
7971
10498
|
sideboardReposDir,
|
|
7972
10499
|
sideboardWorkspacesDir,
|
|
10500
|
+
slackAppLevelToken,
|
|
10501
|
+
slackArchiveUrl,
|
|
10502
|
+
slackCoordinatorSourceRef,
|
|
10503
|
+
slackListenEnabled,
|
|
10504
|
+
slackOAuthCredentials,
|
|
10505
|
+
slackRelayUrl,
|
|
7973
10506
|
slugify,
|
|
7974
10507
|
spawnAgentTurn,
|
|
7975
10508
|
splitForCompaction,
|
|
@@ -7979,8 +10512,11 @@ export {
|
|
|
7979
10512
|
stageAbsolutePathsAsAttachments,
|
|
7980
10513
|
stageBuffersAsAttachments,
|
|
7981
10514
|
startDevServer,
|
|
10515
|
+
startLinearOAuth,
|
|
7982
10516
|
startMcpServer,
|
|
7983
10517
|
startOrchestration,
|
|
10518
|
+
startSlackOAuth,
|
|
10519
|
+
startSlackRelayServer,
|
|
7984
10520
|
stripBrightsyNdjsonNoise,
|
|
7985
10521
|
submitPrStack,
|
|
7986
10522
|
suggestSlug,
|
|
@@ -7998,6 +10534,7 @@ export {
|
|
|
7998
10534
|
threadRequestsBrightsyMcp,
|
|
7999
10535
|
threadsDir,
|
|
8000
10536
|
threadsSharingWorktree,
|
|
10537
|
+
toPublicAppSettings,
|
|
8001
10538
|
toolDescription,
|
|
8002
10539
|
toolDetail,
|
|
8003
10540
|
toolFilePath,
|