@sideboard-ai/core 0.1.67 → 0.1.71
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{agents-OTBZVTZO.js → agents-GB7XL2LI.js} +8 -7
- package/dist/{agents-TS3EI2YI.js → agents-TCAK3MXN.js} +8 -7
- package/dist/{app-settings-ONKMFSEJ.js → app-settings-3TLA2EJ7.js} +16 -1
- package/dist/{app-settings-GJVZGU3M.js → app-settings-P42Z3VOB.js} +16 -1
- package/dist/caffeinate-hold-BEJIYPJ7.js +107 -0
- package/dist/caffeinate-hold-KLC6SABD.js +14 -0
- package/dist/{chunk-WAFIF22N.js → chunk-3CY6WJ7O.js} +2 -2
- package/dist/{chunk-7ZOQY4A7.js → chunk-57ROTAFD.js} +62 -3
- package/dist/{chunk-57WWNMFP.js → chunk-7NCIHRAU.js} +22 -4
- package/dist/{chunk-FGDJOJ2L.js → chunk-AE5VBOFE.js} +1 -1
- package/dist/{chunk-DB3I75FI.js → chunk-BTL7EMGT.js} +4 -4
- package/dist/{chunk-4UIYN56C.js → chunk-D4DPQ552.js} +1 -1
- package/dist/{chunk-JWZNK3CQ.js → chunk-DGPUAZA4.js} +62 -3
- package/dist/{chunk-ZRFAZVGL.js → chunk-H2IXNFQ2.js} +22 -4
- package/dist/chunk-JAWEDVVA.js +106 -0
- package/dist/chunk-JT7R45JB.js +20 -0
- package/dist/{chunk-4BUCGNZS.js → chunk-MF2YMEGD.js} +83 -29
- package/dist/{chunk-K7LM4CLD.js → chunk-NG7S3OPX.js} +12 -6
- package/dist/chunk-NSTQ6QKD.js +23 -0
- package/dist/{chunk-LW5CMQGZ.js → chunk-OE7YBB5X.js} +83 -29
- package/dist/{chunk-BPOTVI5J.js → chunk-PTJJIQK7.js} +472 -18
- package/dist/{chunk-B4PRRKMA.js → chunk-S25IHL5H.js} +12 -6
- package/dist/{chunk-5HTTCYWA.js → chunk-UH57WVFP.js} +2 -2
- package/dist/{chunk-M5V4QIWF.js → chunk-VROPG6QF.js} +1 -1
- package/dist/{chunk-MHR4OGZ4.js → chunk-W7YOTDVO.js} +493 -18
- package/dist/{connected-teams-RUNMOCG5.js → connected-teams-35EIRJCP.js} +2 -2
- package/dist/{connected-teams-OMDHTL6A.js → connected-teams-ZRS53OAT.js} +2 -2
- package/dist/{coordinator-prompt-6HMTOX4G.js → coordinator-prompt-2J4PIMUF.js} +7 -4
- package/dist/{coordinator-prompt-3BSMKZL4.js → coordinator-prompt-QYYG2IBB.js} +7 -4
- package/dist/{global-workspace-YZ2NJVLL.js → global-workspace-KHIFQUUM.js} +12 -5
- package/dist/{global-workspace-QS7A23CB.js → global-workspace-SVRYNJZA.js} +12 -5
- package/dist/index.cjs +4805 -1262
- package/dist/index.d.cts +570 -22
- package/dist/index.d.ts +570 -22
- package/dist/index.js +3084 -340
- package/dist/mcp/run-stdio.cjs +2339 -617
- package/dist/mcp/run-stdio.js +1289 -290
- package/dist/{run-Y2MI3EVV.js → run-A4RHY7HH.js} +1 -1
- package/dist/{run-JLD2Y52J.js → run-V2WZUMJA.js} +1 -1
- package/dist/{workspaces-EBFW7I7S.js → workspaces-AHFTHVBT.js} +7 -6
- package/dist/{workspaces-AIUYUXB7.js → workspaces-SVQIEVIZ.js} +7 -6
- package/dist/{worktree-WMDJW2BL.js → worktree-KWXHMKRN.js} +2 -2
- package/dist/{worktree-7NBPIVFZ.js → worktree-VOCH3WS3.js} +2 -2
- package/package.json +3 -1
package/dist/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,
|
|
@@ -94,7 +106,7 @@ import {
|
|
|
94
106
|
loadBrightsyConfig,
|
|
95
107
|
saveBrightsyConfig,
|
|
96
108
|
switchBrightsyAccount
|
|
97
|
-
} from "./chunk-
|
|
109
|
+
} from "./chunk-BTL7EMGT.js";
|
|
98
110
|
import {
|
|
99
111
|
LEGACY_PLAN_FILE_REL,
|
|
100
112
|
PLAN_FILE_NAME,
|
|
@@ -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-ZRFAZVGL.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,
|
|
@@ -295,7 +322,7 @@ import {
|
|
|
295
322
|
resolveGhAuthToken,
|
|
296
323
|
run,
|
|
297
324
|
withExportedPath
|
|
298
|
-
} from "./chunk-
|
|
325
|
+
} from "./chunk-D4DPQ552.js";
|
|
299
326
|
|
|
300
327
|
// src/integrations/github.ts
|
|
301
328
|
async function getGitHubStatus() {
|
|
@@ -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));
|
|
@@ -1512,24 +1786,25 @@ function capPatch(patch, maxHunk) {
|
|
|
1512
1786
|
|
|
1513
1787
|
\u2026 truncated (${patch.length - maxHunk} more chars)`;
|
|
1514
1788
|
}
|
|
1789
|
+
var mergeBaseCache = /* @__PURE__ */ new Map();
|
|
1790
|
+
var MERGE_BASE_TTL_MS = 45e3;
|
|
1515
1791
|
async function resolveMergeBase(worktreePath, base, repoPath) {
|
|
1516
1792
|
const baseRef = await resolveDiffBaseRef(worktreePath, base, repoPath);
|
|
1793
|
+
const cacheKey = `${worktreePath}\0${baseRef}`;
|
|
1794
|
+
const hit = mergeBaseCache.get(cacheKey);
|
|
1795
|
+
if (hit && Date.now() - hit.at < MERGE_BASE_TTL_MS) return hit.value;
|
|
1517
1796
|
const { stdout } = await git(["merge-base", baseRef, "HEAD"], worktreePath, {
|
|
1518
1797
|
reject: false
|
|
1519
1798
|
});
|
|
1520
|
-
const mb = stdout.trim();
|
|
1521
|
-
|
|
1799
|
+
const mb = stdout.trim() || baseRef;
|
|
1800
|
+
mergeBaseCache.set(cacheKey, { at: Date.now(), value: mb });
|
|
1801
|
+
return mb;
|
|
1522
1802
|
}
|
|
1523
1803
|
async function countUnpushedCommits(worktreePath) {
|
|
1524
1804
|
const head = await git(["rev-parse", "--abbrev-ref", "HEAD"], worktreePath, {
|
|
1525
1805
|
reject: false
|
|
1526
1806
|
});
|
|
1527
1807
|
const branch = head.stdout.trim();
|
|
1528
|
-
if (branch && branch !== "HEAD") {
|
|
1529
|
-
await git(["fetch", "origin", branch, "--quiet"], worktreePath, {
|
|
1530
|
-
reject: false
|
|
1531
|
-
});
|
|
1532
|
-
}
|
|
1533
1808
|
const upstream = await git(
|
|
1534
1809
|
["rev-list", "--count", "@{upstream}..HEAD"],
|
|
1535
1810
|
worktreePath,
|
|
@@ -1562,11 +1837,37 @@ function splitCombinedDiff(stdout) {
|
|
|
1562
1837
|
}
|
|
1563
1838
|
return map;
|
|
1564
1839
|
}
|
|
1840
|
+
function syntheticAddPatch(path, content, maxHunk) {
|
|
1841
|
+
const lines = content.split("\n");
|
|
1842
|
+
const body = lines.map((l) => `+${l}`).join("\n");
|
|
1843
|
+
const header = `diff --git a/${path} b/${path}
|
|
1844
|
+
new file mode 100644
|
|
1845
|
+
--- /dev/null
|
|
1846
|
+
+++ b/${path}
|
|
1847
|
+
@@ -0,0 +1,${lines.length} @@
|
|
1848
|
+
`;
|
|
1849
|
+
return {
|
|
1850
|
+
path,
|
|
1851
|
+
status: "A",
|
|
1852
|
+
patch: capPatch(`${header}${body}`, maxHunk),
|
|
1853
|
+
additions: lines.length,
|
|
1854
|
+
deletions: 0
|
|
1855
|
+
};
|
|
1856
|
+
}
|
|
1565
1857
|
async function untrackedPatch(worktreePath, path, maxHunk) {
|
|
1858
|
+
const abs = join4(worktreePath, path);
|
|
1859
|
+
try {
|
|
1860
|
+
const st = statSync2(abs);
|
|
1861
|
+
if (st.isFile() && st.size > maxHunk) {
|
|
1862
|
+
const buf = readFileSync4(abs).subarray(0, maxHunk);
|
|
1863
|
+
return syntheticAddPatch(path, buf.toString("utf8"), maxHunk);
|
|
1864
|
+
}
|
|
1865
|
+
} catch {
|
|
1866
|
+
}
|
|
1566
1867
|
const { stdout: patch } = await git(
|
|
1567
|
-
["diff", "--no-index", "--", "/dev/null", path],
|
|
1868
|
+
["diff", "--no-ext-diff", "--no-color", "--no-index", "--", "/dev/null", path],
|
|
1568
1869
|
worktreePath,
|
|
1569
|
-
{ reject: false }
|
|
1870
|
+
{ reject: false, timeoutMs: 4e3 }
|
|
1570
1871
|
);
|
|
1571
1872
|
const normalized = patch.replace(/^diff --git a\/.+? b\/.+?$/m, `diff --git a/${path} b/${path}`).replace(/^--- .*$/m, "--- /dev/null").replace(/^\+\+\+ .*$/m, `+++ b/${path}`);
|
|
1572
1873
|
const addLines = normalized.split("\n").filter((l) => l.startsWith("+") && !l.startsWith("+++")).length;
|
|
@@ -1578,22 +1879,97 @@ async function untrackedPatch(worktreePath, path, maxHunk) {
|
|
|
1578
1879
|
deletions: 0
|
|
1579
1880
|
};
|
|
1580
1881
|
}
|
|
1581
|
-
async function listUntrackedPaths(worktreePath) {
|
|
1582
|
-
const
|
|
1583
|
-
|
|
1584
|
-
|
|
1585
|
-
{ reject: false }
|
|
1586
|
-
);
|
|
1882
|
+
async function listUntrackedPaths(worktreePath, path) {
|
|
1883
|
+
const args = ["ls-files", "-z", "--others", "--exclude-standard"];
|
|
1884
|
+
if (path) args.push("--", path);
|
|
1885
|
+
const { stdout } = await git(args, worktreePath, { reject: false });
|
|
1587
1886
|
return stdout.split("\0").filter(Boolean).filter((p) => !isSideboardScratchPath(p));
|
|
1588
1887
|
}
|
|
1589
|
-
async function listUntrackedFiles(worktreePath, maxHunk) {
|
|
1590
|
-
const
|
|
1591
|
-
if (
|
|
1592
|
-
return Promise.all(
|
|
1888
|
+
async function listUntrackedFiles(worktreePath, maxHunk, paths) {
|
|
1889
|
+
const list = paths ?? await listUntrackedPaths(worktreePath);
|
|
1890
|
+
if (list.length === 0) return [];
|
|
1891
|
+
return Promise.all(list.map((path) => untrackedPatch(worktreePath, path, maxHunk)));
|
|
1892
|
+
}
|
|
1893
|
+
function untrackedStubs(paths) {
|
|
1894
|
+
return paths.map((path) => ({
|
|
1895
|
+
path,
|
|
1896
|
+
status: "A",
|
|
1897
|
+
patch: "",
|
|
1898
|
+
additions: 0,
|
|
1899
|
+
deletions: 0
|
|
1900
|
+
}));
|
|
1901
|
+
}
|
|
1902
|
+
function pathspec(path) {
|
|
1903
|
+
return path ? ["--", path] : [];
|
|
1904
|
+
}
|
|
1905
|
+
function gitDiff(args, cwd, opts) {
|
|
1906
|
+
return git(["diff", "--no-ext-diff", "--no-color", ...args], cwd, {
|
|
1907
|
+
reject: false,
|
|
1908
|
+
timeoutMs: opts?.timeoutMs
|
|
1909
|
+
});
|
|
1910
|
+
}
|
|
1911
|
+
function fileFromPatch(path, patch, maxHunk) {
|
|
1912
|
+
let status = "M";
|
|
1913
|
+
if (/^new file mode /m.test(patch) || /^--- \/dev\/null/m.test(patch)) {
|
|
1914
|
+
status = "A";
|
|
1915
|
+
} else if (/^deleted file mode /m.test(patch) || /^\+\+\+ \/dev\/null/m.test(patch)) {
|
|
1916
|
+
status = "D";
|
|
1917
|
+
} else if (/^rename from /m.test(patch)) {
|
|
1918
|
+
status = "R";
|
|
1919
|
+
}
|
|
1920
|
+
const additions = patch.split("\n").filter((l) => l.startsWith("+") && !l.startsWith("+++")).length;
|
|
1921
|
+
const deletions = patch.split("\n").filter((l) => l.startsWith("-") && !l.startsWith("---")).length;
|
|
1922
|
+
return {
|
|
1923
|
+
path,
|
|
1924
|
+
status,
|
|
1925
|
+
patch: capPatch(patch, maxHunk),
|
|
1926
|
+
additions,
|
|
1927
|
+
deletions
|
|
1928
|
+
};
|
|
1929
|
+
}
|
|
1930
|
+
async function gitDiffParts(worktreePath, diffArgs, includePatches, path) {
|
|
1931
|
+
const extra = pathspec(path);
|
|
1932
|
+
if (includePatches) {
|
|
1933
|
+
const diff = await gitDiff([...diffArgs, ...extra], worktreePath, {
|
|
1934
|
+
timeoutMs: 4e3
|
|
1935
|
+
});
|
|
1936
|
+
return { nameStatus: "", numstat: "", combinedDiff: diff.stdout };
|
|
1937
|
+
}
|
|
1938
|
+
const [ns, num] = await Promise.all([
|
|
1939
|
+
gitDiff(["--name-status", ...diffArgs, ...extra], worktreePath),
|
|
1940
|
+
gitDiff(["--numstat", ...diffArgs, ...extra], worktreePath)
|
|
1941
|
+
]);
|
|
1942
|
+
return {
|
|
1943
|
+
nameStatus: ns.stdout,
|
|
1944
|
+
numstat: num.stdout,
|
|
1945
|
+
combinedDiff: ""
|
|
1946
|
+
};
|
|
1947
|
+
}
|
|
1948
|
+
function formatStat(files) {
|
|
1949
|
+
if (files.length === 0) return "(no changes)";
|
|
1950
|
+
const additions = files.reduce((n2, f) => n2 + (f.additions ?? 0), 0);
|
|
1951
|
+
const deletions = files.reduce((n2, f) => n2 + (f.deletions ?? 0), 0);
|
|
1952
|
+
const n = files.length;
|
|
1953
|
+
return `${n} file${n === 1 ? "" : "s"} changed, ${additions} insertions(+), ${deletions} deletions(-)`;
|
|
1954
|
+
}
|
|
1955
|
+
function emptyScopeStats() {
|
|
1956
|
+
const z3 = emptyStat();
|
|
1957
|
+
return {
|
|
1958
|
+
commits: z3,
|
|
1959
|
+
uncommitted: z3,
|
|
1960
|
+
staged: z3,
|
|
1961
|
+
unstaged: z3,
|
|
1962
|
+
last_turn: z3
|
|
1963
|
+
};
|
|
1593
1964
|
}
|
|
1594
1965
|
function filesFromDiff(nameStatus, numstat, combinedDiff, maxHunk) {
|
|
1595
|
-
const counts = parseNumstat(numstat);
|
|
1596
1966
|
const patches = splitCombinedDiff(combinedDiff);
|
|
1967
|
+
if (!nameStatus.trim() && patches.size > 0) {
|
|
1968
|
+
return [...patches.entries()].map(
|
|
1969
|
+
([path, patch]) => fileFromPatch(path, patch, maxHunk)
|
|
1970
|
+
);
|
|
1971
|
+
}
|
|
1972
|
+
const counts = parseNumstat(numstat);
|
|
1597
1973
|
const files = [];
|
|
1598
1974
|
for (const { status, path } of parseNameStatus(nameStatus)) {
|
|
1599
1975
|
const n = counts.get(path);
|
|
@@ -1615,11 +1991,11 @@ async function computeScopeStats(worktreePath, mergeBase, lastTurnBase, untracke
|
|
|
1615
1991
|
{ stdout: unstagedNum },
|
|
1616
1992
|
lastTurnNum
|
|
1617
1993
|
] = await Promise.all([
|
|
1618
|
-
mergeBase ?
|
|
1619
|
-
|
|
1620
|
-
|
|
1621
|
-
|
|
1622
|
-
lastTurnBase ?
|
|
1994
|
+
mergeBase ? gitDiff(["--numstat", mergeBase], worktreePath) : Promise.resolve({ stdout: "" }),
|
|
1995
|
+
gitDiff(["--numstat", "HEAD"], worktreePath),
|
|
1996
|
+
gitDiff(["--numstat", "--cached"], worktreePath),
|
|
1997
|
+
gitDiff(["--numstat"], worktreePath),
|
|
1998
|
+
lastTurnBase ? gitDiff(["--numstat", lastTurnBase], worktreePath) : Promise.resolve({ stdout: "" })
|
|
1623
1999
|
]);
|
|
1624
2000
|
return {
|
|
1625
2001
|
commits: mergeBase ? statFromNumstat(commitsNum, untrackedCount) : emptyStat(),
|
|
@@ -1638,7 +2014,7 @@ async function listBranchCommits(worktreePath, repoPath, opts) {
|
|
|
1638
2014
|
if (head.exitCode !== 0) return [];
|
|
1639
2015
|
let base = opts?.base;
|
|
1640
2016
|
try {
|
|
1641
|
-
base = base ?? await resolveDefaultBranch(repoPath);
|
|
2017
|
+
base = base ?? await resolveDefaultBranch(repoPath, { network: false });
|
|
1642
2018
|
} catch {
|
|
1643
2019
|
base = "HEAD";
|
|
1644
2020
|
}
|
|
@@ -1668,7 +2044,121 @@ async function listBranchCommits(worktreePath, repoPath, opts) {
|
|
|
1668
2044
|
}
|
|
1669
2045
|
return commits;
|
|
1670
2046
|
}
|
|
2047
|
+
var SHA_RE = /^[0-9a-f]{7,40}$/i;
|
|
2048
|
+
function emptyPathDiff(scope, base, hasLastTurnBase, commitSha = null) {
|
|
2049
|
+
return {
|
|
2050
|
+
scope,
|
|
2051
|
+
commitSha: scope === "commits" ? commitSha : null,
|
|
2052
|
+
base,
|
|
2053
|
+
files: [],
|
|
2054
|
+
stat: formatStat([]),
|
|
2055
|
+
dirty: false,
|
|
2056
|
+
unpushed: 0,
|
|
2057
|
+
hasLastTurnBase,
|
|
2058
|
+
commits: [],
|
|
2059
|
+
scopeStats: emptyScopeStats()
|
|
2060
|
+
};
|
|
2061
|
+
}
|
|
2062
|
+
async function getPathDiff(worktreePath, repoPath, opts) {
|
|
2063
|
+
const { path, scope, commitSha, lastTurnBase, hasLastTurnBase, includePatches, maxHunk } = opts;
|
|
2064
|
+
if (scope === "last_turn" && !lastTurnBase) {
|
|
2065
|
+
return emptyPathDiff(scope, "last turn", false);
|
|
2066
|
+
}
|
|
2067
|
+
let diffArgs;
|
|
2068
|
+
let labelBase;
|
|
2069
|
+
if (scope === "staged") {
|
|
2070
|
+
diffArgs = ["--cached"];
|
|
2071
|
+
labelBase = "staged";
|
|
2072
|
+
} else if (scope === "unstaged") {
|
|
2073
|
+
diffArgs = [];
|
|
2074
|
+
labelBase = "unstaged";
|
|
2075
|
+
} else if (scope === "uncommitted") {
|
|
2076
|
+
diffArgs = ["HEAD"];
|
|
2077
|
+
labelBase = "HEAD";
|
|
2078
|
+
} else if (scope === "last_turn") {
|
|
2079
|
+
diffArgs = [lastTurnBase];
|
|
2080
|
+
labelBase = "last turn";
|
|
2081
|
+
} else if (scope === "commits" && commitSha) {
|
|
2082
|
+
diffArgs = [`${commitSha}^!`];
|
|
2083
|
+
labelBase = commitSha.slice(0, 7);
|
|
2084
|
+
} else if (scope === "commits" && opts.base && SHA_RE.test(opts.base)) {
|
|
2085
|
+
diffArgs = [opts.base];
|
|
2086
|
+
labelBase = opts.base.slice(0, 7);
|
|
2087
|
+
} else if (scope === "commits") {
|
|
2088
|
+
let base = opts.base;
|
|
2089
|
+
try {
|
|
2090
|
+
base = base ?? await resolveDefaultBranch(repoPath, { network: false });
|
|
2091
|
+
} catch {
|
|
2092
|
+
base = "HEAD";
|
|
2093
|
+
}
|
|
2094
|
+
const mergeBase = await resolveMergeBase(worktreePath, base, repoPath);
|
|
2095
|
+
diffArgs = [mergeBase];
|
|
2096
|
+
labelBase = mergeBase;
|
|
2097
|
+
} else {
|
|
2098
|
+
diffArgs = ["HEAD"];
|
|
2099
|
+
labelBase = "HEAD";
|
|
2100
|
+
}
|
|
2101
|
+
const diff = await gitDiff(
|
|
2102
|
+
includePatches ? [...diffArgs, "--", path] : ["--name-status", ...diffArgs, "--", path],
|
|
2103
|
+
worktreePath,
|
|
2104
|
+
{ timeoutMs: 4e3 }
|
|
2105
|
+
);
|
|
2106
|
+
const filesMap = /* @__PURE__ */ new Map();
|
|
2107
|
+
if (includePatches) {
|
|
2108
|
+
const parsed = filesFromDiff("", "", diff.stdout, maxHunk);
|
|
2109
|
+
for (const file of parsed) filesMap.set(file.path, file);
|
|
2110
|
+
if (diff.stdout.trim() && !filesMap.has(path)) {
|
|
2111
|
+
filesMap.set(path, fileFromPatch(path, diff.stdout, maxHunk));
|
|
2112
|
+
}
|
|
2113
|
+
} else {
|
|
2114
|
+
for (const file of filesFromDiff(diff.stdout, "", "", maxHunk)) {
|
|
2115
|
+
filesMap.set(file.path, file);
|
|
2116
|
+
}
|
|
2117
|
+
}
|
|
2118
|
+
if (filesMap.size === 0) {
|
|
2119
|
+
const maybe = await listUntrackedPaths(worktreePath, path);
|
|
2120
|
+
if (maybe.length > 0) {
|
|
2121
|
+
const extra = includePatches ? await listUntrackedFiles(worktreePath, maxHunk, maybe) : untrackedStubs(maybe);
|
|
2122
|
+
for (const file of extra) filesMap.set(file.path, file);
|
|
2123
|
+
}
|
|
2124
|
+
}
|
|
2125
|
+
const files = [...filesMap.values()].sort((a, b) => a.path.localeCompare(b.path));
|
|
2126
|
+
return {
|
|
2127
|
+
scope,
|
|
2128
|
+
commitSha: scope === "commits" ? commitSha : null,
|
|
2129
|
+
base: labelBase,
|
|
2130
|
+
files,
|
|
2131
|
+
stat: formatStat(files),
|
|
2132
|
+
dirty: files.length > 0,
|
|
2133
|
+
unpushed: 0,
|
|
2134
|
+
hasLastTurnBase,
|
|
2135
|
+
commits: [],
|
|
2136
|
+
scopeStats: emptyScopeStats(),
|
|
2137
|
+
mergeBase: SHA_RE.test(diffArgs[0] ?? "") ? diffArgs[0] : null
|
|
2138
|
+
};
|
|
2139
|
+
}
|
|
1671
2140
|
async function getDiff(worktreePath, repoPath, opts) {
|
|
2141
|
+
const maxHunk = opts?.maxHunkChars ?? 8e3;
|
|
2142
|
+
const scope = opts?.scope ?? "commits";
|
|
2143
|
+
const lastTurnBase = opts?.lastTurnBase?.trim() || null;
|
|
2144
|
+
const hasLastTurnBase = Boolean(lastTurnBase);
|
|
2145
|
+
const commitSha = opts?.commitSha?.trim() || null;
|
|
2146
|
+
const includePatches = opts?.includePatches ?? true;
|
|
2147
|
+
const pathFilter = opts?.path?.trim() || null;
|
|
2148
|
+
const fileOnly = Boolean(pathFilter);
|
|
2149
|
+
const includeMeta = (opts?.includeMeta ?? true) && !fileOnly;
|
|
2150
|
+
if (fileOnly && pathFilter && !includeMeta) {
|
|
2151
|
+
return getPathDiff(worktreePath, repoPath, {
|
|
2152
|
+
path: pathFilter,
|
|
2153
|
+
scope,
|
|
2154
|
+
commitSha,
|
|
2155
|
+
lastTurnBase,
|
|
2156
|
+
hasLastTurnBase,
|
|
2157
|
+
base: opts?.base,
|
|
2158
|
+
includePatches,
|
|
2159
|
+
maxHunk
|
|
2160
|
+
});
|
|
2161
|
+
}
|
|
1672
2162
|
const status = await inspectGitWorktree(worktreePath);
|
|
1673
2163
|
if (status === "missing_worktree") {
|
|
1674
2164
|
throw new Error("Worktree not found");
|
|
@@ -1676,18 +2166,19 @@ async function getDiff(worktreePath, repoPath, opts) {
|
|
|
1676
2166
|
if (status === "not_git") {
|
|
1677
2167
|
throw new Error("Not a Git repository");
|
|
1678
2168
|
}
|
|
1679
|
-
const
|
|
1680
|
-
const
|
|
1681
|
-
const
|
|
1682
|
-
const hasLastTurnBase = Boolean(lastTurnBase);
|
|
1683
|
-
const commitSha = opts?.commitSha?.trim() || null;
|
|
1684
|
-
const head = await git(["rev-parse", "--verify", "HEAD"], worktreePath, {
|
|
2169
|
+
const wantUntracked = opts?.includeUntracked !== void 0 ? opts.includeUntracked : !fileOnly && (scope === "commits" && !commitSha || scope === "uncommitted" || scope === "unstaged" || scope === "last_turn");
|
|
2170
|
+
const needsMergeBase = scope === "commits" && !commitSha || includeMeta;
|
|
2171
|
+
const headP = git(["rev-parse", "--verify", "HEAD"], worktreePath, {
|
|
1685
2172
|
reject: false
|
|
1686
2173
|
});
|
|
2174
|
+
const untrackedP = wantUntracked ? listUntrackedPaths(worktreePath, pathFilter) : Promise.resolve([]);
|
|
2175
|
+
const head = await headP;
|
|
1687
2176
|
const hasHead = head.exitCode === 0;
|
|
1688
|
-
const untrackedPaths = await listUntrackedPaths(worktreePath);
|
|
1689
2177
|
if (!hasHead) {
|
|
1690
|
-
const
|
|
2178
|
+
const untrackedPaths2 = await untrackedP;
|
|
2179
|
+
const files2 = scope === "staged" ? [] : includePatches ? (await listUntrackedFiles(worktreePath, maxHunk, untrackedPaths2)).sort(
|
|
2180
|
+
(a, b) => a.path.localeCompare(b.path)
|
|
2181
|
+
) : untrackedStubs(untrackedPaths2).sort(
|
|
1691
2182
|
(a, b) => a.path.localeCompare(b.path)
|
|
1692
2183
|
);
|
|
1693
2184
|
const untrackedStat = {
|
|
@@ -1701,7 +2192,7 @@ async function getDiff(worktreePath, repoPath, opts) {
|
|
|
1701
2192
|
commitSha: null,
|
|
1702
2193
|
base: "(no commits)",
|
|
1703
2194
|
files: files2,
|
|
1704
|
-
stat: files2
|
|
2195
|
+
stat: formatStat(files2),
|
|
1705
2196
|
dirty: files2.length > 0,
|
|
1706
2197
|
unpushed: 0,
|
|
1707
2198
|
hasLastTurnBase,
|
|
@@ -1715,137 +2206,122 @@ async function getDiff(worktreePath, repoPath, opts) {
|
|
|
1715
2206
|
}
|
|
1716
2207
|
};
|
|
1717
2208
|
}
|
|
1718
|
-
let
|
|
1719
|
-
|
|
1720
|
-
|
|
1721
|
-
|
|
1722
|
-
|
|
1723
|
-
|
|
1724
|
-
|
|
1725
|
-
|
|
1726
|
-
|
|
1727
|
-
await
|
|
1728
|
-
|
|
1729
|
-
|
|
1730
|
-
|
|
2209
|
+
let mergeBase = "HEAD";
|
|
2210
|
+
let baseLabel = "HEAD";
|
|
2211
|
+
if (needsMergeBase) {
|
|
2212
|
+
let base = opts?.base;
|
|
2213
|
+
try {
|
|
2214
|
+
base = base ?? await resolveDefaultBranch(repoPath, { network: false });
|
|
2215
|
+
} catch {
|
|
2216
|
+
base = "HEAD";
|
|
2217
|
+
}
|
|
2218
|
+
mergeBase = await resolveMergeBase(worktreePath, base, repoPath);
|
|
2219
|
+
baseLabel = await resolveDiffBaseRef(worktreePath, base, repoPath);
|
|
2220
|
+
}
|
|
2221
|
+
const includeUntracked = wantUntracked;
|
|
2222
|
+
const metaPromise = includeMeta ? Promise.all([
|
|
2223
|
+
computeScopeStats(
|
|
2224
|
+
worktreePath,
|
|
2225
|
+
mergeBase,
|
|
2226
|
+
lastTurnBase,
|
|
2227
|
+
// Cheap: don't wait on the untracked walk just to pad the badge.
|
|
2228
|
+
0
|
|
2229
|
+
),
|
|
2230
|
+
listBranchCommits(worktreePath, repoPath, { base: baseLabel }),
|
|
2231
|
+
isDirty(worktreePath),
|
|
2232
|
+
countUnpushedCommits(worktreePath)
|
|
2233
|
+
]).then(([scopeStats, commits, dirty, unpushed]) => ({
|
|
2234
|
+
scopeStats,
|
|
2235
|
+
commits,
|
|
2236
|
+
dirty,
|
|
2237
|
+
unpushed
|
|
2238
|
+
})) : Promise.resolve({
|
|
2239
|
+
scopeStats: emptyScopeStats(),
|
|
2240
|
+
commits: [],
|
|
2241
|
+
dirty: false,
|
|
2242
|
+
unpushed: 0
|
|
2243
|
+
});
|
|
2244
|
+
if (scope === "last_turn" && !lastTurnBase) {
|
|
2245
|
+
const meta2 = await metaPromise;
|
|
2246
|
+
return {
|
|
2247
|
+
scope,
|
|
2248
|
+
commitSha: null,
|
|
2249
|
+
base: "last turn",
|
|
2250
|
+
files: [],
|
|
2251
|
+
stat: "(no last agent turn)",
|
|
2252
|
+
dirty: meta2.dirty,
|
|
2253
|
+
unpushed: meta2.unpushed,
|
|
2254
|
+
hasLastTurnBase: false,
|
|
2255
|
+
commits: meta2.commits,
|
|
2256
|
+
scopeStats: meta2.scopeStats
|
|
2257
|
+
};
|
|
1731
2258
|
}
|
|
1732
|
-
|
|
1733
|
-
|
|
1734
|
-
worktreePath,
|
|
1735
|
-
mergeBase,
|
|
1736
|
-
lastTurnBase,
|
|
1737
|
-
untrackedPaths.length
|
|
1738
|
-
);
|
|
1739
|
-
const includeUntracked = scope === "commits" && !commitSha || scope === "uncommitted" || scope === "unstaged" || scope === "last_turn";
|
|
1740
|
-
let nameStatus = "";
|
|
1741
|
-
let numstat = "";
|
|
1742
|
-
let combinedDiff = "";
|
|
1743
|
-
let labelBase = base;
|
|
2259
|
+
let diffArgs;
|
|
2260
|
+
let labelBase = baseLabel;
|
|
1744
2261
|
if (scope === "staged") {
|
|
1745
|
-
|
|
1746
|
-
git(["diff", "--name-status", "--cached"], worktreePath, { reject: false }),
|
|
1747
|
-
git(["diff", "--numstat", "--cached"], worktreePath, { reject: false }),
|
|
1748
|
-
git(["diff", "--cached"], worktreePath, { reject: false })
|
|
1749
|
-
]);
|
|
1750
|
-
nameStatus = ns.stdout;
|
|
1751
|
-
numstat = num.stdout;
|
|
1752
|
-
combinedDiff = diff.stdout;
|
|
2262
|
+
diffArgs = ["--cached"];
|
|
1753
2263
|
labelBase = "staged";
|
|
1754
2264
|
} else if (scope === "unstaged") {
|
|
1755
|
-
|
|
1756
|
-
git(["diff", "--name-status"], worktreePath, { reject: false }),
|
|
1757
|
-
git(["diff", "--numstat"], worktreePath, { reject: false }),
|
|
1758
|
-
git(["diff"], worktreePath, { reject: false })
|
|
1759
|
-
]);
|
|
1760
|
-
nameStatus = ns.stdout;
|
|
1761
|
-
numstat = num.stdout;
|
|
1762
|
-
combinedDiff = diff.stdout;
|
|
2265
|
+
diffArgs = [];
|
|
1763
2266
|
labelBase = "unstaged";
|
|
1764
2267
|
} else if (scope === "uncommitted") {
|
|
1765
|
-
|
|
1766
|
-
git(["diff", "--name-status", "HEAD"], worktreePath, { reject: false }),
|
|
1767
|
-
git(["diff", "--numstat", "HEAD"], worktreePath, { reject: false }),
|
|
1768
|
-
git(["diff", "HEAD"], worktreePath, { reject: false })
|
|
1769
|
-
]);
|
|
1770
|
-
nameStatus = ns.stdout;
|
|
1771
|
-
numstat = num.stdout;
|
|
1772
|
-
combinedDiff = diff.stdout;
|
|
2268
|
+
diffArgs = ["HEAD"];
|
|
1773
2269
|
labelBase = "HEAD";
|
|
1774
2270
|
} else if (scope === "last_turn") {
|
|
1775
|
-
|
|
1776
|
-
const commits2 = await listBranchCommits(worktreePath, repoPath, { base });
|
|
1777
|
-
return {
|
|
1778
|
-
scope,
|
|
1779
|
-
commitSha: null,
|
|
1780
|
-
base: "last turn",
|
|
1781
|
-
files: [],
|
|
1782
|
-
stat: "(no last agent turn)",
|
|
1783
|
-
dirty: await isDirty(worktreePath),
|
|
1784
|
-
unpushed: await countUnpushedCommits(worktreePath),
|
|
1785
|
-
hasLastTurnBase: false,
|
|
1786
|
-
commits: commits2,
|
|
1787
|
-
scopeStats
|
|
1788
|
-
};
|
|
1789
|
-
}
|
|
1790
|
-
const [ns, num, diff] = await Promise.all([
|
|
1791
|
-
git(["diff", "--name-status", lastTurnBase], worktreePath, { reject: false }),
|
|
1792
|
-
git(["diff", "--numstat", lastTurnBase], worktreePath, { reject: false }),
|
|
1793
|
-
git(["diff", lastTurnBase], worktreePath, { reject: false })
|
|
1794
|
-
]);
|
|
1795
|
-
nameStatus = ns.stdout;
|
|
1796
|
-
numstat = num.stdout;
|
|
1797
|
-
combinedDiff = diff.stdout;
|
|
2271
|
+
diffArgs = [lastTurnBase];
|
|
1798
2272
|
labelBase = "last turn";
|
|
1799
2273
|
} else if (scope === "commits" && commitSha) {
|
|
1800
|
-
|
|
1801
|
-
const [ns, num, diff] = await Promise.all([
|
|
1802
|
-
git(["diff", "--name-status", range], worktreePath, { reject: false }),
|
|
1803
|
-
git(["diff", "--numstat", range], worktreePath, { reject: false }),
|
|
1804
|
-
git(["diff", range], worktreePath, { reject: false })
|
|
1805
|
-
]);
|
|
1806
|
-
nameStatus = ns.stdout;
|
|
1807
|
-
numstat = num.stdout;
|
|
1808
|
-
combinedDiff = diff.stdout;
|
|
2274
|
+
diffArgs = [`${commitSha}^!`];
|
|
1809
2275
|
labelBase = commitSha.slice(0, 7);
|
|
1810
2276
|
} else {
|
|
1811
|
-
|
|
1812
|
-
|
|
1813
|
-
|
|
1814
|
-
|
|
1815
|
-
|
|
1816
|
-
|
|
1817
|
-
|
|
1818
|
-
|
|
1819
|
-
|
|
1820
|
-
|
|
1821
|
-
|
|
2277
|
+
diffArgs = [mergeBase];
|
|
2278
|
+
labelBase = baseLabel;
|
|
2279
|
+
}
|
|
2280
|
+
const partsPromise = gitDiffParts(
|
|
2281
|
+
worktreePath,
|
|
2282
|
+
diffArgs,
|
|
2283
|
+
includePatches,
|
|
2284
|
+
pathFilter
|
|
2285
|
+
);
|
|
2286
|
+
const [parts, meta, untrackedPaths] = await Promise.all([
|
|
2287
|
+
partsPromise,
|
|
2288
|
+
metaPromise,
|
|
2289
|
+
untrackedP
|
|
2290
|
+
]);
|
|
1822
2291
|
const filesMap = /* @__PURE__ */ new Map();
|
|
1823
|
-
for (const file of filesFromDiff(
|
|
2292
|
+
for (const file of filesFromDiff(
|
|
2293
|
+
parts.nameStatus,
|
|
2294
|
+
parts.numstat,
|
|
2295
|
+
parts.combinedDiff,
|
|
2296
|
+
maxHunk
|
|
2297
|
+
)) {
|
|
1824
2298
|
filesMap.set(file.path, file);
|
|
1825
2299
|
}
|
|
1826
2300
|
if (includeUntracked) {
|
|
1827
|
-
const
|
|
1828
|
-
for (const file of
|
|
2301
|
+
const extra = includePatches ? await listUntrackedFiles(worktreePath, maxHunk, untrackedPaths) : untrackedStubs(untrackedPaths);
|
|
2302
|
+
for (const file of extra) {
|
|
1829
2303
|
if (!filesMap.has(file.path)) filesMap.set(file.path, file);
|
|
1830
2304
|
}
|
|
2305
|
+
} else if (fileOnly && pathFilter && !filesMap.has(pathFilter)) {
|
|
2306
|
+
const maybe = await listUntrackedPaths(worktreePath, pathFilter);
|
|
2307
|
+
if (maybe.length > 0) {
|
|
2308
|
+
const extra = includePatches ? await listUntrackedFiles(worktreePath, maxHunk, maybe) : untrackedStubs(maybe);
|
|
2309
|
+
for (const file of extra) filesMap.set(file.path, file);
|
|
2310
|
+
}
|
|
1831
2311
|
}
|
|
1832
2312
|
const files = [...filesMap.values()].sort((a, b) => a.path.localeCompare(b.path));
|
|
1833
|
-
const { stdout: statOut } = await git(
|
|
1834
|
-
scope === "staged" ? ["diff", "--stat", "--cached"] : scope === "unstaged" ? ["diff", "--stat"] : scope === "uncommitted" ? ["diff", "--stat", "HEAD"] : scope === "last_turn" && lastTurnBase ? ["diff", "--stat", lastTurnBase] : scope === "commits" && commitSha ? ["diff", "--stat", `${commitSha}^!`] : ["diff", "--stat", mergeBase],
|
|
1835
|
-
worktreePath,
|
|
1836
|
-
{ reject: false }
|
|
1837
|
-
);
|
|
1838
2313
|
return {
|
|
1839
2314
|
scope,
|
|
1840
2315
|
commitSha: scope === "commits" ? commitSha : null,
|
|
1841
2316
|
base: labelBase,
|
|
1842
2317
|
files,
|
|
1843
|
-
stat:
|
|
1844
|
-
dirty:
|
|
1845
|
-
unpushed:
|
|
2318
|
+
stat: formatStat(files),
|
|
2319
|
+
dirty: fileOnly || !includeMeta ? files.length > 0 : meta.dirty,
|
|
2320
|
+
unpushed: meta.unpushed,
|
|
1846
2321
|
hasLastTurnBase,
|
|
1847
|
-
commits,
|
|
1848
|
-
scopeStats
|
|
2322
|
+
commits: meta.commits,
|
|
2323
|
+
scopeStats: meta.scopeStats,
|
|
2324
|
+
mergeBase: needsMergeBase ? mergeBase : null
|
|
1849
2325
|
};
|
|
1850
2326
|
}
|
|
1851
2327
|
async function listWorktreeFiles(worktreePath, opts) {
|
|
@@ -2472,16 +2948,16 @@ var PASTE_ATTACH_MIN_CHARS = 1200;
|
|
|
2472
2948
|
var PASTE_ATTACH_MIN_LINES = 15;
|
|
2473
2949
|
var PASTED_NAME_RE = /^Pasted text #(\d+)\.txt$/i;
|
|
2474
2950
|
var PASTED_NAME_ALT_RE = /^pasted-(\d+)\.txt$/i;
|
|
2475
|
-
function pastedTextStats(
|
|
2476
|
-
const chars =
|
|
2951
|
+
function pastedTextStats(text2) {
|
|
2952
|
+
const chars = text2.length;
|
|
2477
2953
|
if (chars === 0) return { chars: 0, lines: 0 };
|
|
2478
|
-
const lines =
|
|
2954
|
+
const lines = text2.split(/\r\n|\r|\n/).length;
|
|
2479
2955
|
return { chars, lines };
|
|
2480
2956
|
}
|
|
2481
|
-
function shouldAttachPastedText(
|
|
2482
|
-
const trimmed =
|
|
2957
|
+
function shouldAttachPastedText(text2) {
|
|
2958
|
+
const trimmed = text2.trim();
|
|
2483
2959
|
if (!trimmed) return false;
|
|
2484
|
-
const { chars, lines } = pastedTextStats(
|
|
2960
|
+
const { chars, lines } = pastedTextStats(text2);
|
|
2485
2961
|
return chars >= PASTE_ATTACH_MIN_CHARS || lines >= PASTE_ATTACH_MIN_LINES;
|
|
2486
2962
|
}
|
|
2487
2963
|
function nextPastedTextName(existing) {
|
|
@@ -2492,13 +2968,13 @@ function nextPastedTextName(existing) {
|
|
|
2492
2968
|
}
|
|
2493
2969
|
return `Pasted text #${max + 1}.txt`;
|
|
2494
2970
|
}
|
|
2495
|
-
function buildPastedTextAttachment(
|
|
2971
|
+
function buildPastedTextAttachment(text2, opts) {
|
|
2496
2972
|
return {
|
|
2497
2973
|
id: opts?.id ?? randomUUID2(),
|
|
2498
2974
|
name: opts?.name ?? "Pasted text #1.txt",
|
|
2499
2975
|
kind: "file",
|
|
2500
2976
|
path: opts?.path,
|
|
2501
|
-
content:
|
|
2977
|
+
content: text2
|
|
2502
2978
|
};
|
|
2503
2979
|
}
|
|
2504
2980
|
|
|
@@ -2546,9 +3022,9 @@ async function tryClaudeSummary(transcript, opts) {
|
|
|
2546
3022
|
{ cwd: opts?.cwd, reject: false }
|
|
2547
3023
|
);
|
|
2548
3024
|
if (exitCode !== 0) return null;
|
|
2549
|
-
const
|
|
2550
|
-
if (
|
|
2551
|
-
return
|
|
3025
|
+
const text2 = stdout.trim();
|
|
3026
|
+
if (text2.length < 40) return null;
|
|
3027
|
+
return text2;
|
|
2552
3028
|
} catch {
|
|
2553
3029
|
return null;
|
|
2554
3030
|
}
|
|
@@ -2597,9 +3073,9 @@ function extractiveSummary(transcript) {
|
|
|
2597
3073
|
);
|
|
2598
3074
|
return parts.join("\n");
|
|
2599
3075
|
}
|
|
2600
|
-
function clipSummary(
|
|
2601
|
-
if (
|
|
2602
|
-
return `${
|
|
3076
|
+
function clipSummary(text2) {
|
|
3077
|
+
if (text2.length <= MAX_SUMMARY_CHARS) return text2;
|
|
3078
|
+
return `${text2.slice(0, MAX_SUMMARY_CHARS)}
|
|
2603
3079
|
|
|
2604
3080
|
[\u2026summary truncated\u2026]`;
|
|
2605
3081
|
}
|
|
@@ -2864,7 +3340,7 @@ async function previewLand(thread) {
|
|
|
2864
3340
|
const target = await resolveDefaultBranch(thread.repoPath);
|
|
2865
3341
|
if (thread.branchName === target || thread.branchName === "main" || thread.branchName === "master") {
|
|
2866
3342
|
}
|
|
2867
|
-
const { stdout: head } = await import("./run-
|
|
3343
|
+
const { stdout: head } = await import("./run-V2WZUMJA.js").then(
|
|
2868
3344
|
({ git: git2 }) => git2(["rev-parse", "--abbrev-ref", "HEAD"], thread.worktreePath)
|
|
2869
3345
|
);
|
|
2870
3346
|
const current = head.trim();
|
|
@@ -2903,7 +3379,7 @@ async function confirmLand(thread, opts) {
|
|
|
2903
3379
|
if (preview.dirty) {
|
|
2904
3380
|
committed = await commitAll(thread.worktreePath, meta.commitMessage);
|
|
2905
3381
|
}
|
|
2906
|
-
const { git: git2 } = await import("./run-
|
|
3382
|
+
const { git: git2 } = await import("./run-V2WZUMJA.js");
|
|
2907
3383
|
const { stdout: headOut } = await git2(
|
|
2908
3384
|
["rev-parse", "--abbrev-ref", "HEAD"],
|
|
2909
3385
|
thread.worktreePath,
|
|
@@ -3001,7 +3477,7 @@ async function createThread(input, _onSetupLine) {
|
|
|
3001
3477
|
return readThread(thread.id) ?? thread;
|
|
3002
3478
|
}
|
|
3003
3479
|
async function listLinearIssues(agent, repoPath) {
|
|
3004
|
-
const { getAdapter: getAdapter2 } = await import("./agents-
|
|
3480
|
+
const { getAdapter: getAdapter2 } = await import("./agents-TCAK3MXN.js");
|
|
3005
3481
|
await requireAgent(agent, { requireLinear: true });
|
|
3006
3482
|
const adapter = getAdapter2(agent);
|
|
3007
3483
|
if (!adapter.listLinearIssues) {
|
|
@@ -3499,13 +3975,13 @@ function resolveConductorCursorAgentId(workspacePath) {
|
|
|
3499
3975
|
for (const hash of hashes) {
|
|
3500
3976
|
const agentsFile = join7(CURSOR_SDK_STORE, hash, "agents.ndjson");
|
|
3501
3977
|
if (!existsSync9(agentsFile)) continue;
|
|
3502
|
-
let
|
|
3978
|
+
let text2;
|
|
3503
3979
|
try {
|
|
3504
|
-
|
|
3980
|
+
text2 = readFileSync7(agentsFile, "utf8");
|
|
3505
3981
|
} catch {
|
|
3506
3982
|
continue;
|
|
3507
3983
|
}
|
|
3508
|
-
for (const line of
|
|
3984
|
+
for (const line of text2.split("\n")) {
|
|
3509
3985
|
const trimmed = line.trim();
|
|
3510
3986
|
if (!trimmed) continue;
|
|
3511
3987
|
try {
|
|
@@ -3529,7 +4005,7 @@ async function adoptThread(input) {
|
|
|
3529
4005
|
throw new Error(`Worktree not found: ${input.worktreePath}`);
|
|
3530
4006
|
}
|
|
3531
4007
|
const repoPath = await resolveRepoRoot(input.worktreePath);
|
|
3532
|
-
const { stdout: branch } = await import("./run-
|
|
4008
|
+
const { stdout: branch } = await import("./run-V2WZUMJA.js").then(
|
|
3533
4009
|
({ git: git2 }) => git2(["rev-parse", "--abbrev-ref", "HEAD"], input.worktreePath)
|
|
3534
4010
|
);
|
|
3535
4011
|
const thread = createEmptyThread({
|
|
@@ -3544,7 +4020,7 @@ async function adoptThread(input) {
|
|
|
3544
4020
|
messages: input.messages ?? []
|
|
3545
4021
|
});
|
|
3546
4022
|
writeThread(thread);
|
|
3547
|
-
const { ensureWorkspace: ensureWorkspace2 } = await import("./workspaces-
|
|
4023
|
+
const { ensureWorkspace: ensureWorkspace2 } = await import("./workspaces-AHFTHVBT.js");
|
|
3548
4024
|
await ensureWorkspace2(repoPath);
|
|
3549
4025
|
return thread;
|
|
3550
4026
|
}
|
|
@@ -4233,7 +4709,7 @@ function buildReviewRequestAttachment(content, opts) {
|
|
|
4233
4709
|
function readExistingReviewRequestFile(worktreePath) {
|
|
4234
4710
|
return readTextIfPresent(join10(worktreePath, REPO_REVIEW_PATH)) ?? readTextIfPresent(join10(worktreePath, REVIEW_REQUEST_PATH)) ?? readTextIfPresent(join10(worktreePath, LEGACY_REVIEW_REQUEST_PATH));
|
|
4235
4711
|
}
|
|
4236
|
-
async function requestReview(threadRef,
|
|
4712
|
+
async function requestReview(threadRef, send2) {
|
|
4237
4713
|
const from = findThreadByRef(threadRef);
|
|
4238
4714
|
if (!from) throw new Error(`Thread not found: ${threadRef}`);
|
|
4239
4715
|
if (isOrchestratorThread(from)) {
|
|
@@ -4255,7 +4731,7 @@ async function requestReview(threadRef, send) {
|
|
|
4255
4731
|
})
|
|
4256
4732
|
]
|
|
4257
4733
|
});
|
|
4258
|
-
const started = await
|
|
4734
|
+
const started = await send2(tab.id, REVIEW_REQUEST_PREFILL);
|
|
4259
4735
|
return { tab: started, from };
|
|
4260
4736
|
}
|
|
4261
4737
|
|
|
@@ -4311,8 +4787,8 @@ function buildQuotaHandoffAttachment(from, limitText, fallbackAgent) {
|
|
|
4311
4787
|
);
|
|
4312
4788
|
const recent = from.messages.slice(-8).map((m) => {
|
|
4313
4789
|
const role = m.role === "user" ? "User" : m.role === "agent" ? "Agent" : "Summary";
|
|
4314
|
-
const
|
|
4315
|
-
return
|
|
4790
|
+
const text2 = m.text.trim().replace(/\s+/g, " ").slice(0, 280);
|
|
4791
|
+
return text2 ? `- ${role}: ${text2}` : null;
|
|
4316
4792
|
}).filter(Boolean);
|
|
4317
4793
|
const body = [
|
|
4318
4794
|
`# Orchestration handoff`,
|
|
@@ -4495,7 +4971,7 @@ var Orchestrator = class {
|
|
|
4495
4971
|
orphans: orphans.map((o) => ({ path: o.path, repoPath: o.repoPath }))
|
|
4496
4972
|
});
|
|
4497
4973
|
}
|
|
4498
|
-
const { autoCleanupOrphansEnabled: autoCleanupOrphansEnabled2 } = await import("./app-settings-
|
|
4974
|
+
const { autoCleanupOrphansEnabled: autoCleanupOrphansEnabled2 } = await import("./app-settings-3TLA2EJ7.js");
|
|
4499
4975
|
if (autoCleanupOrphansEnabled2() && shouldRunWorktreeCleanup() && orphans.length > 0) {
|
|
4500
4976
|
await cleanupOrphanWorktrees({ repoPaths });
|
|
4501
4977
|
}
|
|
@@ -4652,7 +5128,7 @@ var Orchestrator = class {
|
|
|
4652
5128
|
}
|
|
4653
5129
|
async finishCreateThread(threadId, prompt) {
|
|
4654
5130
|
await this.runSetupAfterCreate(threadId);
|
|
4655
|
-
const { autoRunAfterSetupEnabled: autoRunAfterSetupEnabled2 } = await import("./app-settings-
|
|
5131
|
+
const { autoRunAfterSetupEnabled: autoRunAfterSetupEnabled2 } = await import("./app-settings-3TLA2EJ7.js");
|
|
4656
5132
|
if (autoRunAfterSetupEnabled2()) {
|
|
4657
5133
|
try {
|
|
4658
5134
|
await this.startDev(threadId);
|
|
@@ -4732,11 +5208,11 @@ var Orchestrator = class {
|
|
|
4732
5208
|
return results;
|
|
4733
5209
|
}
|
|
4734
5210
|
/** Edit the text of a not-yet-started queued message. */
|
|
4735
|
-
async editQueuedMessage(threadRef, index,
|
|
5211
|
+
async editQueuedMessage(threadRef, index, text2) {
|
|
4736
5212
|
const thread = this.requireThread(threadRef);
|
|
4737
5213
|
return withThreadLock(thread.id, async () => {
|
|
4738
5214
|
const current = this.requireThread(thread.id);
|
|
4739
|
-
const trimmed =
|
|
5215
|
+
const trimmed = text2.trim();
|
|
4740
5216
|
if (!trimmed || index < 0 || index >= current.queue.length) {
|
|
4741
5217
|
return current;
|
|
4742
5218
|
}
|
|
@@ -4881,10 +5357,19 @@ var Orchestrator = class {
|
|
|
4881
5357
|
attachments: sentAttachments ?? thread.attachments
|
|
4882
5358
|
}
|
|
4883
5359
|
);
|
|
4884
|
-
const
|
|
4885
|
-
|
|
4886
|
-
|
|
4887
|
-
|
|
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;
|
|
4888
5373
|
const artifactReminder = thread.agent !== "brightsy" ? [
|
|
4889
5374
|
"Sideboard side column (important):",
|
|
4890
5375
|
"There is no claude.ai Artifact tool here \u2014 that is normal.",
|
|
@@ -4924,7 +5409,7 @@ var Orchestrator = class {
|
|
|
4924
5409
|
});
|
|
4925
5410
|
const artifactDirective = isBrightsy ? null : formatArtifactDirective();
|
|
4926
5411
|
const settings = loadWorkspaceSettings(fresh.worktreePath, fresh.repoPath);
|
|
4927
|
-
const { autoRenameBranchEnabled: autoRenameBranchEnabled2 } = await import("./app-settings-
|
|
5412
|
+
const { autoRenameBranchEnabled: autoRenameBranchEnabled2 } = await import("./app-settings-3TLA2EJ7.js");
|
|
4928
5413
|
const renameBranchDirective = !isBrightsy && !isOrchestration && autoRenameBranchEnabled2() ? formatRenameBranchDirective(fresh, {
|
|
4929
5414
|
customPrompt: settings?.prompts?.renameBranch
|
|
4930
5415
|
}) : null;
|
|
@@ -4944,7 +5429,9 @@ var Orchestrator = class {
|
|
|
4944
5429
|
goal: fresh.sourceRef || fresh.title || "Orchestration",
|
|
4945
5430
|
parentId: fresh.id,
|
|
4946
5431
|
workspaces: inventory,
|
|
4947
|
-
audience:
|
|
5432
|
+
audience: /^(Slack DM|Slack @mention)(?:\n|$)/.test(
|
|
5433
|
+
expandedPrompt.trim()
|
|
5434
|
+
) ? "slack" : "desktop"
|
|
4948
5435
|
});
|
|
4949
5436
|
}
|
|
4950
5437
|
}
|
|
@@ -5487,6 +5974,11 @@ var Orchestrator = class {
|
|
|
5487
5974
|
return getDiff(thread.worktreePath, thread.repoPath, {
|
|
5488
5975
|
scope: opts?.scope,
|
|
5489
5976
|
commitSha: opts?.commitSha,
|
|
5977
|
+
base: opts?.base,
|
|
5978
|
+
includePatches: opts?.includePatches,
|
|
5979
|
+
includeMeta: opts?.includeMeta,
|
|
5980
|
+
includeUntracked: opts?.includeUntracked,
|
|
5981
|
+
path: opts?.path,
|
|
5490
5982
|
lastTurnBase: this.turnBaselines.get(thread.id) ?? null
|
|
5491
5983
|
});
|
|
5492
5984
|
}
|
|
@@ -5564,7 +6056,8 @@ var Orchestrator = class {
|
|
|
5564
6056
|
isDraft: false,
|
|
5565
6057
|
reviewDecision: null,
|
|
5566
6058
|
baseRefName: "",
|
|
5567
|
-
headRefName: ""
|
|
6059
|
+
headRefName: "",
|
|
6060
|
+
isInMergeQueue: false
|
|
5568
6061
|
};
|
|
5569
6062
|
await this.persistPrMetaAndMaybeArchive(thread, metaLike);
|
|
5570
6063
|
return { url: metaLike.url, state };
|
|
@@ -5614,7 +6107,7 @@ var Orchestrator = class {
|
|
|
5614
6107
|
updateThread(thread.id, { title: meta.title });
|
|
5615
6108
|
}
|
|
5616
6109
|
}
|
|
5617
|
-
const { autoArchiveOnMergeEnabled: autoArchiveOnMergeEnabled2 } = await import("./app-settings-
|
|
6110
|
+
const { autoArchiveOnMergeEnabled: autoArchiveOnMergeEnabled2 } = await import("./app-settings-3TLA2EJ7.js");
|
|
5618
6111
|
const latest = this.requireThread(thread.id);
|
|
5619
6112
|
if (!shouldAutoArchiveOnPrMerge({
|
|
5620
6113
|
previousPrState: prevState || null,
|
|
@@ -5838,7 +6331,7 @@ var Orchestrator = class {
|
|
|
5838
6331
|
this.emit({ type: "status_changed", threadId: archived.id, status: "archived" });
|
|
5839
6332
|
if (thread.repoPath && !isGlobalRepoPath(thread.repoPath)) {
|
|
5840
6333
|
try {
|
|
5841
|
-
const { ensureWorkspace: ensureWorkspace2 } = await import("./workspaces-
|
|
6334
|
+
const { ensureWorkspace: ensureWorkspace2 } = await import("./workspaces-AHFTHVBT.js");
|
|
5842
6335
|
await ensureWorkspace2(thread.repoPath);
|
|
5843
6336
|
} catch {
|
|
5844
6337
|
}
|
|
@@ -5859,7 +6352,7 @@ var Orchestrator = class {
|
|
|
5859
6352
|
await runArchiveScript(thread.repoPath, thread.worktreePath);
|
|
5860
6353
|
} catch {
|
|
5861
6354
|
}
|
|
5862
|
-
const { deleteBranchOnPurgeEnabled: deleteBranchOnPurgeEnabled2 } = await import("./app-settings-
|
|
6355
|
+
const { deleteBranchOnPurgeEnabled: deleteBranchOnPurgeEnabled2 } = await import("./app-settings-3TLA2EJ7.js");
|
|
5863
6356
|
const deleteBranch = opts?.deleteBranch ?? deleteBranchOnPurgeEnabled2();
|
|
5864
6357
|
await removeWorktree(thread.repoPath, thread.worktreePath, {
|
|
5865
6358
|
deleteBranch: deleteBranch ? thread.branchName : void 0
|
|
@@ -5880,7 +6373,7 @@ var Orchestrator = class {
|
|
|
5880
6373
|
return restored2;
|
|
5881
6374
|
}
|
|
5882
6375
|
if (!existsSync13(thread.worktreePath)) {
|
|
5883
|
-
const { createThreadWorktree: createThreadWorktree2 } = await import("./worktree-
|
|
6376
|
+
const { createThreadWorktree: createThreadWorktree2 } = await import("./worktree-VOCH3WS3.js");
|
|
5884
6377
|
const { execa: execa6 } = await import("execa");
|
|
5885
6378
|
const slug = thread.worktreePath.split("/").pop();
|
|
5886
6379
|
const dest = thread.worktreePath;
|
|
@@ -5989,7 +6482,7 @@ async function startOrchestration(opts) {
|
|
|
5989
6482
|
sourceRef: "default",
|
|
5990
6483
|
...createOpts
|
|
5991
6484
|
}).catch(async () => {
|
|
5992
|
-
const { resolveDefaultBranch: resolveDefaultBranch2, resolveRepoRoot: resolveRepoRoot2 } = await import("./worktree-
|
|
6485
|
+
const { resolveDefaultBranch: resolveDefaultBranch2, resolveRepoRoot: resolveRepoRoot2 } = await import("./worktree-VOCH3WS3.js");
|
|
5993
6486
|
const repo = await resolveRepoRoot2(repoPath);
|
|
5994
6487
|
const def = await resolveDefaultBranch2(repo);
|
|
5995
6488
|
return createThread({
|
|
@@ -6111,7 +6604,7 @@ function formatPlanQuestionsForChat(questions) {
|
|
|
6111
6604
|
// src/mcp/server.ts
|
|
6112
6605
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
6113
6606
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
6114
|
-
import { z } from "zod";
|
|
6607
|
+
import { z as z2 } from "zod";
|
|
6115
6608
|
import { basename as basename4 } from "path";
|
|
6116
6609
|
|
|
6117
6610
|
// src/mcp/archive-guard.ts
|
|
@@ -6122,35 +6615,907 @@ function mcpArchiveBlockedReason(thread) {
|
|
|
6122
6615
|
return null;
|
|
6123
6616
|
}
|
|
6124
6617
|
|
|
6125
|
-
// src/mcp/
|
|
6126
|
-
|
|
6127
|
-
|
|
6128
|
-
|
|
6129
|
-
|
|
6130
|
-
|
|
6131
|
-
|
|
6132
|
-
}
|
|
6133
|
-
|
|
6134
|
-
|
|
6135
|
-
|
|
6136
|
-
|
|
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
|
|
6649
|
+
});
|
|
6650
|
+
const json = await res.json();
|
|
6651
|
+
if (!json.ok) {
|
|
6652
|
+
throw new SlackApiError(method, json.error || `HTTP ${res.status}`);
|
|
6653
|
+
}
|
|
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
|
|
6137
6691
|
},
|
|
6138
|
-
|
|
6139
|
-
clearTimeout(timer);
|
|
6140
|
-
reject(err);
|
|
6141
|
-
}
|
|
6692
|
+
opts?.fetchImpl
|
|
6142
6693
|
);
|
|
6143
|
-
|
|
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);
|
|
6144
6712
|
}
|
|
6145
|
-
async function
|
|
6146
|
-
|
|
6147
|
-
|
|
6148
|
-
const { maxConcurrentAgents: maxConcurrentAgents2 } = await import("./app-settings-ONKMFSEJ.js");
|
|
6149
|
-
orch.setMaxConcurrent(maxConcurrentAgents2());
|
|
6150
|
-
} catch {
|
|
6713
|
+
async function findUserId(token, handle, fetchImpl) {
|
|
6714
|
+
if (isUserId(handle)) {
|
|
6715
|
+
return { id: handle, name: handle };
|
|
6151
6716
|
}
|
|
6152
|
-
|
|
6153
|
-
|
|
6717
|
+
if (isEmail(handle)) {
|
|
6718
|
+
try {
|
|
6719
|
+
const data = await slackApi(
|
|
6720
|
+
token,
|
|
6721
|
+
"users.lookupByEmail",
|
|
6722
|
+
{ email: handle },
|
|
6723
|
+
fetchImpl
|
|
6724
|
+
);
|
|
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 {
|
|
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()
|
|
6740
|
+
);
|
|
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.`
|
|
6748
|
+
);
|
|
6749
|
+
}
|
|
6750
|
+
async function openDmChannel(token, userId, fetchImpl) {
|
|
6751
|
+
const data = await slackApi(
|
|
6752
|
+
token,
|
|
6753
|
+
"conversations.open",
|
|
6754
|
+
{ users: userId },
|
|
6755
|
+
fetchImpl
|
|
6756
|
+
);
|
|
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",
|
|
6765
|
+
{
|
|
6766
|
+
types: "public_channel,private_channel",
|
|
6767
|
+
exclude_archived: true,
|
|
6768
|
+
limit: 200
|
|
6769
|
+
},
|
|
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
|
|
6798
|
+
};
|
|
6799
|
+
}
|
|
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
|
|
6824
|
+
};
|
|
6825
|
+
} catch {
|
|
6826
|
+
throw channelErr;
|
|
6827
|
+
}
|
|
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
|
+
});
|
|
7290
|
+
}
|
|
7291
|
+
);
|
|
7292
|
+
server.tool(
|
|
7293
|
+
"slack_list_channels",
|
|
7294
|
+
"List channels in a connected Slack workspace. Pass team_id from list_teams.",
|
|
7295
|
+
{
|
|
7296
|
+
team_id: z.string(),
|
|
7297
|
+
limit: z.number().optional()
|
|
7298
|
+
},
|
|
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
|
+
}
|
|
7321
|
+
}
|
|
7322
|
+
);
|
|
7323
|
+
server.tool(
|
|
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.",
|
|
7326
|
+
{
|
|
7327
|
+
team_id: z.string(),
|
|
7328
|
+
query: z.string().optional(),
|
|
7329
|
+
limit: z.number().optional()
|
|
7330
|
+
},
|
|
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
|
+
}
|
|
7345
|
+
}
|
|
7346
|
+
);
|
|
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
|
+
});
|
|
7409
|
+
}
|
|
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)"));
|
|
7441
|
+
}
|
|
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 {
|
|
7472
|
+
}
|
|
7473
|
+
}
|
|
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
|
|
7482
|
+
});
|
|
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 });
|
|
6154
7519
|
} catch (err) {
|
|
6155
7520
|
console.error(
|
|
6156
7521
|
"[sideboard-mcp] reconcile on boot failed (continuing):",
|
|
@@ -6198,7 +7563,7 @@ async function startMcpServer() {
|
|
|
6198
7563
|
server.tool(
|
|
6199
7564
|
"get_thread",
|
|
6200
7565
|
"Get a compact thread summary by id/ref",
|
|
6201
|
-
{ ref:
|
|
7566
|
+
{ ref: z2.string() },
|
|
6202
7567
|
async ({ ref }) => {
|
|
6203
7568
|
const t = orch.getThread(ref);
|
|
6204
7569
|
if (!t) {
|
|
@@ -6227,14 +7592,14 @@ async function startMcpServer() {
|
|
|
6227
7592
|
"present_artifact",
|
|
6228
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.",
|
|
6229
7594
|
{
|
|
6230
|
-
title:
|
|
6231
|
-
type:
|
|
7595
|
+
title: z2.string().describe("Short title shown in the artifact pane header"),
|
|
7596
|
+
type: z2.enum(["html", "svg", "markdown", "react"]).describe(
|
|
6232
7597
|
"Artifact kind \u2014 html opens an iframe preview; react transpiles and renders a default-exported component in a sandboxed iframe"
|
|
6233
7598
|
),
|
|
6234
|
-
content:
|
|
7599
|
+
content: z2.string().describe(
|
|
6235
7600
|
"Full document body (complete HTML page, SVG markup, markdown, or a React component module with a default export)"
|
|
6236
7601
|
),
|
|
6237
|
-
artifact_id:
|
|
7602
|
+
artifact_id: z2.string().optional().describe("Stable id when updating the same artifact across turns")
|
|
6238
7603
|
},
|
|
6239
7604
|
async ({ title, type, artifact_id }) => {
|
|
6240
7605
|
const id = artifact_id?.trim() || `artifact_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 8)}`;
|
|
@@ -6252,15 +7617,15 @@ async function startMcpServer() {
|
|
|
6252
7617
|
"ask_user",
|
|
6253
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.",
|
|
6254
7619
|
{
|
|
6255
|
-
questions:
|
|
6256
|
-
|
|
6257
|
-
question:
|
|
6258
|
-
header:
|
|
6259
|
-
multiSelect:
|
|
6260
|
-
options:
|
|
6261
|
-
|
|
6262
|
-
label:
|
|
6263
|
-
description:
|
|
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)")
|
|
6264
7629
|
})
|
|
6265
7630
|
).min(2).max(6).describe("2\u20136 choices (Sideboard also offers Other)")
|
|
6266
7631
|
})
|
|
@@ -6279,9 +7644,9 @@ async function startMcpServer() {
|
|
|
6279
7644
|
"present_plan",
|
|
6280
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.",
|
|
6281
7646
|
{
|
|
6282
|
-
title:
|
|
6283
|
-
content:
|
|
6284
|
-
thread_id:
|
|
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")
|
|
6285
7650
|
},
|
|
6286
7651
|
async ({ title, content, thread_id }) => {
|
|
6287
7652
|
const { writePlanFile: writePlanFile2 } = await import("./plan-file-6O7G4VPQ.js");
|
|
@@ -6302,17 +7667,17 @@ async function startMcpServer() {
|
|
|
6302
7667
|
);
|
|
6303
7668
|
server.tool(
|
|
6304
7669
|
"present_schema",
|
|
6305
|
-
"Open Sideboard\u2019s schema-driven
|
|
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.",
|
|
6306
7671
|
{
|
|
6307
|
-
title:
|
|
6308
|
-
mode:
|
|
6309
|
-
datasource:
|
|
6310
|
-
resource_id:
|
|
6311
|
-
record_id:
|
|
6312
|
-
resource:
|
|
6313
|
-
record:
|
|
6314
|
-
records:
|
|
6315
|
-
pane_id:
|
|
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")
|
|
6316
7681
|
},
|
|
6317
7682
|
async (args) => {
|
|
6318
7683
|
const id = args.pane_id?.trim() || `schema_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 8)}`;
|
|
@@ -6333,10 +7698,10 @@ async function startMcpServer() {
|
|
|
6333
7698
|
"present_files",
|
|
6334
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.",
|
|
6335
7700
|
{
|
|
6336
|
-
title:
|
|
6337
|
-
datasource:
|
|
6338
|
-
path:
|
|
6339
|
-
pane_id:
|
|
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")
|
|
6340
7705
|
},
|
|
6341
7706
|
async (args) => {
|
|
6342
7707
|
const id = args.pane_id?.trim() || `files_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 8)}`;
|
|
@@ -6351,23 +7716,62 @@ async function startMcpServer() {
|
|
|
6351
7716
|
return { content: [{ type: "text", text: JSON.stringify(payload) }] };
|
|
6352
7717
|
}
|
|
6353
7718
|
);
|
|
7719
|
+
registerSlackTools(server);
|
|
6354
7720
|
if (sideboardMcpProfile() !== "worktree") {
|
|
6355
|
-
const {
|
|
7721
|
+
const { getCaffeinateHold: getCaffeinateHold2, setCaffeinateHold: setCaffeinateHold2 } = await import("./caffeinate-hold-KLC6SABD.js");
|
|
7722
|
+
const { resolveNewThreadOptions: resolveNewThreadOptions2, resolveThreadDefaults: resolveThreadDefaults2 } = await import("./app-settings-3TLA2EJ7.js");
|
|
6356
7723
|
const accountDefaults = resolveThreadDefaults2();
|
|
6357
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) {
|
|
7734
|
+
return {
|
|
7735
|
+
content: [
|
|
7736
|
+
{
|
|
7737
|
+
type: "text",
|
|
7738
|
+
text: JSON.stringify({
|
|
7739
|
+
...state,
|
|
7740
|
+
ok: false,
|
|
7741
|
+
message: state.platform === "darwin" ? "Could not start caffeinate." : "Caffeinate is macOS only."
|
|
7742
|
+
})
|
|
7743
|
+
}
|
|
7744
|
+
],
|
|
7745
|
+
isError: true
|
|
7746
|
+
};
|
|
7747
|
+
}
|
|
7748
|
+
return {
|
|
7749
|
+
content: [
|
|
7750
|
+
{
|
|
7751
|
+
type: "text",
|
|
7752
|
+
text: JSON.stringify({
|
|
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)."
|
|
7756
|
+
})
|
|
7757
|
+
}
|
|
7758
|
+
]
|
|
7759
|
+
};
|
|
7760
|
+
}
|
|
7761
|
+
);
|
|
6358
7762
|
server.tool(
|
|
6359
7763
|
"create_thread",
|
|
6360
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.`,
|
|
6361
7765
|
{
|
|
6362
|
-
sourceType:
|
|
6363
|
-
sourceRef:
|
|
6364
|
-
agent:
|
|
6365
|
-
model:
|
|
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(
|
|
6366
7770
|
`Usually omit to use Account default model (${accountDefaults.model?.trim() || "Auto"}). Pass null only to force Auto / agent-default.`
|
|
6367
7771
|
),
|
|
6368
|
-
repoPath:
|
|
6369
|
-
title:
|
|
6370
|
-
parentThreadId:
|
|
7772
|
+
repoPath: z2.string(),
|
|
7773
|
+
title: z2.string().optional(),
|
|
7774
|
+
parentThreadId: z2.string().optional().describe(
|
|
6371
7775
|
"Orchestration: omit (preferred) or pass YOUR chat id from the turn reminder / AGENTS.md. Do not invent uuids."
|
|
6372
7776
|
)
|
|
6373
7777
|
},
|
|
@@ -6474,9 +7878,9 @@ async function startMcpServer() {
|
|
|
6474
7878
|
"send_to_thread",
|
|
6475
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.",
|
|
6476
7880
|
{
|
|
6477
|
-
ref:
|
|
6478
|
-
prompt:
|
|
6479
|
-
force_stop:
|
|
7881
|
+
ref: z2.string(),
|
|
7882
|
+
prompt: z2.string(),
|
|
7883
|
+
force_stop: z2.boolean().optional()
|
|
6480
7884
|
},
|
|
6481
7885
|
async ({ ref, prompt, force_stop }) => {
|
|
6482
7886
|
if (force_stop) {
|
|
@@ -6505,8 +7909,8 @@ async function startMcpServer() {
|
|
|
6505
7909
|
"wait_for_turn",
|
|
6506
7910
|
"Block until the thread finishes its current/queued turn (avoids polling). Use after send_to_thread to read the agent reply.",
|
|
6507
7911
|
{
|
|
6508
|
-
ref:
|
|
6509
|
-
timeoutMs:
|
|
7912
|
+
ref: z2.string(),
|
|
7913
|
+
timeoutMs: z2.number().optional()
|
|
6510
7914
|
},
|
|
6511
7915
|
async ({ ref, timeoutMs }) => {
|
|
6512
7916
|
const thread = await orch.waitForTurn(ref, timeoutMs ?? 6e5);
|
|
@@ -6528,7 +7932,7 @@ async function startMcpServer() {
|
|
|
6528
7932
|
server.tool(
|
|
6529
7933
|
"get_turn_result",
|
|
6530
7934
|
"Final assistant message only (not full transcript)",
|
|
6531
|
-
{ ref:
|
|
7935
|
+
{ ref: z2.string() },
|
|
6532
7936
|
async ({ ref }) => {
|
|
6533
7937
|
const result = orch.getTurnResult(ref);
|
|
6534
7938
|
return { content: [{ type: "text", text: JSON.stringify(result) }] };
|
|
@@ -6538,8 +7942,8 @@ async function startMcpServer() {
|
|
|
6538
7942
|
"stop_thread",
|
|
6539
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.",
|
|
6540
7944
|
{
|
|
6541
|
-
ref:
|
|
6542
|
-
force:
|
|
7945
|
+
ref: z2.string(),
|
|
7946
|
+
force: z2.boolean().optional()
|
|
6543
7947
|
},
|
|
6544
7948
|
async ({ ref, force }) => {
|
|
6545
7949
|
const t = orch.getThread(ref);
|
|
@@ -6568,8 +7972,8 @@ async function startMcpServer() {
|
|
|
6568
7972
|
);
|
|
6569
7973
|
server.tool(
|
|
6570
7974
|
"archive_thread",
|
|
6571
|
-
"Archive a thread (stops agent/dev, runs archive script, removes worktree when last chat tab).
|
|
6572
|
-
{ 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() },
|
|
6573
7977
|
async ({ ref }) => {
|
|
6574
7978
|
const t = orch.getThread(ref);
|
|
6575
7979
|
if (!t) {
|
|
@@ -6602,7 +8006,7 @@ async function startMcpServer() {
|
|
|
6602
8006
|
server.tool(
|
|
6603
8007
|
"restore_thread",
|
|
6604
8008
|
"Restore an archived thread (recreates worktree from branch when needed)",
|
|
6605
|
-
{ ref:
|
|
8009
|
+
{ ref: z2.string() },
|
|
6606
8010
|
async ({ ref }) => {
|
|
6607
8011
|
try {
|
|
6608
8012
|
const restored = await orch.restore(ref);
|
|
@@ -6628,8 +8032,8 @@ async function startMcpServer() {
|
|
|
6628
8032
|
"get_diff",
|
|
6629
8033
|
"Compact diff summary (capped hunks, paginated)",
|
|
6630
8034
|
{
|
|
6631
|
-
ref:
|
|
6632
|
-
maxFiles:
|
|
8035
|
+
ref: z2.string(),
|
|
8036
|
+
maxFiles: z2.number().optional()
|
|
6633
8037
|
},
|
|
6634
8038
|
async ({ ref, maxFiles }) => {
|
|
6635
8039
|
const summary = await orch.diffSummary(ref);
|
|
@@ -6649,7 +8053,7 @@ async function startMcpServer() {
|
|
|
6649
8053
|
server.tool(
|
|
6650
8054
|
"request_review",
|
|
6651
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.',
|
|
6652
|
-
{ ref:
|
|
8056
|
+
{ ref: z2.string().describe("Worktree thread id/ref to review") },
|
|
6653
8057
|
async ({ ref }) => {
|
|
6654
8058
|
try {
|
|
6655
8059
|
const tab = await orch.requestReview(ref);
|
|
@@ -6674,7 +8078,7 @@ async function startMcpServer() {
|
|
|
6674
8078
|
}
|
|
6675
8079
|
}
|
|
6676
8080
|
);
|
|
6677
|
-
const agentEnum =
|
|
8081
|
+
const agentEnum = z2.enum(["claude", "codex", "opencode", "brightsy", "cursor"]);
|
|
6678
8082
|
server.tool(
|
|
6679
8083
|
"list_models",
|
|
6680
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.",
|
|
@@ -6697,11 +8101,11 @@ async function startMcpServer() {
|
|
|
6697
8101
|
"fork_worktree",
|
|
6698
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.",
|
|
6699
8103
|
{
|
|
6700
|
-
ref:
|
|
6701
|
-
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)"),
|
|
6702
8106
|
agent: agentEnum.optional().describe("Agent for the forked chat (default: same as source)"),
|
|
6703
|
-
model:
|
|
6704
|
-
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()
|
|
6705
8109
|
},
|
|
6706
8110
|
async ({ ref, through_index, agent, model, title }) => {
|
|
6707
8111
|
try {
|
|
@@ -6740,13 +8144,13 @@ async function startMcpServer() {
|
|
|
6740
8144
|
);
|
|
6741
8145
|
server.tool(
|
|
6742
8146
|
"fork_chat",
|
|
6743
|
-
"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.",
|
|
6744
8148
|
{
|
|
6745
|
-
ref:
|
|
6746
|
-
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)"),
|
|
6747
8151
|
agent: agentEnum.optional().describe("Agent for the forked chat (default: same as source)"),
|
|
6748
|
-
model:
|
|
6749
|
-
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()
|
|
6750
8154
|
},
|
|
6751
8155
|
async ({ ref, through_index, agent, model, title }) => {
|
|
6752
8156
|
try {
|
|
@@ -6792,8 +8196,8 @@ async function startMcpServer() {
|
|
|
6792
8196
|
"run_dev_script",
|
|
6793
8197
|
"Start a .sideboard/.conductor run script for a thread (default script if name omitted); returns port",
|
|
6794
8198
|
{
|
|
6795
|
-
ref:
|
|
6796
|
-
name:
|
|
8199
|
+
ref: z2.string(),
|
|
8200
|
+
name: z2.string().optional()
|
|
6797
8201
|
},
|
|
6798
8202
|
async ({ ref, name }) => {
|
|
6799
8203
|
const result = await orch.startDev(ref, name);
|
|
@@ -6815,7 +8219,7 @@ async function startMcpServer() {
|
|
|
6815
8219
|
server.tool(
|
|
6816
8220
|
"list_run_scripts",
|
|
6817
8221
|
"List named run scripts available for a thread",
|
|
6818
|
-
{ ref:
|
|
8222
|
+
{ ref: z2.string() },
|
|
6819
8223
|
async ({ ref }) => {
|
|
6820
8224
|
const scripts = orch.listThreadRunScripts(ref);
|
|
6821
8225
|
const active = orch.getActiveRuns(ref);
|
|
@@ -6833,8 +8237,8 @@ async function startMcpServer() {
|
|
|
6833
8237
|
"stop_dev_script",
|
|
6834
8238
|
"Stop a running script for a thread (all scripts if name omitted)",
|
|
6835
8239
|
{
|
|
6836
|
-
ref:
|
|
6837
|
-
name:
|
|
8240
|
+
ref: z2.string(),
|
|
8241
|
+
name: z2.string().optional()
|
|
6838
8242
|
},
|
|
6839
8243
|
async ({ ref, name }) => {
|
|
6840
8244
|
orch.stopDev(ref, name);
|
|
@@ -6844,7 +8248,7 @@ async function startMcpServer() {
|
|
|
6844
8248
|
server.tool(
|
|
6845
8249
|
"run_setup",
|
|
6846
8250
|
"Re-run workspace setup (Sideboard/Conductor settings or .cursor/worktrees.json)",
|
|
6847
|
-
{ ref:
|
|
8251
|
+
{ ref: z2.string() },
|
|
6848
8252
|
async ({ ref }) => {
|
|
6849
8253
|
const result = await orch.runSetup(ref);
|
|
6850
8254
|
return {
|
|
@@ -6855,7 +8259,7 @@ async function startMcpServer() {
|
|
|
6855
8259
|
server.tool(
|
|
6856
8260
|
"add_workspace",
|
|
6857
8261
|
"Register a git repo as a Sideboard workspace",
|
|
6858
|
-
{ repoPath:
|
|
8262
|
+
{ repoPath: z2.string() },
|
|
6859
8263
|
async ({ repoPath }) => {
|
|
6860
8264
|
const ws = await orch.addWorkspace(repoPath);
|
|
6861
8265
|
return { content: [{ type: "text", text: JSON.stringify(ws) }] };
|
|
@@ -6864,7 +8268,7 @@ async function startMcpServer() {
|
|
|
6864
8268
|
server.tool(
|
|
6865
8269
|
"remove_workspace",
|
|
6866
8270
|
"Unregister a Sideboard workspace (does not archive threads)",
|
|
6867
|
-
{ repoPath:
|
|
8271
|
+
{ repoPath: z2.string() },
|
|
6868
8272
|
async ({ repoPath }) => {
|
|
6869
8273
|
orch.removeWorkspace(repoPath);
|
|
6870
8274
|
return { content: [{ type: "text", text: "ok" }] };
|
|
@@ -6874,12 +8278,12 @@ async function startMcpServer() {
|
|
|
6874
8278
|
"fanout",
|
|
6875
8279
|
"Best-of-n: create one thread per agent with the same prompt (parallel attempts)",
|
|
6876
8280
|
{
|
|
6877
|
-
prompt:
|
|
6878
|
-
agents:
|
|
6879
|
-
repoPath:
|
|
6880
|
-
sourceType:
|
|
6881
|
-
sourceRef:
|
|
6882
|
-
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()
|
|
6883
8287
|
},
|
|
6884
8288
|
async (args) => {
|
|
6885
8289
|
const threads = await orch.bestOfN(args);
|
|
@@ -6906,8 +8310,8 @@ async function startMcpServer() {
|
|
|
6906
8310
|
"list_branches",
|
|
6907
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).",
|
|
6908
8312
|
{
|
|
6909
|
-
repoPath:
|
|
6910
|
-
unmergedOnly:
|
|
8313
|
+
repoPath: z2.string(),
|
|
8314
|
+
unmergedOnly: z2.boolean().optional()
|
|
6911
8315
|
},
|
|
6912
8316
|
async ({ repoPath, unmergedOnly }) => {
|
|
6913
8317
|
const root = await resolveRepoRoot(repoPath);
|
|
@@ -6927,7 +8331,7 @@ async function startMcpServer() {
|
|
|
6927
8331
|
server.tool(
|
|
6928
8332
|
"list_prs",
|
|
6929
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.",
|
|
6930
|
-
{ repoPath:
|
|
8334
|
+
{ repoPath: z2.string() },
|
|
6931
8335
|
async ({ repoPath }) => {
|
|
6932
8336
|
const root = await resolveRepoRoot(repoPath);
|
|
6933
8337
|
const prs = await listPrs(root);
|
|
@@ -6946,7 +8350,7 @@ async function startMcpServer() {
|
|
|
6946
8350
|
server.tool(
|
|
6947
8351
|
"get_pr_stack",
|
|
6948
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.",
|
|
6949
|
-
{ ref:
|
|
8353
|
+
{ ref: z2.string() },
|
|
6950
8354
|
async ({ ref }) => {
|
|
6951
8355
|
const stack = await orch.getPrStack(ref);
|
|
6952
8356
|
return {
|
|
@@ -6958,8 +8362,8 @@ async function startMcpServer() {
|
|
|
6958
8362
|
"open_pr_stack_layers",
|
|
6959
8363
|
"Materialize one worktree+thread per stack layer (or a single 1-based layer). Pass a thread ref already on the stack.",
|
|
6960
8364
|
{
|
|
6961
|
-
ref:
|
|
6962
|
-
layer:
|
|
8365
|
+
ref: z2.string(),
|
|
8366
|
+
layer: z2.number().int().positive().optional()
|
|
6963
8367
|
},
|
|
6964
8368
|
async ({ ref, layer }) => {
|
|
6965
8369
|
const result = await orch.openPrStackLayers(ref, { layer });
|
|
@@ -6993,9 +8397,9 @@ async function startMcpServer() {
|
|
|
6993
8397
|
"add_stack_layer",
|
|
6994
8398
|
"Add a branch on top of the current stack (`gh stack add`) and open a worktree+thread for it.",
|
|
6995
8399
|
{
|
|
6996
|
-
ref:
|
|
6997
|
-
branchName:
|
|
6998
|
-
title:
|
|
8400
|
+
ref: z2.string(),
|
|
8401
|
+
branchName: z2.string(),
|
|
8402
|
+
title: z2.string().optional()
|
|
6999
8403
|
},
|
|
7000
8404
|
async ({ ref, branchName, title }) => {
|
|
7001
8405
|
const result = await orch.addStackLayer(ref, branchName, { title });
|
|
@@ -7024,11 +8428,11 @@ async function startMcpServer() {
|
|
|
7024
8428
|
"create_pr_stack",
|
|
7025
8429
|
"Create a new GitHub PR stack with one Sideboard worktree per layer (bottom\u2192top branch names). Requires `gh extension install github/gh-stack`.",
|
|
7026
8430
|
{
|
|
7027
|
-
repoPath:
|
|
7028
|
-
branches:
|
|
7029
|
-
agent:
|
|
7030
|
-
base:
|
|
7031
|
-
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()
|
|
7032
8436
|
},
|
|
7033
8437
|
async (args) => {
|
|
7034
8438
|
const result = await orch.createPrStack({
|
|
@@ -7066,7 +8470,7 @@ async function startMcpServer() {
|
|
|
7066
8470
|
server.tool(
|
|
7067
8471
|
"list_issues",
|
|
7068
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.",
|
|
7069
|
-
{ repoPath:
|
|
8473
|
+
{ repoPath: z2.string() },
|
|
7070
8474
|
async ({ repoPath }) => {
|
|
7071
8475
|
const root = await resolveRepoRoot(repoPath);
|
|
7072
8476
|
const result = await listIssues(root);
|
|
@@ -7077,8 +8481,8 @@ async function startMcpServer() {
|
|
|
7077
8481
|
"list_linear_issues",
|
|
7078
8482
|
"Deprecated: prefer list_issues. Lists assigned Linear issues via the chosen agent MCP connector",
|
|
7079
8483
|
{
|
|
7080
|
-
agent:
|
|
7081
|
-
repoPath:
|
|
8484
|
+
agent: z2.enum(["claude", "codex", "opencode"]),
|
|
8485
|
+
repoPath: z2.string()
|
|
7082
8486
|
},
|
|
7083
8487
|
async ({ agent, repoPath }) => {
|
|
7084
8488
|
const issues = await listLinearIssues(agent, repoPath);
|
|
@@ -7222,8 +8626,8 @@ var BrightsySideboardApi = class {
|
|
|
7222
8626
|
};
|
|
7223
8627
|
function taskMessageText(task) {
|
|
7224
8628
|
const parts = task.message?.parts ?? [];
|
|
7225
|
-
const
|
|
7226
|
-
return
|
|
8629
|
+
const text2 = parts.find((p) => p.kind === "text")?.text ?? "";
|
|
8630
|
+
return text2.trim();
|
|
7227
8631
|
}
|
|
7228
8632
|
|
|
7229
8633
|
// src/brightsy/cloud-connect.ts
|
|
@@ -7416,8 +8820,1286 @@ async function runCloudConnect(opts) {
|
|
|
7416
8820
|
});
|
|
7417
8821
|
}
|
|
7418
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;
|
|
9187
|
+
}
|
|
9188
|
+
if (frame.type === "disconnect") {
|
|
9189
|
+
log(`socket disconnect: ${frame.reason || "refresh"}`);
|
|
9190
|
+
try {
|
|
9191
|
+
ws.close();
|
|
9192
|
+
} catch {
|
|
9193
|
+
}
|
|
9194
|
+
return;
|
|
9195
|
+
}
|
|
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
|
+
});
|
|
9204
|
+
}
|
|
9205
|
+
|
|
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 {};
|
|
9220
|
+
}
|
|
9221
|
+
}
|
|
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
|
+
};
|
|
9270
|
+
}
|
|
9271
|
+
return null;
|
|
9272
|
+
} catch {
|
|
9273
|
+
return null;
|
|
9274
|
+
}
|
|
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 };
|
|
9305
|
+
}
|
|
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;
|
|
9320
|
+
}
|
|
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
|
+
);
|
|
9346
|
+
}
|
|
9347
|
+
let backoff = BACKOFF_START_MS2;
|
|
9348
|
+
while (!opts.signal?.aborted) {
|
|
9349
|
+
let ws = null;
|
|
9350
|
+
try {
|
|
9351
|
+
ws = new Ws(url);
|
|
9352
|
+
await connectSession2(ws, opts, log);
|
|
9353
|
+
backoff = BACKOFF_START_MS2;
|
|
9354
|
+
} catch (err) {
|
|
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
|
+
}
|
|
9364
|
+
}
|
|
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;
|
|
9371
|
+
}
|
|
9372
|
+
backoff = Math.min(backoff * 2, BACKOFF_MAX_MS2);
|
|
9373
|
+
}
|
|
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()}`)
|
|
9381
|
+
);
|
|
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
|
+
});
|
|
9411
|
+
}
|
|
9412
|
+
});
|
|
9413
|
+
ws.addEventListener("error", () => {
|
|
9414
|
+
finish(new Error("WebSocket error"));
|
|
9415
|
+
});
|
|
9416
|
+
ws.addEventListener("close", () => {
|
|
9417
|
+
finish();
|
|
9418
|
+
});
|
|
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
|
+
}
|
|
9467
|
+
});
|
|
9468
|
+
});
|
|
9469
|
+
}
|
|
9470
|
+
|
|
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(
|
|
9486
|
+
() => void 0,
|
|
9487
|
+
() => void 0
|
|
9488
|
+
);
|
|
9489
|
+
return run2;
|
|
9490
|
+
}
|
|
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}`);
|
|
9597
|
+
}
|
|
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);
|
|
9612
|
+
const log = opts.onLog ?? (() => void 0);
|
|
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}`);
|
|
9620
|
+
}
|
|
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();
|
|
9627
|
+
for (const ws of workspaces) {
|
|
9628
|
+
await getOrchestrator().reconcile(ws.path).catch(() => void 0);
|
|
9629
|
+
}
|
|
9630
|
+
const inventory = await enrichWorkspacesWithGithub(workspaces);
|
|
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)) {
|
|
9639
|
+
try {
|
|
9640
|
+
getOrchestrator().stop(fresh.id);
|
|
9641
|
+
log(`stop ${msg.kind} ${msg.ts} \u2192 coordinator ${fresh.id.slice(0, 8)}`);
|
|
9642
|
+
} catch (err) {
|
|
9643
|
+
const errMsg = err instanceof Error ? err.message : String(err);
|
|
9644
|
+
log(`stop ${msg.ts}: ${errMsg}`);
|
|
9645
|
+
}
|
|
9646
|
+
await postSlackReply(msg, SLACK_LISTEN_STOPPED_REPLY, opts);
|
|
9647
|
+
log(`replied stopped ${msg.ts}`);
|
|
9648
|
+
return;
|
|
9649
|
+
}
|
|
9650
|
+
if (fresh.status === "running" || fresh.status === "queued") {
|
|
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}`);
|
|
9654
|
+
return;
|
|
9655
|
+
}
|
|
9656
|
+
log(
|
|
9657
|
+
`run ${msg.kind} ${msg.ts} user ${userId} \u2192 coordinator ${fresh.id.slice(0, 8)} (${inventory.length} workspace${inventory.length === 1 ? "" : "s"})`
|
|
9658
|
+
);
|
|
9659
|
+
const prompt = formatSlackInboundPrompt(msg);
|
|
9660
|
+
setSlackReplyTarget({
|
|
9661
|
+
threadId: fresh.id,
|
|
9662
|
+
teamId: msg.teamId,
|
|
9663
|
+
channelId: msg.channelId,
|
|
9664
|
+
threadTs: slackReplyThreadTs(msg)
|
|
9665
|
+
});
|
|
9666
|
+
const orch = getOrchestrator();
|
|
9667
|
+
let reply;
|
|
9668
|
+
try {
|
|
9669
|
+
await orch.send(fresh.id, prompt);
|
|
9670
|
+
await orch.waitForTurn(fresh.id, 14 * 60 * 1e3);
|
|
9671
|
+
reply = orch.getTurnResult(fresh.id).text.trim();
|
|
9672
|
+
} catch (err) {
|
|
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}`);
|
|
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}`);
|
|
9699
|
+
}
|
|
9700
|
+
}
|
|
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 = {}) {
|
|
9717
|
+
const log = opts.onLog ?? console.log;
|
|
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) {
|
|
9742
|
+
throw new Error(
|
|
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."
|
|
9744
|
+
);
|
|
9745
|
+
}
|
|
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();
|
|
9767
|
+
}
|
|
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
|
+
}
|
|
9838
|
+
}
|
|
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
|
+
}
|
|
9853
|
+
try {
|
|
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;
|
|
9890
|
+
}
|
|
9891
|
+
} catch (err) {
|
|
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;
|
|
9951
|
+
}
|
|
9952
|
+
outbound = { ...message, text: rest };
|
|
9953
|
+
}
|
|
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
|
+
)
|
|
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");
|
|
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
|
+
};
|
|
10099
|
+
}
|
|
7419
10100
|
export {
|
|
7420
10101
|
ATTACHMENTS_DIR,
|
|
10102
|
+
BAKED_SLACK_RELAY_URL,
|
|
7421
10103
|
BRIGHTSY_MCP_ALLOWED_TOOLS,
|
|
7422
10104
|
BrightsySideboardApi,
|
|
7423
10105
|
CLAUDE_MODEL_CATALOG,
|
|
@@ -7436,6 +10118,9 @@ export {
|
|
|
7436
10118
|
LEGACY_ATTACHMENTS_DIR,
|
|
7437
10119
|
LEGACY_PLAN_FILE_REL,
|
|
7438
10120
|
LEGACY_REVIEW_REQUEST_PATH,
|
|
10121
|
+
LINEAR_OAUTH_PORT,
|
|
10122
|
+
LINEAR_OAUTH_REDIRECT,
|
|
10123
|
+
LINEAR_OAUTH_SCOPES,
|
|
7439
10124
|
MAX_ANTHROPIC_CACHE_CONTROL_BLOCKS,
|
|
7440
10125
|
ORCHESTRATOR_AGENT_KINDS,
|
|
7441
10126
|
Orchestrator,
|
|
@@ -7452,7 +10137,16 @@ export {
|
|
|
7452
10137
|
SIDEBOARD_FORCE_STOP,
|
|
7453
10138
|
SIDEBOARD_MCP_ALLOWED_TOOLS,
|
|
7454
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,
|
|
7455
10148
|
THINKING_EFFORTS,
|
|
10149
|
+
ackSlackInboundSeen,
|
|
7456
10150
|
addPrStackLayer,
|
|
7457
10151
|
addStackLayerFromThread,
|
|
7458
10152
|
addWorkspace,
|
|
@@ -7494,8 +10188,10 @@ export {
|
|
|
7494
10188
|
buildReviewRequestAttachment,
|
|
7495
10189
|
buildSessionSeed,
|
|
7496
10190
|
buildWorkspaceScriptEnv,
|
|
10191
|
+
caffeinateHoldPath,
|
|
7497
10192
|
caffeinateWhileCloudConnectEnabled,
|
|
7498
10193
|
caffeinateWhileRunningEnabled,
|
|
10194
|
+
caffeinateWhileSlackListenEnabled,
|
|
7499
10195
|
captureLoginEnv,
|
|
7500
10196
|
captureTurnBaseline,
|
|
7501
10197
|
checkoutPrStackLayer,
|
|
@@ -7513,6 +10209,7 @@ export {
|
|
|
7513
10209
|
conductorDbPath,
|
|
7514
10210
|
confirmLand,
|
|
7515
10211
|
connectBrightsyTeam,
|
|
10212
|
+
connectSlackToken,
|
|
7516
10213
|
coordinatorSystemPrompt,
|
|
7517
10214
|
coordinatorTurnReminder,
|
|
7518
10215
|
copyConfiguredFiles,
|
|
@@ -7521,6 +10218,7 @@ export {
|
|
|
7521
10218
|
createEmptyThread,
|
|
7522
10219
|
createExistingBranchWorktree,
|
|
7523
10220
|
createGlobalChat,
|
|
10221
|
+
createLinearPkce,
|
|
7524
10222
|
createOrUpdatePr,
|
|
7525
10223
|
createPrStack,
|
|
7526
10224
|
createThread,
|
|
@@ -7535,7 +10233,11 @@ export {
|
|
|
7535
10233
|
detectGhStack,
|
|
7536
10234
|
detectLocalMergeConflicts,
|
|
7537
10235
|
disconnectBrightsyTeam,
|
|
10236
|
+
disconnectLinear,
|
|
10237
|
+
disconnectLinearConnection,
|
|
10238
|
+
disconnectSlackWorkspace,
|
|
7538
10239
|
discoverSkills,
|
|
10240
|
+
dismissSlackReplyBadge,
|
|
7539
10241
|
dropCachedPrefixOnResume,
|
|
7540
10242
|
encodeBrightsyTarget,
|
|
7541
10243
|
enrichPathWithNpmGlobalBin,
|
|
@@ -7545,6 +10247,8 @@ export {
|
|
|
7545
10247
|
ensureGhPreferOrigin,
|
|
7546
10248
|
ensureGlobalCoordinatorCwd,
|
|
7547
10249
|
ensureReviewRequestFile,
|
|
10250
|
+
ensureSlackCoordinator,
|
|
10251
|
+
ensureSlackDeviceIdentity,
|
|
7548
10252
|
ensureWorkspace,
|
|
7549
10253
|
estimateMessageChars,
|
|
7550
10254
|
estimateThreadChars,
|
|
@@ -7573,6 +10277,8 @@ export {
|
|
|
7573
10277
|
formatPlanQuestionsForChat,
|
|
7574
10278
|
formatRateLimitResetHint,
|
|
7575
10279
|
formatRenameBranchDirective,
|
|
10280
|
+
formatSlackInboundPrompt,
|
|
10281
|
+
formatSlackSignedReply,
|
|
7576
10282
|
formatTranscriptMarkdown,
|
|
7577
10283
|
formatWorkspaceInventory,
|
|
7578
10284
|
formatWorktreeDirective,
|
|
@@ -7580,6 +10286,7 @@ export {
|
|
|
7580
10286
|
getAdapter,
|
|
7581
10287
|
getAgentSetupInfo,
|
|
7582
10288
|
getBrightsySession,
|
|
10289
|
+
getCaffeinateHold,
|
|
7583
10290
|
getDefaultAgent,
|
|
7584
10291
|
getDefaultEffort,
|
|
7585
10292
|
getDefaultFast,
|
|
@@ -7590,6 +10297,7 @@ export {
|
|
|
7590
10297
|
getGitHubStatus,
|
|
7591
10298
|
getIssueSource,
|
|
7592
10299
|
getLinearApiKey,
|
|
10300
|
+
getLinearAuthToken,
|
|
7593
10301
|
getOrchestrator,
|
|
7594
10302
|
getPr,
|
|
7595
10303
|
getPrChecks,
|
|
@@ -7599,12 +10307,16 @@ export {
|
|
|
7599
10307
|
getRepoSetupInfo,
|
|
7600
10308
|
getRunMode,
|
|
7601
10309
|
getRunScript,
|
|
10310
|
+
getSlackWorkspace,
|
|
7602
10311
|
gh,
|
|
7603
10312
|
ghHeadRef,
|
|
7604
10313
|
ghRepoSelectArgs,
|
|
7605
10314
|
git,
|
|
7606
10315
|
globalAgentCwd,
|
|
10316
|
+
handleSlackInbound,
|
|
7607
10317
|
harnessEnvKey,
|
|
10318
|
+
hasBakedLinearOAuth,
|
|
10319
|
+
hasBakedSlackOAuth,
|
|
7608
10320
|
hasConductorHook,
|
|
7609
10321
|
hasCursorWorktreeSetup,
|
|
7610
10322
|
hasRepoHook,
|
|
@@ -7629,6 +10341,7 @@ export {
|
|
|
7629
10341
|
isGlobalThread,
|
|
7630
10342
|
isImageFilePath,
|
|
7631
10343
|
isInPrStack,
|
|
10344
|
+
isInboundForThisDesktop,
|
|
7632
10345
|
isLinearConnected,
|
|
7633
10346
|
isOrchestratorCapableAgent,
|
|
7634
10347
|
isOrchestratorThread,
|
|
@@ -7637,8 +10350,12 @@ export {
|
|
|
7637
10350
|
isPresentPlanToolName,
|
|
7638
10351
|
isSessionQuotaLimit,
|
|
7639
10352
|
isSideboardScratchPath,
|
|
10353
|
+
isSlackCoordinatorThread,
|
|
7640
10354
|
isThinkingEffort,
|
|
7641
10355
|
isWorkspaceScratchPath,
|
|
10356
|
+
linearAuthorizationHeader,
|
|
10357
|
+
linearOAuthAuthorizeUrl,
|
|
10358
|
+
linearOAuthCredentials,
|
|
7642
10359
|
listAgentSetupInfo,
|
|
7643
10360
|
listBranchCommits,
|
|
7644
10361
|
listBranches,
|
|
@@ -7657,6 +10374,8 @@ export {
|
|
|
7657
10374
|
listOpencodeModels,
|
|
7658
10375
|
listPrs,
|
|
7659
10376
|
listRunScripts,
|
|
10377
|
+
listSlackReplyBadges,
|
|
10378
|
+
listSlackWorkspaces,
|
|
7660
10379
|
listThreads,
|
|
7661
10380
|
listWorkspaces,
|
|
7662
10381
|
listWorktreeFiles,
|
|
@@ -7700,9 +10419,13 @@ export {
|
|
|
7700
10419
|
parseMcpList,
|
|
7701
10420
|
parsePlanQuestionsInput,
|
|
7702
10421
|
parseSessionQuotaResetAt,
|
|
10422
|
+
parseSlackRelayClientMessage,
|
|
10423
|
+
parseSlackRelayServerMessage,
|
|
7703
10424
|
partsToAssistantText,
|
|
7704
10425
|
pastedTextStats,
|
|
10426
|
+
permalinkForSlackReplyBadge,
|
|
7705
10427
|
permissionMode,
|
|
10428
|
+
persistVaultKeyInKeychain,
|
|
7706
10429
|
planFileAbs,
|
|
7707
10430
|
posixShellSingleQuote,
|
|
7708
10431
|
prepareTerminalCommand,
|
|
@@ -7710,13 +10433,16 @@ export {
|
|
|
7710
10433
|
promptMentionsBrightsy,
|
|
7711
10434
|
pushBranch,
|
|
7712
10435
|
readExistingReviewRequestFile,
|
|
10436
|
+
readKeychainVaultKey,
|
|
7713
10437
|
readPlanFile,
|
|
7714
10438
|
readSkillBody,
|
|
7715
10439
|
readThread,
|
|
7716
10440
|
readWorktreeFile,
|
|
7717
10441
|
readWorktreeFileForUpload,
|
|
7718
10442
|
readWorktreeInclude,
|
|
10443
|
+
recordSlackOutboundWatch,
|
|
7719
10444
|
refreshGitHubAuth,
|
|
10445
|
+
refreshSlackReplyBadges,
|
|
7720
10446
|
removeWorkspace,
|
|
7721
10447
|
removeWorktree,
|
|
7722
10448
|
repoSlug,
|
|
@@ -7741,18 +10467,26 @@ export {
|
|
|
7741
10467
|
resolveQuotaFallbackAgent,
|
|
7742
10468
|
resolveRepoRoot,
|
|
7743
10469
|
resolveReviewGuidelines,
|
|
10470
|
+
resolveSlackListenMode,
|
|
7744
10471
|
resolveThreadDefaults,
|
|
7745
10472
|
resolveThreadEffort,
|
|
10473
|
+
resolveVaultKey,
|
|
7746
10474
|
resolveWorktreeStartPoint,
|
|
7747
10475
|
run,
|
|
7748
10476
|
runArchiveScript,
|
|
7749
10477
|
runCloudConnect,
|
|
7750
10478
|
runCursorWorktreeSetup,
|
|
7751
10479
|
runSetupScript,
|
|
10480
|
+
runSlackListen,
|
|
10481
|
+
runSlackRelayClient,
|
|
7752
10482
|
sameWorktreePath,
|
|
7753
10483
|
sanitizeMcpServerName,
|
|
7754
10484
|
saveAppSettings,
|
|
10485
|
+
saveLinearOAuth,
|
|
10486
|
+
secureFileUnlocksWith,
|
|
10487
|
+
setCaffeinateHold,
|
|
7755
10488
|
setStatus,
|
|
10489
|
+
setVaultMasterKey,
|
|
7756
10490
|
settingsSourceLabel,
|
|
7757
10491
|
shouldAttachPastedText,
|
|
7758
10492
|
shouldCompactContext,
|
|
@@ -7763,6 +10497,12 @@ export {
|
|
|
7763
10497
|
sideboardMcpProfile,
|
|
7764
10498
|
sideboardReposDir,
|
|
7765
10499
|
sideboardWorkspacesDir,
|
|
10500
|
+
slackAppLevelToken,
|
|
10501
|
+
slackArchiveUrl,
|
|
10502
|
+
slackCoordinatorSourceRef,
|
|
10503
|
+
slackListenEnabled,
|
|
10504
|
+
slackOAuthCredentials,
|
|
10505
|
+
slackRelayUrl,
|
|
7766
10506
|
slugify,
|
|
7767
10507
|
spawnAgentTurn,
|
|
7768
10508
|
splitForCompaction,
|
|
@@ -7772,8 +10512,11 @@ export {
|
|
|
7772
10512
|
stageAbsolutePathsAsAttachments,
|
|
7773
10513
|
stageBuffersAsAttachments,
|
|
7774
10514
|
startDevServer,
|
|
10515
|
+
startLinearOAuth,
|
|
7775
10516
|
startMcpServer,
|
|
7776
10517
|
startOrchestration,
|
|
10518
|
+
startSlackOAuth,
|
|
10519
|
+
startSlackRelayServer,
|
|
7777
10520
|
stripBrightsyNdjsonNoise,
|
|
7778
10521
|
submitPrStack,
|
|
7779
10522
|
suggestSlug,
|
|
@@ -7791,6 +10534,7 @@ export {
|
|
|
7791
10534
|
threadRequestsBrightsyMcp,
|
|
7792
10535
|
threadsDir,
|
|
7793
10536
|
threadsSharingWorktree,
|
|
10537
|
+
toPublicAppSettings,
|
|
7794
10538
|
toolDescription,
|
|
7795
10539
|
toolDetail,
|
|
7796
10540
|
toolFilePath,
|