@sideboard-ai/core 0.1.273 → 0.1.275

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.
Files changed (48) hide show
  1. package/dist/{abletime-4ZSDI4YP.js → abletime-VCVPSLSU.js} +4 -4
  2. package/dist/{abletime-JNQAHLHH.js → abletime-VRATUMDO.js} +4 -4
  3. package/dist/{abletime-oauth-4RXZH2KU.js → abletime-oauth-4C4G2J3R.js} +3 -3
  4. package/dist/{abletime-oauth-DCEIINYT.js → abletime-oauth-4K7MICAD.js} +3 -3
  5. package/dist/{abletime-rest-KVBJFVFD.js → abletime-rest-6N63ZVQ3.js} +3 -3
  6. package/dist/{abletime-rest-VN6PZ6P2.js → abletime-rest-PIBCNHF3.js} +3 -3
  7. package/dist/{agents-LAFFXBBZ.js → agents-2FOWVJCN.js} +4 -4
  8. package/dist/{agents-ZCO37SKG.js → agents-JBVJRDH3.js} +4 -4
  9. package/dist/{app-settings-RAC4PSSP.js → app-settings-EQQZQNYD.js} +1 -1
  10. package/dist/{app-settings-L5HGYXWJ.js → app-settings-NI4X5HW3.js} +1 -1
  11. package/dist/{chunk-UXI52MLJ.js → chunk-2J34G3WJ.js} +3 -3
  12. package/dist/{chunk-PTNGMA5C.js → chunk-A2G3ZBJS.js} +2 -2
  13. package/dist/{chunk-T47ARZ5Q.js → chunk-FELFTNH2.js} +25 -24
  14. package/dist/{chunk-OUBQH5GR.js → chunk-HOMZKYHF.js} +31 -29
  15. package/dist/{chunk-GONPMH36.js → chunk-IJDQKC5A.js} +5 -5
  16. package/dist/{chunk-USBZ7PH4.js → chunk-IPPY3FG5.js} +1 -1
  17. package/dist/{chunk-NWE3NOWQ.js → chunk-K7MWJLR7.js} +1 -1
  18. package/dist/{chunk-HO2T6BC3.js → chunk-MGS5T2A3.js} +31 -29
  19. package/dist/{chunk-ADWNK2JH.js → chunk-NIB2IZ6J.js} +5 -5
  20. package/dist/{chunk-AGXZAMOF.js → chunk-NQ7JMVSU.js} +2 -2
  21. package/dist/{chunk-JLDJGGCL.js → chunk-NT7RIWRR.js} +5 -5
  22. package/dist/{chunk-L6CYOON4.js → chunk-PZURG3XN.js} +5 -5
  23. package/dist/{chunk-LAIBYKZS.js → chunk-QINJKNLR.js} +3 -3
  24. package/dist/{chunk-2QUILREI.js → chunk-QTZO4LPU.js} +2 -2
  25. package/dist/{chunk-KZ2CDZBZ.js → chunk-R7HLXD4I.js} +3 -3
  26. package/dist/{chunk-N3LASDJM.js → chunk-SQOAFR2U.js} +26 -25
  27. package/dist/{chunk-OIENQEGG.js → chunk-WRIMAEF2.js} +67 -122
  28. package/dist/{chunk-DBFAFI5A.js → chunk-XKZAVGTT.js} +2 -2
  29. package/dist/{chunk-PAH7KDL2.js → chunk-YNTXWFXT.js} +67 -122
  30. package/dist/{conductor-3AQM67VK.js → conductor-GBCE37K4.js} +4 -4
  31. package/dist/{conductor-FLWACOVC.js → conductor-YYQKZAND.js} +4 -4
  32. package/dist/{coordinator-prompt-4KVA6XUX.js → coordinator-prompt-B3GLVXPP.js} +3 -3
  33. package/dist/{coordinator-prompt-MVUHFNSC.js → coordinator-prompt-SFZCZ3HR.js} +3 -3
  34. package/dist/{global-workspace-FR7QTLPK.js → global-workspace-7XEXTIHL.js} +3 -3
  35. package/dist/{global-workspace-34Z2IWYQ.js → global-workspace-N2YYJKA5.js} +3 -3
  36. package/dist/index.cjs +1700 -186
  37. package/dist/index.d.cts +99 -20
  38. package/dist/index.d.ts +99 -20
  39. package/dist/index.js +1583 -43
  40. package/dist/mcp/run-stdio.cjs +95 -141
  41. package/dist/mcp/run-stdio.js +11 -11
  42. package/dist/{orchestrator-YKSDWR6C.js → orchestrator-6QJLM6Y4.js} +6 -6
  43. package/dist/{orchestrator-K42IBMBE.js → orchestrator-QMKVXVUR.js} +6 -6
  44. package/dist/{workspaces-5GERYHDJ.js → workspaces-BJCHMGVG.js} +3 -3
  45. package/dist/{workspaces-2JBDUMM6.js → workspaces-PBB6HY2D.js} +3 -3
  46. package/dist/{worktree-ACW23KJ5.js → worktree-L57T3KH7.js} +2 -2
  47. package/dist/{worktree-C7RPVHZ7.js → worktree-Q6UJHG25.js} +2 -2
  48. package/package.json +1 -1
package/dist/index.cjs CHANGED
@@ -1081,6 +1081,9 @@ function normalizeVault(raw) {
1081
1081
  if (typeof raw.remoteHostSecret === "string" && raw.remoteHostSecret.trim()) {
1082
1082
  out.remoteHostSecret = raw.remoteHostSecret.trim();
1083
1083
  }
1084
+ if (typeof raw.remoteAccountToken === "string" && raw.remoteAccountToken.trim()) {
1085
+ out.remoteAccountToken = raw.remoteAccountToken.trim();
1086
+ }
1084
1087
  if (typeof raw.githubPat === "string" && raw.githubPat.trim()) {
1085
1088
  out.githubPat = raw.githubPat.trim();
1086
1089
  }
@@ -1777,7 +1780,8 @@ function emptyPublicIntegrations() {
1777
1780
  hasVercelToken: false,
1778
1781
  hasSupabaseToken: false,
1779
1782
  hasPosthogToken: false,
1780
- hasSentryToken: false
1783
+ hasSentryToken: false,
1784
+ hasRemoteAccount: false
1781
1785
  };
1782
1786
  }
1783
1787
  function hasLinearOAuth(integrations) {
@@ -1794,29 +1798,7 @@ function toPublicAppSettings(settings) {
1794
1798
  if (key.trim()) environment[key] = "";
1795
1799
  }
1796
1800
  const integrations = { ...settings.integrations };
1797
- const slackClientSecret = integrations.slackClientSecret;
1798
- const slackAppToken = integrations.slackAppToken;
1799
- const githubPat = integrations.githubPat;
1800
- const abletimeAccessToken = integrations.abletimeAccessToken;
1801
- const abletimeRefreshToken = integrations.abletimeRefreshToken;
1802
- const vercelToken = integrations.vercelToken;
1803
- const supabaseAccessToken = integrations.supabaseAccessToken;
1804
- const posthogPersonalApiKey = integrations.posthogPersonalApiKey;
1805
- const sentryAuthToken = integrations.sentryAuthToken;
1806
- delete integrations.linearApiKey;
1807
- delete integrations.linearAccessToken;
1808
- delete integrations.linearRefreshToken;
1809
- delete integrations.linearClientSecret;
1810
- delete integrations.slackClientSecret;
1811
- delete integrations.slackAppToken;
1812
- delete integrations.remoteHostSecret;
1813
- delete integrations.githubPat;
1814
- delete integrations.abletimeAccessToken;
1815
- delete integrations.abletimeRefreshToken;
1816
- delete integrations.vercelToken;
1817
- delete integrations.supabaseAccessToken;
1818
- delete integrations.posthogPersonalApiKey;
1819
- delete integrations.sentryAuthToken;
1801
+ for (const key of INTEGRATION_VAULT_KEYS) delete integrations[key];
1820
1802
  return {
1821
1803
  ...settings,
1822
1804
  environment,
@@ -1824,15 +1806,18 @@ function toPublicAppSettings(settings) {
1824
1806
  ...integrations,
1825
1807
  hasLinearApiKey: hasLinearCredentials(settings.integrations),
1826
1808
  hasLinearOAuth: hasLinearOAuth(settings.integrations),
1827
- hasSlackClientSecret: Boolean(slackClientSecret?.trim()),
1828
- hasSlackAppToken: Boolean(slackAppToken?.trim()),
1829
- hasGithubPat: Boolean(githubPat?.trim()),
1830
- hasAbleTimeToken: Boolean(abletimeAccessToken?.trim() || abletimeRefreshToken?.trim()),
1831
- hasAbleTimeOAuth: Boolean(abletimeRefreshToken?.trim()),
1832
- hasVercelToken: Boolean(vercelToken?.trim()),
1833
- hasSupabaseToken: Boolean(supabaseAccessToken?.trim()),
1834
- hasPosthogToken: Boolean(posthogPersonalApiKey?.trim()),
1835
- hasSentryToken: Boolean(sentryAuthToken?.trim())
1809
+ hasSlackClientSecret: Boolean(settings.integrations.slackClientSecret?.trim()),
1810
+ hasSlackAppToken: Boolean(settings.integrations.slackAppToken?.trim()),
1811
+ hasGithubPat: Boolean(settings.integrations.githubPat?.trim()),
1812
+ hasAbleTimeToken: Boolean(
1813
+ settings.integrations.abletimeAccessToken?.trim() || settings.integrations.abletimeRefreshToken?.trim()
1814
+ ),
1815
+ hasAbleTimeOAuth: Boolean(settings.integrations.abletimeRefreshToken?.trim()),
1816
+ hasVercelToken: Boolean(settings.integrations.vercelToken?.trim()),
1817
+ hasSupabaseToken: Boolean(settings.integrations.supabaseAccessToken?.trim()),
1818
+ hasPosthogToken: Boolean(settings.integrations.posthogPersonalApiKey?.trim()),
1819
+ hasSentryToken: Boolean(settings.integrations.sentryAuthToken?.trim()),
1820
+ hasRemoteAccount: Boolean(settings.integrations.remoteAccountToken?.trim())
1836
1821
  }
1837
1822
  };
1838
1823
  }
@@ -1959,6 +1944,14 @@ function normalizeIntegrations(raw) {
1959
1944
  const secret = source.remoteHostSecret.trim();
1960
1945
  if (secret) out.remoteHostSecret = secret;
1961
1946
  }
1947
+ if (typeof source.remoteAccountToken === "string") {
1948
+ const token = source.remoteAccountToken.trim();
1949
+ if (token) out.remoteAccountToken = token;
1950
+ }
1951
+ if (typeof source.remoteAccountEmail === "string") {
1952
+ const email = source.remoteAccountEmail.trim().toLowerCase();
1953
+ if (email) out.remoteAccountEmail = email;
1954
+ }
1962
1955
  if (typeof source.githubGitAuthMode === "string" && GIT_AUTH_MODES.has(source.githubGitAuthMode)) {
1963
1956
  out.githubGitAuthMode = source.githubGitAuthMode;
1964
1957
  }
@@ -2302,110 +2295,31 @@ function readSettingsFile() {
2302
2295
  }
2303
2296
  }
2304
2297
  function diskHoldsSecrets(disk) {
2305
- return Boolean(
2306
- disk.integrations.linearApiKey || disk.integrations.linearAccessToken || disk.integrations.linearRefreshToken || disk.integrations.linearClientSecret || disk.integrations.slackClientSecret || disk.integrations.slackAppToken || disk.integrations.remoteHostSecret || disk.integrations.githubPat || disk.integrations.abletimeAccessToken || disk.integrations.abletimeRefreshToken || disk.integrations.vercelToken || disk.integrations.supabaseAccessToken || disk.integrations.posthogPersonalApiKey || disk.integrations.sentryAuthToken || Object.keys(disk.environment).length > 0
2307
- );
2298
+ return INTEGRATION_VAULT_KEYS.some((key) => Boolean(disk.integrations[key])) || Object.keys(disk.environment).length > 0;
2308
2299
  }
2309
2300
  function mergeVault(disk) {
2310
2301
  const vault = loadSecretVault();
2311
2302
  const environment = { ...vault.environment ?? {}, ...disk.environment };
2312
2303
  const integrations = { ...disk.integrations };
2313
- if (vault.linearApiKey && !integrations.linearApiKey) {
2314
- integrations.linearApiKey = vault.linearApiKey;
2315
- }
2316
- if (vault.linearAccessToken && !integrations.linearAccessToken) {
2317
- integrations.linearAccessToken = vault.linearAccessToken;
2318
- }
2319
- if (vault.linearRefreshToken && !integrations.linearRefreshToken) {
2320
- integrations.linearRefreshToken = vault.linearRefreshToken;
2321
- }
2322
- if (vault.linearClientSecret && !integrations.linearClientSecret) {
2323
- integrations.linearClientSecret = vault.linearClientSecret;
2324
- }
2325
- if (vault.slackClientSecret && !integrations.slackClientSecret) {
2326
- integrations.slackClientSecret = vault.slackClientSecret;
2327
- }
2328
- if (vault.slackAppToken && !integrations.slackAppToken) {
2329
- integrations.slackAppToken = vault.slackAppToken;
2330
- }
2331
- if (vault.remoteHostSecret && !integrations.remoteHostSecret) {
2332
- integrations.remoteHostSecret = vault.remoteHostSecret;
2333
- }
2334
- if (vault.githubPat && !integrations.githubPat) {
2335
- integrations.githubPat = vault.githubPat;
2336
- }
2337
- if (vault.abletimeAccessToken && !integrations.abletimeAccessToken) {
2338
- integrations.abletimeAccessToken = vault.abletimeAccessToken;
2339
- }
2340
- if (vault.abletimeRefreshToken && !integrations.abletimeRefreshToken) {
2341
- integrations.abletimeRefreshToken = vault.abletimeRefreshToken;
2342
- }
2343
- if (vault.vercelToken && !integrations.vercelToken) {
2344
- integrations.vercelToken = vault.vercelToken;
2345
- }
2346
- if (vault.supabaseAccessToken && !integrations.supabaseAccessToken) {
2347
- integrations.supabaseAccessToken = vault.supabaseAccessToken;
2348
- }
2349
- if (vault.posthogPersonalApiKey && !integrations.posthogPersonalApiKey) {
2350
- integrations.posthogPersonalApiKey = vault.posthogPersonalApiKey;
2351
- }
2352
- if (vault.sentryAuthToken && !integrations.sentryAuthToken) {
2353
- integrations.sentryAuthToken = vault.sentryAuthToken;
2304
+ for (const key of INTEGRATION_VAULT_KEYS) {
2305
+ const stored = vault[key];
2306
+ if (stored && !integrations[key]) integrations[key] = stored;
2354
2307
  }
2355
2308
  return { ...disk, environment, integrations };
2356
2309
  }
2357
2310
  function persistSplit(settings) {
2358
2311
  const integrations = { ...settings.integrations };
2359
- const linearApiKey = integrations.linearApiKey;
2360
- const linearAccessToken = integrations.linearAccessToken;
2361
- const linearRefreshToken = integrations.linearRefreshToken;
2362
- const linearClientSecret = integrations.linearClientSecret;
2363
- const slackClientSecret = integrations.slackClientSecret;
2364
- const slackAppToken = integrations.slackAppToken;
2365
- const remoteHostSecret = integrations.remoteHostSecret;
2366
- const githubPat = integrations.githubPat;
2367
- const abletimeAccessToken = integrations.abletimeAccessToken;
2368
- const abletimeRefreshToken = integrations.abletimeRefreshToken;
2369
- const vercelToken = integrations.vercelToken;
2370
- const supabaseAccessToken = integrations.supabaseAccessToken;
2371
- const posthogPersonalApiKey = integrations.posthogPersonalApiKey;
2372
- const sentryAuthToken = integrations.sentryAuthToken;
2373
- delete integrations.linearApiKey;
2374
- delete integrations.linearAccessToken;
2375
- delete integrations.linearRefreshToken;
2376
- delete integrations.linearClientSecret;
2377
- delete integrations.slackClientSecret;
2378
- delete integrations.slackAppToken;
2379
- delete integrations.remoteHostSecret;
2380
- delete integrations.githubPat;
2381
- delete integrations.abletimeAccessToken;
2382
- delete integrations.abletimeRefreshToken;
2383
- delete integrations.vercelToken;
2384
- delete integrations.supabaseAccessToken;
2385
- delete integrations.posthogPersonalApiKey;
2386
- delete integrations.sentryAuthToken;
2312
+ const vault = { environment: settings.environment };
2313
+ for (const key of INTEGRATION_VAULT_KEYS) {
2314
+ vault[key] = integrations[key];
2315
+ delete integrations[key];
2316
+ }
2387
2317
  writePrivateFile(
2388
2318
  appSettingsPath(),
2389
2319
  `${JSON.stringify({ ...settings, environment: {}, integrations }, null, 2)}
2390
2320
  `
2391
2321
  );
2392
- saveSecretVault({
2393
- linearApiKey,
2394
- linearAccessToken,
2395
- linearRefreshToken,
2396
- linearClientSecret,
2397
- slackClientSecret,
2398
- slackAppToken,
2399
- remoteHostSecret,
2400
- githubPat,
2401
- abletimeAccessToken,
2402
- abletimeRefreshToken,
2403
- vercelToken,
2404
- supabaseAccessToken,
2405
- posthogPersonalApiKey,
2406
- sentryAuthToken,
2407
- environment: settings.environment
2408
- });
2322
+ saveSecretVault(vault);
2409
2323
  }
2410
2324
  function loadAppSettings() {
2411
2325
  const disk = readSettingsFile();
@@ -2616,6 +2530,20 @@ function updateIntegrationsSettings(patch) {
2616
2530
  integrations.remoteHostSecret = patch.remoteHostSecret.trim();
2617
2531
  }
2618
2532
  }
2533
+ if ("remoteAccountToken" in patch) {
2534
+ if (patch.remoteAccountToken == null || patch.remoteAccountToken.trim() === "") {
2535
+ delete integrations.remoteAccountToken;
2536
+ } else {
2537
+ integrations.remoteAccountToken = patch.remoteAccountToken.trim();
2538
+ }
2539
+ }
2540
+ if ("remoteAccountEmail" in patch) {
2541
+ if (patch.remoteAccountEmail == null || patch.remoteAccountEmail.trim() === "") {
2542
+ delete integrations.remoteAccountEmail;
2543
+ } else {
2544
+ integrations.remoteAccountEmail = patch.remoteAccountEmail.trim().toLowerCase();
2545
+ }
2546
+ }
2619
2547
  if ("githubGitAuthMode" in patch) {
2620
2548
  if (patch.githubGitAuthMode == null) {
2621
2549
  delete integrations.githubGitAuthMode;
@@ -3354,7 +3282,7 @@ function childEnvWithAppSettings(extra) {
3354
3282
  function harnessEnvKey(harness) {
3355
3283
  return HARNESS_ENV_KEYS[harness];
3356
3284
  }
3357
- var import_node_crypto2, import_node_fs9, import_node_os4, import_node_path10, HARNESS_ENV_KEYS, DEFAULT_AGENTS, GITHUB_GIT_AUTH_MODES, FOLLOW_UP_BEHAVIORS, AGENT_DONE_SOUNDS, CHAT_TEXT_SCALES, CHAT_TEXT_SCALE_DEFAULT, CLOUD_CONNECT_AGENTS, ISSUE_SOURCES, GIT_AUTH_MODES, REVIEW_LABEL_MAX, EMPTY_SETTINGS, CONFIRM_ACCOUNT, CONFIRM_PROJECT, DEFAULT_CLI_BIN;
3285
+ var import_node_crypto2, import_node_fs9, import_node_os4, import_node_path10, HARNESS_ENV_KEYS, DEFAULT_AGENTS, GITHUB_GIT_AUTH_MODES, FOLLOW_UP_BEHAVIORS, AGENT_DONE_SOUNDS, CHAT_TEXT_SCALES, CHAT_TEXT_SCALE_DEFAULT, INTEGRATION_VAULT_KEYS, CLOUD_CONNECT_AGENTS, ISSUE_SOURCES, GIT_AUTH_MODES, REVIEW_LABEL_MAX, EMPTY_SETTINGS, CONFIRM_ACCOUNT, CONFIRM_PROJECT, DEFAULT_CLI_BIN;
3358
3286
  var init_app_settings = __esm({
3359
3287
  "src/store/app-settings.ts"() {
3360
3288
  "use strict";
@@ -3407,6 +3335,23 @@ var init_app_settings = __esm({
3407
3335
  ];
3408
3336
  CHAT_TEXT_SCALES = [1.2, 1.4];
3409
3337
  CHAT_TEXT_SCALE_DEFAULT = 1.2;
3338
+ INTEGRATION_VAULT_KEYS = [
3339
+ "linearApiKey",
3340
+ "linearAccessToken",
3341
+ "linearRefreshToken",
3342
+ "linearClientSecret",
3343
+ "slackClientSecret",
3344
+ "slackAppToken",
3345
+ "remoteHostSecret",
3346
+ "remoteAccountToken",
3347
+ "githubPat",
3348
+ "abletimeAccessToken",
3349
+ "abletimeRefreshToken",
3350
+ "vercelToken",
3351
+ "supabaseAccessToken",
3352
+ "posthogPersonalApiKey",
3353
+ "sentryAuthToken"
3354
+ ];
3410
3355
  CLOUD_CONNECT_AGENTS = /* @__PURE__ */ new Set([
3411
3356
  "claude",
3412
3357
  "codex",
@@ -18515,7 +18460,7 @@ function lastRequestOccupancy(thread) {
18515
18460
  function shouldResetSessionForOccupancy(thread, occupancyTokens = SESSION_RESET_OCCUPANCY_TOKENS) {
18516
18461
  return lastRequestOccupancy(thread) >= occupancyTokens;
18517
18462
  }
18518
- async function maybeCompactContext(thread, thresholds = {}, summarize = summarizeConversation) {
18463
+ async function maybeCompactContext(thread, thresholds = {}, summarize2 = summarizeConversation) {
18519
18464
  if (!shouldCompactContext(thread.messages, thresholds)) {
18520
18465
  return { didCompact: false, thread };
18521
18466
  }
@@ -18524,7 +18469,7 @@ async function maybeCompactContext(thread, thresholds = {}, summarize = summariz
18524
18469
  return { didCompact: false, thread };
18525
18470
  }
18526
18471
  const transcript = formatMessagesAsTranscript(older, { tools: "summary" });
18527
- const { summary, method } = await summarize(transcript, {
18472
+ const { summary, method } = await summarize2(transcript, {
18528
18473
  cwd: thread.worktreePath
18529
18474
  });
18530
18475
  let messages = applyCompaction(thread.messages, summary, thresholds);
@@ -19213,17 +19158,7 @@ var init_worktree_run_ports = __esm({
19213
19158
  }
19214
19159
  });
19215
19160
 
19216
- // src/mcp/wait-for-job.ts
19217
- function mcpWaitForJobTimeoutMs(requested) {
19218
- return mcpWaitForTurnTimeoutMs(requested);
19219
- }
19220
- function sanitizeDetachedJobId(id) {
19221
- const s = id.trim();
19222
- if (!JOB_ID_RE.test(s)) {
19223
- throw new Error(`detached-job id must be 1\u201364 chars [A-Za-z0-9._-], got ${JSON.stringify(id)}`);
19224
- }
19225
- return s;
19226
- }
19161
+ // src/mcp/job-process.ts
19227
19162
  function jobAlive(pid) {
19228
19163
  if (!Number.isInteger(pid) || pid <= 0) return false;
19229
19164
  try {
@@ -19247,6 +19182,23 @@ function jobTreeAlive(pid) {
19247
19182
  if (pid == null) return false;
19248
19183
  return jobAlive(pid) || processGroupAlive(pid);
19249
19184
  }
19185
+ var init_job_process = __esm({
19186
+ "src/mcp/job-process.ts"() {
19187
+ "use strict";
19188
+ }
19189
+ });
19190
+
19191
+ // src/mcp/wait-for-job.ts
19192
+ function mcpWaitForJobTimeoutMs(requested) {
19193
+ return mcpWaitForTurnTimeoutMs(requested);
19194
+ }
19195
+ function sanitizeDetachedJobId(id) {
19196
+ const s = id.trim();
19197
+ if (!JOB_ID_RE.test(s)) {
19198
+ throw new Error(`detached-job id must be 1\u201364 chars [A-Za-z0-9._-], got ${JSON.stringify(id)}`);
19199
+ }
19200
+ return s;
19201
+ }
19250
19202
  function readIntFile(file) {
19251
19203
  if (!(0, import_node_fs50.existsSync)(file)) return null;
19252
19204
  const n = Number.parseInt((0, import_node_fs50.readFileSync)(file, "utf8").trim(), 10);
@@ -19287,7 +19239,7 @@ function looksLikeDeferredDonePromise(text6) {
19287
19239
  if (!t || t.length > DEFERRED_DONE_MAX_CHARS) return false;
19288
19240
  const unquoted = t.replace(/`[^`]*`/g, " ").replace(/["“”][^"“”]*["“”]/g, " ").replace(/\s+/g, " ").trim();
19289
19241
  if (!unquoted) return false;
19290
- return /\b(i['’]?ll|i will)\s+let you know\b/i.test(unquoted) || /\blet you know when\b/i.test(unquoted) || /\b(check back|ping me)\s+when\b/i.test(unquoted) || /\bi(?:['’]ll| will)\s+(report|update you)\s+when\b/i.test(unquoted) || /\bbackground poll\b/i.test(unquoted) || /\bwill notify (?:me|you) when\b/i.test(unquoted) || /\bi(?:['’]ll| will) be notified when\b/i.test(unquoted) || /\bi(?:['’]ll| will)\s+(?:keep|continue)\s+(?:watching|waiting|polling|checking)\b/i.test(
19242
+ return /\b(i['’]?ll|i will)\s+let you know\b/i.test(unquoted) || /\blet you know when\b/i.test(unquoted) || /\b(check back|ping me)\s+when\b/i.test(unquoted) || /\bi(?:['’]ll| will)\s+(report|update you)\s+when\b/i.test(unquoted) || /\bwill notify (?:me|you) when\b/i.test(unquoted) || /\bi(?:['’]ll| will) be notified when\b/i.test(unquoted) || /\bi(?:['’]ll| will)\s+(?:keep|continue)\s+(?:watching|waiting|polling|checking)\b/i.test(
19291
19243
  unquoted
19292
19244
  ) || /\bi(?:['’]ll| will)\s+come back when\b/i.test(unquoted);
19293
19245
  }
@@ -19331,7 +19283,7 @@ function formatJobStillRunningContinuePrompt(jobIds) {
19331
19283
  function formatDeferredDoneContinuePrompt() {
19332
19284
  return [
19333
19285
  "You ended the turn after promising to report later, but no detached job is running.",
19334
- "A native background poll / Task / bash notify does not resume this chat (Claude, Cursor, Codex, OpenCode).",
19286
+ "A native CLI Task or bash notify does not resume this chat (Claude, Cursor, Codex, OpenCode).",
19335
19287
  "If tests/pack/deploy still need to run: start once with detached-job.cjs, then loop wait_for_job until stillRunning is false (the log pane updates from wait JSON).",
19336
19288
  "Do not say you will let the user know later."
19337
19289
  ].join(" ");
@@ -19430,7 +19382,7 @@ async function waitForDetachedJob(cwd, id, opts) {
19430
19382
  const root = cwd.trim() || process.cwd();
19431
19383
  const dir = resolveJobDir(root, jobId);
19432
19384
  const timeoutMs = mcpWaitForJobTimeoutMs(opts?.timeoutMs);
19433
- const sleep2 = opts?.sleep ?? sleepMs;
19385
+ const sleep3 = opts?.sleep ?? sleepMs;
19434
19386
  if (!(0, import_node_fs50.existsSync)(dir)) {
19435
19387
  return {
19436
19388
  stillRunning: false,
@@ -19456,7 +19408,7 @@ async function waitForDetachedJob(cwd, id, opts) {
19456
19408
  snap = snapshotJob(dir);
19457
19409
  if (snap.exitCode === 0 && !snap.running) return toResult(jobId, snap);
19458
19410
  if (!snap.running) return toResult(jobId, snap, { failed: true });
19459
- await sleep2(Math.min(2e3, Math.max(50, deadline - Date.now())));
19411
+ await sleep3(Math.min(2e3, Math.max(50, deadline - Date.now())));
19460
19412
  }
19461
19413
  snap = snapshotJob(dir);
19462
19414
  return toResult(jobId, snap);
@@ -19488,7 +19440,7 @@ async function stopDetachedJob(cwd, id, opts) {
19488
19440
  const jobId = sanitizeDetachedJobId(id);
19489
19441
  const root = cwd.trim() || process.cwd();
19490
19442
  const dir = resolveJobDir(root, jobId);
19491
- const sleep2 = opts?.sleep ?? sleepMs;
19443
+ const sleep3 = opts?.sleep ?? sleepMs;
19492
19444
  const why = opts?.reason?.trim() ?? "";
19493
19445
  if (!(0, import_node_fs50.existsSync)(dir)) {
19494
19446
  return {
@@ -19520,11 +19472,11 @@ async function stopDetachedJob(cwd, id, opts) {
19520
19472
  const graceMs = Number.isFinite(opts?.graceMs) ? Math.max(50, opts.graceMs) : 2e3;
19521
19473
  const deadline = Date.now() + graceMs;
19522
19474
  while (Date.now() < deadline && jobTreeAlive(targetPid)) {
19523
- await sleep2(Math.min(100, Math.max(20, deadline - Date.now())));
19475
+ await sleep3(Math.min(100, Math.max(20, deadline - Date.now())));
19524
19476
  }
19525
19477
  if (targetPid != null) {
19526
19478
  killProcessTree(targetPid, "SIGKILL");
19527
- await sleep2(100);
19479
+ await sleep3(100);
19528
19480
  }
19529
19481
  const exitFile = (0, import_node_path50.join)(dir, "exit");
19530
19482
  if (!(0, import_node_fs50.existsSync)(exitFile)) {
@@ -19556,6 +19508,7 @@ var init_wait_for_job = __esm({
19556
19508
  import_node_fs50 = require("fs");
19557
19509
  import_node_path50 = require("path");
19558
19510
  init_workspace_scratch();
19511
+ init_job_process();
19559
19512
  init_wait_for_turn();
19560
19513
  MAX_JOB_CONTINUES = 8;
19561
19514
  MAX_JOB_DELTA_LINES = 80;
@@ -24865,7 +24818,7 @@ var init_long_running = __esm({
24865
24818
 
24866
24819
  A Sideboard or Cursor **worktree turn** SIGTERMs the agent shell (and its process group) when the user sends another message or the turn is interrupted. \`block_until_ms: 0\` is not enough \u2014 the child stays in that group.
24867
24820
 
24868
- Do **not** ask the human to check back. Detach, then **wait** in 45s slices (MCP \`wait_for_job\`, same idea as \`wait_for_turn\`) until \`stillRunning\` is false. Do not end the turn with \u201CI\u2019ll let you know\u201D or \u201Ca background poll will notify me.\u201D Native background bash/Task notify (Claude, Cursor, Codex, OpenCode) does **not** resume a Sideboard chat.
24821
+ Do **not** ask the human to check back. Detach, then **wait** in 45s slices (MCP \`wait_for_job\`, same idea as \`wait_for_turn\`) until \`stillRunning\` is false. Do not end the turn after starting a job \u2014 a native CLI Task or bash notify does **not** resume a Sideboard chat (Claude, Cursor, Codex, OpenCode).
24869
24822
 
24870
24823
  ## Tool
24871
24824
 
@@ -25534,7 +25487,7 @@ function formatPrGateDirective() {
25534
25487
  return [
25535
25488
  "If a goal is given (not after every push):",
25536
25489
  "- Enter a watch-fix-push loop and stay in it until that goal is met or you are blocked (examples: \u201CGreptile 5/5\u201D, \u201CCI green\u201D, \u201Cuntil checks pass\u201D). Do not stop after one red run or one review cycle.",
25537
- "- Wait with `/long-running`: detach `gh pr checks --watch` (add `-R <origin>` when needed), then loop `wait_for_job` until stillRunning is false. Do not end the turn \u2014 a background poll / Task notify does not resume this chat. Do not ask the human to poll.",
25490
+ "- Wait with `/long-running`: detach `gh pr checks --watch` (add `-R <origin>` when needed), then loop `wait_for_job` until stillRunning is false. Do not end the turn \u2014 a native CLI Task or bash notify does not resume this chat. Do not ask the human to poll.",
25538
25491
  "- Greptile 5/5: wait for the greptile check; read the latest `N/5` from the PR body or Greptile\u2019s most recently *updated* issue comment; also fetch unresolved review comments / the \u201CPrompt to fix all with AI\u201D block. Done = **5/5 and zero unresolved Greptile comments**. If Greptile is idle, comment `@greptile review`. Cap about 5 iterations, then report leftovers. Do not merge unless asked."
25539
25492
  ].join("\n");
25540
25493
  }
@@ -25567,7 +25520,7 @@ function formatLongRunningDirective(opts) {
25567
25520
  }
25568
25521
  function formatLongRunningReminder(opts) {
25569
25522
  const invoke = formatDetachedJobInvoke(opts?.scriptPath);
25570
- return `Long jobs: \`${invoke} start <id> -- <cmd>\`, loop wait_for_job (or detached-job wait). The log pane updates from wait JSON; present_artifact type=log is optional. stop_job if hanging or wrong. Do not say you will let the user know later \u2014 stay in the turn. A background poll / Task notify does not resume this chat. Project Dev button: list_run_scripts / run_dev_script / stop_dev_script / get_run_log (UI-connected \u2014 not detached-job). ${formatDevAccessHint(opts?.activeRuns)}`;
25523
+ return `Long jobs: \`${invoke} start <id> -- <cmd>\`, loop wait_for_job (or detached-job wait). The log pane updates from wait JSON; present_artifact type=log is optional. stop_job if hanging or wrong. Do not say you will let the user know later \u2014 stay in the turn. A native CLI Task or bash notify does not resume this chat. Project Dev button: list_run_scripts / run_dev_script / stop_dev_script / get_run_log (UI-connected \u2014 not detached-job). ${formatDevAccessHint(opts?.activeRuns)}`;
25571
25524
  }
25572
25525
  function formatArtifactDirective() {
25573
25526
  return [
@@ -26890,7 +26843,8 @@ var init_orchestrator = __esm({
26890
26843
  throw new Error(`Thread is archived: ${thread.id}`);
26891
26844
  }
26892
26845
  const followUp = resolveSendFollowUp(current, opts?.followUp);
26893
- const consumed = consumeComposerAttachments(current);
26846
+ const explicit = opts?.attachments;
26847
+ const consumed = explicit !== void 0 ? { attachments: current.attachments, consumed: explicit } : consumeComposerAttachments(current);
26894
26848
  const queued = appendQueuedItem(
26895
26849
  current.queue,
26896
26850
  current.queueAttachments,
@@ -26904,7 +26858,7 @@ var init_orchestrator = __esm({
26904
26858
  const inFlight = this.activeTurns.has(thread.id) || this.startingTurns.has(thread.id) || current.status === "running";
26905
26859
  shouldSteer = followUp === "steer" && (inFlight || current.queue.length > 0);
26906
26860
  const patch = queued;
26907
- if (consumed.consumed.length > 0) {
26861
+ if (explicit === void 0 && consumed.consumed.length > 0) {
26908
26862
  patch.attachments = consumed.attachments;
26909
26863
  }
26910
26864
  if (!inFlight) patch.status = "queued";
@@ -29393,6 +29347,7 @@ __export(index_exports, {
29393
29347
  disconnectLinear: () => disconnectLinear,
29394
29348
  disconnectLinearConnection: () => disconnectLinearConnection,
29395
29349
  disconnectOptionalService: () => disconnectOptionalService,
29350
+ disconnectRemoteAccount: () => disconnectRemoteAccount,
29396
29351
  disconnectSlackWorkspace: () => disconnectSlackWorkspace,
29397
29352
  discoverSkills: () => discoverSkills,
29398
29353
  downloadAbleTimeAttachment: () => downloadAbleTimeAttachment,
@@ -29649,6 +29604,7 @@ __export(index_exports, {
29649
29604
  isPrNotMergeableError: () => isPrNotMergeableError,
29650
29605
  isPresentPlanToolName: () => isPresentPlanToolName,
29651
29606
  isPrimaryCheckoutThread: () => isPrimaryCheckoutThread,
29607
+ isRemoteAccountLoginCancelled: () => isRemoteAccountLoginCancelled,
29652
29608
  isRemoteStopCommand: () => isRemoteStopCommand,
29653
29609
  isSelfWrittenRecord: () => isSelfWrittenRecord,
29654
29610
  isSessionQuotaLimit: () => isSessionQuotaLimit,
@@ -30010,6 +29966,7 @@ __export(index_exports, {
30010
29966
  startMcpServer: () => startMcpServer,
30011
29967
  startOrchestration: () => startOrchestration,
30012
29968
  startRelayServer: () => startRelayServer,
29969
+ startRemoteAccountLogin: () => startRemoteAccountLogin,
30013
29970
  startSlackOAuth: () => startSlackOAuth,
30014
29971
  statWorktreePath: () => statWorktreePath,
30015
29972
  stopAgentRpcServer: () => stopAgentRpcServer,
@@ -30543,12 +30500,12 @@ async function persistLinearTokens(data, viewer) {
30543
30500
  organizationName: viewer?.organizationName
30544
30501
  });
30545
30502
  }
30546
- async function fetchLinearViewer(accessToken) {
30503
+ async function fetchLinearViewer(accessToken2) {
30547
30504
  const res = await httpFetch(LINEAR_GRAPHQL, {
30548
30505
  method: "POST",
30549
30506
  headers: {
30550
30507
  "Content-Type": "application/json",
30551
- Authorization: linearAuthorizationHeader(accessToken)
30508
+ Authorization: linearAuthorizationHeader(accessToken2)
30552
30509
  },
30553
30510
  body: JSON.stringify({
30554
30511
  query: "{ viewer { name organization { name } } }"
@@ -38389,7 +38346,14 @@ function parseRemoteClientMessage(raw) {
38389
38346
  const hostSecret = str5(parsed.hostSecret);
38390
38347
  const deviceLabel = str5(parsed.deviceLabel);
38391
38348
  if (!deviceId || !hostSecret || !deviceLabel) return null;
38392
- return { type: "host_register", deviceId, hostSecret, deviceLabel };
38349
+ const accountToken = str5(parsed.accountToken);
38350
+ return {
38351
+ type: "host_register",
38352
+ deviceId,
38353
+ hostSecret,
38354
+ deviceLabel,
38355
+ ...accountToken ? { accountToken } : {}
38356
+ };
38393
38357
  }
38394
38358
  if (msg.type === "pair") {
38395
38359
  const code = str5(parsed.code).toUpperCase();
@@ -38518,6 +38482,10 @@ function newCode() {
38518
38482
  return code;
38519
38483
  }
38520
38484
  var RemoteHub = class {
38485
+ constructor(opts = {}) {
38486
+ this.opts = opts;
38487
+ }
38488
+ opts;
38521
38489
  hosts = /* @__PURE__ */ new Map();
38522
38490
  remembered = /* @__PURE__ */ new Map();
38523
38491
  bySocket = /* @__PURE__ */ new Map();
@@ -38556,7 +38524,7 @@ var RemoteHub = class {
38556
38524
  }
38557
38525
  dispatch(socket, msg) {
38558
38526
  if (msg.type === "host_register") {
38559
- this.registerHost(socket, msg.deviceId, msg.deviceLabel, msg.hostSecret);
38527
+ this.registerHost(socket, msg.deviceId, msg.deviceLabel, msg.hostSecret, msg.accountToken ?? "");
38560
38528
  return;
38561
38529
  }
38562
38530
  if (msg.type === "host_pair") {
@@ -38583,7 +38551,14 @@ var RemoteHub = class {
38583
38551
  this.fromHost(socket, msg);
38584
38552
  }
38585
38553
  }
38586
- registerHost(socket, deviceId, deviceLabel, hostSecret) {
38554
+ registerHost(socket, deviceId, deviceLabel, hostSecret, accountToken) {
38555
+ if (this.opts.authorizeHost) {
38556
+ const decision = this.opts.authorizeHost({ accountToken, deviceId, deviceLabel, hostSecret });
38557
+ if (!decision.ok) {
38558
+ send2(socket, { type: "error", message: decision.message });
38559
+ return;
38560
+ }
38561
+ }
38587
38562
  const prior = this.remembered.get(deviceId);
38588
38563
  const existing = this.hosts.get(deviceId);
38589
38564
  if (prior && prior.hostSecret !== hostSecret || existing && existing.hostSecret !== hostSecret) {
@@ -38615,6 +38590,7 @@ var RemoteHub = class {
38615
38590
  this.bySocket.set(socket, { role: "host", deviceId });
38616
38591
  send2(socket, { type: "registered", deviceId, deviceLabel });
38617
38592
  if (host.phone) send2(socket, { type: "phone_joined" });
38593
+ this.opts.onAuthorized?.(socket);
38618
38594
  }
38619
38595
  issueCode(socket) {
38620
38596
  const bound = this.bySocket.get(socket);
@@ -38676,6 +38652,7 @@ var RemoteHub = class {
38676
38652
  online: Boolean(host)
38677
38653
  };
38678
38654
  });
38655
+ if (hosts.some((host) => host.deviceId)) this.opts.onAuthorized?.(socket);
38679
38656
  send2(socket, { type: "hosts", hosts });
38680
38657
  }
38681
38658
  releasePhoneSocket(socket) {
@@ -38715,6 +38692,7 @@ var RemoteHub = class {
38715
38692
  deviceLabel: host.deviceLabel
38716
38693
  });
38717
38694
  this.bySocket.set(socket, { role: "phone", deviceId: host.deviceId });
38695
+ this.opts.onAuthorized?.(socket);
38718
38696
  send2(socket, {
38719
38697
  type: "paired",
38720
38698
  deviceId: host.deviceId,
@@ -38753,10 +38731,545 @@ var import_ws5 = require("ws");
38753
38731
 
38754
38732
  // src/remote/listen.ts
38755
38733
  init_orchestrator_capable();
38734
+ init_stage_files();
38756
38735
  init_orchestrator();
38757
38736
  init_app_settings();
38758
38737
  init_global_workspace();
38759
38738
  init_thread_store();
38739
+ init_implied_first_prompt();
38740
+
38741
+ // src/remote/phone-chats.ts
38742
+ var import_node_crypto25 = require("crypto");
38743
+ init_list_models();
38744
+ init_home_board();
38745
+ init_stage_files();
38746
+ init_worktree();
38747
+ init_worktree_labels();
38748
+ init_thinking_effort();
38749
+ init_app_settings();
38750
+ init_global_workspace();
38751
+ init_thread_store();
38752
+ init_workspaces();
38753
+ init_chat_tabs();
38754
+ init_create();
38755
+ init_orchestrator();
38756
+ var PHONE_CONTROL_PREFIX = "\0sb.phone\n";
38757
+ var PHONE_USER_PREFIX = "Phone\n\n";
38758
+ var TRANSCRIPT_LIMIT = 40;
38759
+ var MESSAGE_LIMIT = 4e3;
38760
+ var PREVIEW_LIMIT = 90;
38761
+ var HISTORY_PAGE_SIZE = 40;
38762
+ var AGENT_KINDS = /* @__PURE__ */ new Set(["claude", "codex", "opencode", "brightsy", "cursor"]);
38763
+ var MAX_PHONE_FILES = 8;
38764
+ var MAX_PHONE_FILE_CHARS = Math.ceil(8 * 1024 * 1024 * 4 / 3);
38765
+ var MAX_PHONE_FILES_CHARS = Math.ceil(20 * 1024 * 1024 * 4 / 3);
38766
+ var MAX_DICTATION_CHARS = Math.ceil(12 * 1024 * 1024 * 4 / 3);
38767
+ function encodePhoneControl(payload) {
38768
+ return PHONE_CONTROL_PREFIX + JSON.stringify(payload);
38769
+ }
38770
+ function takePhoneControl(text6) {
38771
+ if (!text6.startsWith(PHONE_CONTROL_PREFIX)) return null;
38772
+ let parsed;
38773
+ try {
38774
+ parsed = JSON.parse(text6.slice(PHONE_CONTROL_PREFIX.length));
38775
+ } catch {
38776
+ return "invalid";
38777
+ }
38778
+ return parsePhoneControl(parsed);
38779
+ }
38780
+ function parsePhoneControl(value) {
38781
+ if (!value || typeof value !== "object") return "invalid";
38782
+ const op = value.op;
38783
+ if (op === "list") return { op: "list" };
38784
+ if (op === "history") {
38785
+ const query = textField(value, "query");
38786
+ const after = textField(value, "after");
38787
+ return { op: "history", ...query ? { query } : {}, ...after ? { after } : {} };
38788
+ }
38789
+ if (op === "sources") {
38790
+ const repoPath = textField(value, "repoPath");
38791
+ if (!repoPath || !isProjectPath(repoPath)) return "invalid";
38792
+ const query = textField(value, "query");
38793
+ return { op: "sources", repoPath, ...query ? { query } : {} };
38794
+ }
38795
+ if (op === "models") {
38796
+ const agent = agentField(value);
38797
+ if (!agent) return "invalid";
38798
+ return { op: "models", agent };
38799
+ }
38800
+ if (op === "dictate") {
38801
+ const id = textField(value, "id");
38802
+ const audioBase64 = base64Field(value, "audioBase64");
38803
+ if (!id || !audioBase64 || audioBase64.length > MAX_DICTATION_CHARS) return "invalid";
38804
+ return { op: "dictate", id, audioBase64 };
38805
+ }
38806
+ if (op === "create") return parsePhoneCreate(value);
38807
+ const chatId = typeof value.chatId === "string" ? value.chatId.trim() : "";
38808
+ if (!chatId) return "invalid";
38809
+ if (op === "open" || op === "archive" || op === "restore" || op === "stop") return { op, chatId };
38810
+ const draft = parsePhoneDraft(value);
38811
+ if (draft === "invalid") return "invalid";
38812
+ if (op === "options") return { op: "options", chatId, ...draft };
38813
+ if (op === "prompt") {
38814
+ const text6 = typeof value.text === "string" ? value.text.trim() : "";
38815
+ const hasPayload = Boolean(text6 || draft.files?.length || draft.links?.length);
38816
+ if (!hasPayload) return "invalid";
38817
+ return { op: "prompt", chatId, text: text6, ...draft };
38818
+ }
38819
+ return "invalid";
38820
+ }
38821
+ function textField(value, key) {
38822
+ const raw = value[key];
38823
+ return typeof raw === "string" ? raw.trim() : "";
38824
+ }
38825
+ function agentField(value) {
38826
+ const agent = textField(value, "agent");
38827
+ if (!agent) return void 0;
38828
+ return AGENT_KINDS.has(agent) ? agent : void 0;
38829
+ }
38830
+ function base64Field(value, key) {
38831
+ const raw = value[key];
38832
+ if (typeof raw !== "string") return "";
38833
+ return raw.replace(/\s/g, "");
38834
+ }
38835
+ function parsePhoneDraft(value) {
38836
+ const draft = {};
38837
+ if (textField(value, "agent")) {
38838
+ const agent = agentField(value);
38839
+ if (!agent) return "invalid";
38840
+ draft.agent = agent;
38841
+ }
38842
+ if ("model" in value) {
38843
+ const model = value.model;
38844
+ if (model === null || model === "") draft.model = null;
38845
+ else if (typeof model === "string") draft.model = model.trim().slice(0, 200);
38846
+ else return "invalid";
38847
+ }
38848
+ if ("effort" in value && value.effort != null) {
38849
+ const effort = value.effort;
38850
+ if (!isThinkingEffort(effort)) return "invalid";
38851
+ draft.effort = effort;
38852
+ }
38853
+ if ("fast" in value) {
38854
+ const fast = value.fast;
38855
+ if (typeof fast !== "boolean") return "invalid";
38856
+ draft.fast = fast;
38857
+ }
38858
+ if ("planMode" in value) {
38859
+ const planMode = value.planMode;
38860
+ if (typeof planMode !== "boolean") return "invalid";
38861
+ draft.planMode = planMode;
38862
+ }
38863
+ if ("autonomy" in value && value.autonomy != null) {
38864
+ const autonomy = value.autonomy;
38865
+ if (autonomy !== "default" && autonomy !== "full") return "invalid";
38866
+ draft.autonomy = autonomy;
38867
+ }
38868
+ const files = parsePhoneFiles(value.files);
38869
+ if (files === "invalid") return "invalid";
38870
+ if (files.length) draft.files = files;
38871
+ const links = parsePhoneLinks(value.links);
38872
+ if (links === "invalid") return "invalid";
38873
+ if (links.length) draft.links = links;
38874
+ const prompt = textField(value, "prompt");
38875
+ if (prompt) draft.prompt = prompt.slice(0, 2e4);
38876
+ return draft;
38877
+ }
38878
+ function parsePhoneFiles(value) {
38879
+ if (value == null) return [];
38880
+ if (!Array.isArray(value) || value.length > MAX_PHONE_FILES) return "invalid";
38881
+ const files = [];
38882
+ let total = 0;
38883
+ for (const item of value) {
38884
+ if (!item || typeof item !== "object") return "invalid";
38885
+ const name = (textField(item, "name").replace(/[/\\]/g, "_") || "file").slice(0, 180);
38886
+ const dataBase64 = base64Field(item, "dataBase64");
38887
+ if (!dataBase64 || !/^[A-Za-z0-9+/=]+$/.test(dataBase64)) return "invalid";
38888
+ if (dataBase64.length > MAX_PHONE_FILE_CHARS) return "invalid";
38889
+ total += dataBase64.length;
38890
+ if (total > MAX_PHONE_FILES_CHARS) return "invalid";
38891
+ files.push({ name, dataBase64 });
38892
+ }
38893
+ return files;
38894
+ }
38895
+ function parsePhoneLinks(value) {
38896
+ if (value == null) return [];
38897
+ if (!Array.isArray(value) || value.length > 8) return "invalid";
38898
+ const links = [];
38899
+ for (const item of value) {
38900
+ if (!item || typeof item !== "object") return "invalid";
38901
+ const ref = textField(item, "ref").slice(0, 80);
38902
+ if (!ref) return "invalid";
38903
+ const title = (textField(item, "title") || ref).slice(0, 200);
38904
+ const url = textField(item, "url").slice(0, 500);
38905
+ links.push({ ref, title, ...url ? { url } : {} });
38906
+ }
38907
+ return links;
38908
+ }
38909
+ function parsePhoneCreate(value) {
38910
+ const draft = parsePhoneDraft(value);
38911
+ if (draft === "invalid") return "invalid";
38912
+ const goal = textField(value, "goal") || draft.prompt || "";
38913
+ const where = value.where;
38914
+ if (where == null || where === "orchestration") {
38915
+ return {
38916
+ op: "create",
38917
+ where: "orchestration",
38918
+ ...draft,
38919
+ ...goal ? { goal, prompt: goal } : {}
38920
+ };
38921
+ }
38922
+ if (where === "worktree") {
38923
+ const chatId = textField(value, "chatId");
38924
+ if (!chatId) return "invalid";
38925
+ return { op: "create", where: "worktree", chatId, ...draft };
38926
+ }
38927
+ if (where === "project") {
38928
+ const repoPath = textField(value, "repoPath");
38929
+ if (!repoPath || !isProjectPath(repoPath)) return "invalid";
38930
+ const sourceRaw = textField(value, "sourceType");
38931
+ let sourceType;
38932
+ if (sourceRaw === "branch" || sourceRaw === "pr" || sourceRaw === "ticket") sourceType = sourceRaw;
38933
+ else if (sourceRaw) return "invalid";
38934
+ const sourceRef = textField(value, "sourceRef");
38935
+ if ((sourceType === "pr" || sourceType === "ticket") && !sourceRef) return "invalid";
38936
+ const title = textField(value, "title").slice(0, 200);
38937
+ const cowboy = value.cowboy === true;
38938
+ return {
38939
+ op: "create",
38940
+ where: "project",
38941
+ repoPath,
38942
+ ...draft,
38943
+ ...sourceType ? { sourceType } : {},
38944
+ ...sourceRef ? { sourceRef } : {},
38945
+ ...title ? { title } : {},
38946
+ ...cowboy ? { cowboy: true } : {}
38947
+ };
38948
+ }
38949
+ return "invalid";
38950
+ }
38951
+ function phoneVisibleText(role, text6) {
38952
+ const trimmed = text6.trim();
38953
+ if (role === "user" && trimmed.startsWith(PHONE_USER_PREFIX)) {
38954
+ return trimmed.slice(PHONE_USER_PREFIX.length).trim();
38955
+ }
38956
+ return trimmed;
38957
+ }
38958
+ function clip(text6, limit) {
38959
+ if (text6.length <= limit) return text6;
38960
+ return `${text6.slice(0, limit - 1)}\u2026`;
38961
+ }
38962
+ function summarize(thread) {
38963
+ return {
38964
+ id: thread.id,
38965
+ title: thread.title?.trim() || "Untitled",
38966
+ status: thread.status,
38967
+ preview: previewOf(thread),
38968
+ updatedAt: thread.updatedAt
38969
+ };
38970
+ }
38971
+ function previewOf(thread) {
38972
+ const messages = listPhoneMessages(thread);
38973
+ const last = messages[messages.length - 1];
38974
+ if (!last) return "";
38975
+ return clip(last.text.replace(/\s+/g, " ").trim(), PREVIEW_LIMIT);
38976
+ }
38977
+ function listPhoneMessages(thread) {
38978
+ const out = [];
38979
+ for (const message of thread.messages) {
38980
+ if (message.role !== "user" && message.role !== "agent") continue;
38981
+ const names = (message.attachments ?? []).map((item) => item.name.trim()).filter(Boolean);
38982
+ const text6 = clip(
38983
+ [phoneVisibleText(message.role, message.text), names.length ? names.join(", ") : ""].filter(Boolean).join("\n"),
38984
+ MESSAGE_LIMIT
38985
+ );
38986
+ if (!text6) continue;
38987
+ out.push({ role: message.role, text: text6 });
38988
+ }
38989
+ for (const prompt of thread.queue) {
38990
+ const text6 = clip(phoneVisibleText("user", prompt), MESSAGE_LIMIT);
38991
+ if (text6) out.push({ role: "user", text: text6 });
38992
+ }
38993
+ return out.slice(-TRANSCRIPT_LIMIT);
38994
+ }
38995
+ function repoName(repoPath) {
38996
+ const parts = repoPath.replace(/\/$/, "").split("/");
38997
+ return parts[parts.length - 1] || repoPath;
38998
+ }
38999
+ function isProjectPath(path2) {
39000
+ if (!path2 || path2 === "/" || path2 === ".") return false;
39001
+ return !isGlobalThread({ repoPath: path2 });
39002
+ }
39003
+ function listPhoneChats() {
39004
+ return listGlobalThreads().slice().sort((a, b) => a.updatedAt < b.updatedAt ? 1 : a.updatedAt > b.updatedAt ? -1 : 0).map(summarize);
39005
+ }
39006
+ function listPhoneSidebar() {
39007
+ const threads = listThreads();
39008
+ const byRepo = /* @__PURE__ */ new Map();
39009
+ for (const thread of threads) {
39010
+ if (!isProjectPath(thread.repoPath)) continue;
39011
+ const list = byRepo.get(thread.repoPath) ?? [];
39012
+ list.push(thread);
39013
+ byRepo.set(thread.repoPath, list);
39014
+ }
39015
+ for (const workspace of listWorkspaces()) {
39016
+ if (!isProjectPath(workspace.path) || byRepo.has(workspace.path)) continue;
39017
+ byRepo.set(workspace.path, []);
39018
+ }
39019
+ const projects = [...byRepo.entries()].sort(([a], [b]) => repoName(a).localeCompare(repoName(b))).map(([path2, repoThreads]) => ({
39020
+ name: repoName(path2),
39021
+ path: path2,
39022
+ worktrees: groupHomeBoardWorktrees(repoThreads).map((group) => ({
39023
+ label: worktreeDisplayLabelForGroup(group),
39024
+ chats: group.map(summarize)
39025
+ }))
39026
+ }));
39027
+ return { orchestration: listPhoneChats(), projects };
39028
+ }
39029
+ function historyStamp(thread) {
39030
+ return thread.archivedAt || thread.updatedAt;
39031
+ }
39032
+ function historyCursor(chat) {
39033
+ return `${chat.updatedAt} ${chat.id}`;
39034
+ }
39035
+ function historyQueryMatch(chat, query) {
39036
+ if (!query) return true;
39037
+ return `${chat.title} ${chat.where} ${chat.agent} ${chat.preview}`.toLowerCase().includes(query);
39038
+ }
39039
+ function listPhoneHistory(opts) {
39040
+ const query = opts?.query?.trim().toLowerCase() ?? "";
39041
+ const after = opts?.after?.trim() ?? "";
39042
+ const all = listThreads({ includeArchived: true }).filter((thread) => thread.status === "archived").filter((thread) => isGlobalThread(thread) || isProjectPath(thread.repoPath)).sort((a, b) => historyStamp(a) < historyStamp(b) ? 1 : historyStamp(a) > historyStamp(b) ? -1 : 0).map((thread) => ({
39043
+ id: thread.id,
39044
+ title: thread.title?.trim() || "Untitled",
39045
+ preview: previewOf(thread),
39046
+ updatedAt: historyStamp(thread),
39047
+ where: isGlobalThread(thread) ? "Orchestration" : repoName(thread.repoPath),
39048
+ agent: thread.agent
39049
+ })).filter((chat) => historyQueryMatch(chat, query));
39050
+ let start = 0;
39051
+ if (after) {
39052
+ const index = all.findIndex((chat) => historyCursor(chat) === after);
39053
+ start = index >= 0 ? index + 1 : all.length;
39054
+ }
39055
+ const chats = all.slice(start, start + HISTORY_PAGE_SIZE);
39056
+ const last = chats.at(-1);
39057
+ const more = start + chats.length < all.length;
39058
+ return {
39059
+ chats,
39060
+ total: all.length,
39061
+ ...more && last ? { next: historyCursor(last) } : {}
39062
+ };
39063
+ }
39064
+ function readPhoneThread(chatId) {
39065
+ const id = chatId.trim();
39066
+ if (!id) return null;
39067
+ const thread = readThread(id);
39068
+ if (!thread || thread.status === "archived") return null;
39069
+ if (isGlobalThread(thread)) return thread;
39070
+ if (!isProjectPath(thread.repoPath)) return null;
39071
+ return thread;
39072
+ }
39073
+ function phoneComposerOptions(thread) {
39074
+ return {
39075
+ agent: thread.agent,
39076
+ model: thread.model ?? null,
39077
+ effort: thread.effort,
39078
+ fast: Boolean(thread.fast),
39079
+ planMode: Boolean(thread.planMode),
39080
+ autonomy: thread.autonomy ?? "default"
39081
+ };
39082
+ }
39083
+ function phoneTurnAttachments(draft) {
39084
+ const links = (draft.links ?? []).map((link) => ({
39085
+ id: (0, import_node_crypto25.randomUUID)(),
39086
+ name: link.ref,
39087
+ kind: "issue",
39088
+ content: [`Linked issue: ${link.ref} \u2014 ${link.title}`, link.url ? `URL: ${link.url}` : null].filter(Boolean).join("\n")
39089
+ }));
39090
+ const files = (draft.files ?? []).map((file) => ({
39091
+ id: (0, import_node_crypto25.randomUUID)(),
39092
+ name: file.name,
39093
+ kind: "file",
39094
+ previewDataUrl: `data:application/octet-stream;base64,${file.dataBase64}`,
39095
+ content: `File attached: ${file.name}`
39096
+ }));
39097
+ return [...links, ...files];
39098
+ }
39099
+ function openedFrom(thread, place) {
39100
+ const fresh = readThread(thread.id) ?? thread;
39101
+ return {
39102
+ chat: summarize(fresh),
39103
+ messages: listPhoneMessages(fresh),
39104
+ place,
39105
+ options: phoneComposerOptions(fresh)
39106
+ };
39107
+ }
39108
+ function openPhoneChat(chatId) {
39109
+ const thread = readPhoneThread(chatId);
39110
+ if (!thread) return null;
39111
+ const place = isGlobalThread(thread) ? { kind: "orchestration" } : projectPlace(thread);
39112
+ return openedFrom(thread, place);
39113
+ }
39114
+ function sameRepoPath3(a, b) {
39115
+ return a.replace(/\/+$/, "") === b.replace(/\/+$/, "");
39116
+ }
39117
+ function projectPlace(thread) {
39118
+ const group = threadsSharingWorktree(thread.worktreePath);
39119
+ return {
39120
+ kind: "project",
39121
+ repoPath: thread.repoPath,
39122
+ worktree: worktreeDisplayLabelForGroup(group.length > 0 ? group : [thread])
39123
+ };
39124
+ }
39125
+ function assertKnownProject(repoPath) {
39126
+ const path2 = repoPath.trim();
39127
+ if (!isProjectPath(path2)) throw new Error("Pick a project on this Mac.");
39128
+ const known = listWorkspaces().some((workspace) => sameRepoPath3(workspace.path, path2)) || listThreads().some(
39129
+ (thread) => thread.status !== "archived" && sameRepoPath3(thread.repoPath, path2)
39130
+ );
39131
+ if (!known) throw new Error("That project is not on this Mac.");
39132
+ return path2;
39133
+ }
39134
+ function draftAgentOptions(draft, defaults) {
39135
+ return {
39136
+ agent: draft?.agent ?? defaults.agent,
39137
+ model: draft && "model" in draft ? draft.model ?? null : defaults.model,
39138
+ effort: draft?.effort ?? defaults.effort,
39139
+ fast: draft?.fast ?? defaults.fast,
39140
+ planMode: Boolean(draft?.planMode),
39141
+ autonomy: draft?.autonomy
39142
+ };
39143
+ }
39144
+ function createPhoneChat(agent) {
39145
+ const defaults = resolveOrchestratorDefaults();
39146
+ const thread = createGlobalChat({
39147
+ agent: agent ?? defaults.agent,
39148
+ model: defaults.model,
39149
+ effort: defaults.effort,
39150
+ fast: defaults.fast
39151
+ });
39152
+ return openedFrom(thread, { kind: "orchestration" });
39153
+ }
39154
+ async function createPhoneOrchestration(draft) {
39155
+ const goal = (draft?.goal ?? draft?.prompt ?? "").trim();
39156
+ const attachments = phoneTurnAttachments(draft ?? {});
39157
+ if (!goal && attachments.length === 0) return createPhoneChat(draft?.agent);
39158
+ const defaults = resolveOrchestratorDefaults();
39159
+ const options = draftAgentOptions(draft, defaults);
39160
+ const thread = await startOrchestration({
39161
+ goal: goal || "See the attached files.",
39162
+ agent: options.agent,
39163
+ model: options.model,
39164
+ effort: options.effort,
39165
+ fast: options.fast,
39166
+ planMode: options.planMode,
39167
+ autonomy: options.autonomy,
39168
+ attachments
39169
+ });
39170
+ return openedFrom(thread, { kind: "orchestration" });
39171
+ }
39172
+ function createPhoneWorktreeAgent(fromChatId, draft) {
39173
+ const source = readPhoneThread(fromChatId);
39174
+ if (!source || isGlobalThread(source)) throw new Error("That worktree is not on this Mac.");
39175
+ const defaults = resolveNewThreadOptions();
39176
+ const options = draftAgentOptions(draft, defaults);
39177
+ const attachments = persistPendingFileAttachments(
39178
+ source.worktreePath,
39179
+ phoneTurnAttachments(draft ?? {})
39180
+ );
39181
+ const thread = createChatTab({
39182
+ fromThreadId: source.id,
39183
+ agent: options.agent,
39184
+ model: options.model,
39185
+ effort: options.effort,
39186
+ fast: options.fast,
39187
+ autonomy: options.autonomy,
39188
+ attachments
39189
+ });
39190
+ if (draft?.planMode !== void 0) updateThread(thread.id, { planMode: draft.planMode });
39191
+ return openedFrom(thread, projectPlace(readThread(thread.id) ?? thread));
39192
+ }
39193
+ async function createPhoneProjectWorktree(repoPathOrInput) {
39194
+ const input = typeof repoPathOrInput === "string" ? { repoPath: repoPathOrInput } : repoPathOrInput;
39195
+ const path2 = assertKnownProject(input.repoPath);
39196
+ const defaults = resolveNewThreadOptions();
39197
+ const options = draftAgentOptions(input, defaults);
39198
+ const thread = await createThread({
39199
+ sourceType: input.sourceType ?? "branch",
39200
+ sourceRef: input.sourceRef || "default",
39201
+ repoPath: path2,
39202
+ title: input.title,
39203
+ cowboy: input.cowboy,
39204
+ agent: options.agent,
39205
+ model: options.model,
39206
+ effort: options.effort,
39207
+ fast: options.fast,
39208
+ planMode: options.planMode,
39209
+ autonomy: options.autonomy,
39210
+ attachments: phoneTurnAttachments(input),
39211
+ prompt: input.prompt,
39212
+ reuseExisting: false
39213
+ });
39214
+ return openedFrom(thread, projectPlace(thread));
39215
+ }
39216
+ function sourceWarning(label, err) {
39217
+ const message = err instanceof Error ? err.message : String(err);
39218
+ return message ? `${label}: ${message}` : `${label} failed.`;
39219
+ }
39220
+ async function listPhoneSources(repoPath, query) {
39221
+ const path2 = assertKnownProject(repoPath);
39222
+ const q = query?.trim() ?? "";
39223
+ const warnings = [];
39224
+ const [prs, branches, issues] = await Promise.all([
39225
+ listPrs(path2, { state: "open", limit: 40, ...q ? { query: q } : {} }).catch((err) => {
39226
+ warnings.push(sourceWarning("Pull requests", err));
39227
+ return [];
39228
+ }),
39229
+ listBranches(path2).catch((err) => {
39230
+ warnings.push(sourceWarning("Branches", err));
39231
+ return [];
39232
+ }),
39233
+ listIssues(path2, { limit: 40, ...q ? { query: q } : {} }).catch((err) => {
39234
+ warnings.push(sourceWarning("Issues", err));
39235
+ return { issues: [] };
39236
+ })
39237
+ ]);
39238
+ const needle = q.toLowerCase();
39239
+ const branchRows = branches.filter((branch) => !needle || branch.name.toLowerCase().includes(needle)).slice(0, 50).map((branch) => ({
39240
+ ref: branch.name,
39241
+ title: branch.current ? `${branch.name} (current)` : branch.name
39242
+ }));
39243
+ return {
39244
+ prs: prs.slice(0, 40).map((pr) => ({
39245
+ ref: String(pr.number),
39246
+ title: clip(pr.title || `PR #${pr.number}`, 140),
39247
+ ...pr.url ? { url: pr.url } : {}
39248
+ })),
39249
+ branches: branchRows,
39250
+ issues: (issues.issues ?? []).slice(0, 40).map((issue) => ({
39251
+ ref: issue.identifier,
39252
+ title: clip(issue.title || issue.identifier, 140),
39253
+ ...issue.url ? { url: issue.url } : {}
39254
+ })),
39255
+ ...warnings.length ? { warnings } : {}
39256
+ };
39257
+ }
39258
+ async function listPhoneModels(agent) {
39259
+ const catalogs = await listModelsForAgent(agent);
39260
+ return (catalogs[0]?.models ?? []).slice(0, 40).map((model) => ({
39261
+ id: model.id,
39262
+ label: model.displayName || model.id
39263
+ }));
39264
+ }
39265
+ function archivePhoneChat(chatId) {
39266
+ const thread = readPhoneThread(chatId);
39267
+ if (!thread) return false;
39268
+ updateThread(thread.id, { status: "archived" });
39269
+ return true;
39270
+ }
39271
+
39272
+ // src/remote/listen.ts
38760
39273
  var REMOTE_STOPPED_REPLY = "Sideboard stopped the in-progress turn.";
38761
39274
  function formatRemotePrompt(text6) {
38762
39275
  return `Phone
@@ -38768,6 +39281,8 @@ function isRemoteStopCommand(text6) {
38768
39281
  }
38769
39282
  var handleChain2 = Promise.resolve();
38770
39283
  var inboundGeneration = 0;
39284
+ var chatChains = /* @__PURE__ */ new Map();
39285
+ var chatGeneration = /* @__PURE__ */ new Map();
38771
39286
  function enqueue(fn) {
38772
39287
  const run2 = handleChain2.then(fn, fn);
38773
39288
  handleChain2 = run2.then(
@@ -38789,10 +39304,340 @@ function interruptRemoteCoordinator(deviceId, log = () => void 0) {
38789
39304
  return false;
38790
39305
  }
38791
39306
  }
39307
+ function bumpChat(chatId) {
39308
+ const next = (chatGeneration.get(chatId) ?? 0) + 1;
39309
+ chatGeneration.set(chatId, next);
39310
+ return next;
39311
+ }
39312
+ function enqueueChat(chatId, fn) {
39313
+ const prev = chatChains.get(chatId) ?? Promise.resolve();
39314
+ const run2 = prev.then(fn, fn);
39315
+ chatChains.set(
39316
+ chatId,
39317
+ run2.then(
39318
+ () => void 0,
39319
+ () => void 0
39320
+ )
39321
+ );
39322
+ }
39323
+ function emitControl(opts, reply) {
39324
+ opts.onOutbound({ type: "assistant", text: encodePhoneControl(reply) });
39325
+ }
39326
+ function interruptChat(chatId, log) {
39327
+ try {
39328
+ const thread = readPhoneThread(chatId);
39329
+ if (!thread) return false;
39330
+ if (thread.status !== "running" && thread.status !== "queued") return false;
39331
+ getOrchestrator().stop(thread.id, { clearQueue: true });
39332
+ log(`interrupt phone \u2192 chat ${thread.id.slice(0, 8)} (${thread.status})`);
39333
+ return true;
39334
+ } catch (err) {
39335
+ log(`interrupt chat: ${err instanceof Error ? err.message : String(err)}`);
39336
+ return false;
39337
+ }
39338
+ }
39339
+ function emitChatTurn(opts, chatId, threadId) {
39340
+ const thread = readThread(threadId);
39341
+ if (!thread) {
39342
+ emitControl(opts, { op: "error", chatId, message: "That agent is not on this Mac." });
39343
+ return;
39344
+ }
39345
+ if (thread.status === "stopped") {
39346
+ emitControl(opts, { op: "stopped", chatId });
39347
+ return;
39348
+ }
39349
+ const result = getOrchestrator().getTurnResult(thread.id);
39350
+ const pending = extractPendingPlanQuestions(
39351
+ [...thread.messages].reverse().find((m) => m.role === "agent")?.parts
39352
+ );
39353
+ if (result.taskState === "input-required" && pending) {
39354
+ emitControl(opts, {
39355
+ op: "ask",
39356
+ chatId,
39357
+ text: result.text.trim(),
39358
+ questions: pending.questions.map((q) => ({
39359
+ question: q.question,
39360
+ options: q.options.map(
39361
+ (o) => o.description ? { label: o.label, description: o.description } : { label: o.label }
39362
+ )
39363
+ }))
39364
+ });
39365
+ return;
39366
+ }
39367
+ const reply = outboundReplyFromTurn(result, {
39368
+ inputRequired: "Sideboard is waiting for an answer.",
39369
+ canceled: REMOTE_STOPPED_REPLY,
39370
+ completedEmpty: () => "",
39371
+ failed: (detail) => detail ? `Sideboard failed: ${detail}` : "Sideboard failed before producing a result."
39372
+ });
39373
+ emitControl(opts, { op: "assistant", chatId, text: reply.trim() });
39374
+ }
39375
+ function phoneError(err) {
39376
+ return err instanceof Error ? err.message : String(err);
39377
+ }
39378
+ function deliverPhoneTurn(opts, chatId) {
39379
+ const thread = readPhoneThread(chatId);
39380
+ if (!thread || thread.status !== "running" && thread.status !== "queued") return;
39381
+ const generation = bumpChat(chatId);
39382
+ enqueueChat(chatId, async () => {
39383
+ if (chatGeneration.get(chatId) !== generation) return;
39384
+ try {
39385
+ await getOrchestrator().waitForTurn(chatId, 14 * 60 * 1e3);
39386
+ } catch (err) {
39387
+ if (chatGeneration.get(chatId) !== generation) return;
39388
+ emitControl(opts, { op: "error", chatId, message: `Sideboard failed: ${phoneError(err)}` });
39389
+ return;
39390
+ }
39391
+ if (chatGeneration.get(chatId) !== generation) return;
39392
+ emitChatTurn(opts, chatId, chatId);
39393
+ });
39394
+ }
39395
+ function applyPhoneOptions(chatId, draft) {
39396
+ const patch = {};
39397
+ if (draft.agent) patch.agent = draft.agent;
39398
+ if ("model" in draft) patch.model = draft.model ?? null;
39399
+ if (draft.effort) patch.effort = draft.effort;
39400
+ if (draft.fast !== void 0) patch.fast = draft.fast;
39401
+ if (draft.planMode !== void 0) patch.planMode = draft.planMode;
39402
+ if (draft.autonomy) patch.autonomy = draft.autonomy;
39403
+ if (Object.keys(patch).length === 0) return;
39404
+ getOrchestrator().setThreadOptions(chatId, patch);
39405
+ }
39406
+ function stagePhoneTurnFiles(worktreePath, draft) {
39407
+ const links = phoneTurnAttachments({ links: draft.links });
39408
+ const files = stageBuffersAsAttachments(
39409
+ worktreePath,
39410
+ (draft.files ?? []).map((file) => ({ name: file.name, dataBase64: file.dataBase64 }))
39411
+ );
39412
+ return [...links, ...files];
39413
+ }
39414
+ function handlePhoneControl(cmd, opts) {
39415
+ const log = opts.onLog ?? (() => void 0);
39416
+ if (cmd.op === "list") {
39417
+ emitControl(opts, { op: "sidebar", ...listPhoneSidebar() });
39418
+ return;
39419
+ }
39420
+ if (cmd.op === "history") {
39421
+ const page = listPhoneHistory({ query: cmd.query, after: cmd.after });
39422
+ emitControl(opts, {
39423
+ op: "history",
39424
+ chats: page.chats,
39425
+ total: page.total,
39426
+ ...page.next ? { next: page.next } : {},
39427
+ ...cmd.query ? { query: cmd.query } : {},
39428
+ ...cmd.after ? { after: cmd.after } : {}
39429
+ });
39430
+ return;
39431
+ }
39432
+ if (cmd.op === "sources") {
39433
+ const repoPath = cmd.repoPath;
39434
+ const query = cmd.query;
39435
+ void listPhoneSources(repoPath, query).then(
39436
+ (page) => emitControl(opts, {
39437
+ op: "sources",
39438
+ repoPath,
39439
+ ...query ? { query } : {},
39440
+ ...page
39441
+ })
39442
+ ).catch((err) => {
39443
+ emitControl(opts, { op: "error", message: phoneError(err) });
39444
+ });
39445
+ return;
39446
+ }
39447
+ if (cmd.op === "models") {
39448
+ const agent = cmd.agent;
39449
+ void listPhoneModels(agent).then((models) => emitControl(opts, { op: "models", agent, models })).catch((err) => {
39450
+ emitControl(opts, { op: "error", message: phoneError(err) });
39451
+ });
39452
+ return;
39453
+ }
39454
+ if (cmd.op === "dictate") {
39455
+ const id = cmd.id;
39456
+ const transcribe = opts.transcribeWav;
39457
+ if (!transcribe) {
39458
+ emitControl(opts, { op: "error", message: "Speech is not available on this Mac." });
39459
+ return;
39460
+ }
39461
+ void transcribe(cmd.audioBase64).then((text6) => emitControl(opts, { op: "dictated", id, text: text6.trim() })).catch((err) => {
39462
+ emitControl(opts, { op: "error", message: phoneError(err) });
39463
+ });
39464
+ return;
39465
+ }
39466
+ if (cmd.op === "options") {
39467
+ const thread = readPhoneThread(cmd.chatId);
39468
+ if (!thread) {
39469
+ emitControl(opts, { op: "error", chatId: cmd.chatId, message: "That agent is not on this Mac." });
39470
+ return;
39471
+ }
39472
+ try {
39473
+ applyPhoneOptions(thread.id, cmd);
39474
+ const fresh = readPhoneThread(thread.id) ?? thread;
39475
+ emitControl(opts, { op: "options", chatId: fresh.id, ...phoneComposerOptions(fresh) });
39476
+ } catch (err) {
39477
+ emitControl(opts, { op: "error", chatId: cmd.chatId, message: phoneError(err) });
39478
+ }
39479
+ return;
39480
+ }
39481
+ if (cmd.op === "create") {
39482
+ if (cmd.where === "worktree") {
39483
+ void (async () => {
39484
+ try {
39485
+ const opened = createPhoneWorktreeAgent(cmd.chatId, cmd);
39486
+ const prompt = cmd.prompt?.trim() || (cmd.files?.length || cmd.links?.length ? "See the attached files." : "");
39487
+ if (prompt) {
39488
+ try {
39489
+ await getOrchestrator().send(opened.chat.id, formatRemotePrompt(prompt));
39490
+ } catch (err) {
39491
+ emitControl(opts, { op: "error", chatId: opened.chat.id, message: phoneError(err) });
39492
+ }
39493
+ }
39494
+ const fresh = openPhoneChat(opened.chat.id) ?? opened;
39495
+ emitControl(opts, { op: "opened", ...fresh });
39496
+ if (prompt) deliverPhoneTurn(opts, fresh.chat.id);
39497
+ } catch (err) {
39498
+ emitControl(opts, { op: "error", message: phoneError(err) });
39499
+ }
39500
+ })();
39501
+ return;
39502
+ }
39503
+ if (cmd.where === "project") {
39504
+ void createPhoneProjectWorktree(cmd).then(async (opened) => {
39505
+ const prompt = resolveCreateFirstPrompt({
39506
+ sourceType: cmd.sourceType ?? "branch",
39507
+ prompt: cmd.prompt
39508
+ }) || (cmd.files?.length || cmd.links?.length ? "See the attached files." : "");
39509
+ if (prompt) {
39510
+ try {
39511
+ await getOrchestrator().send(opened.chat.id, formatRemotePrompt(prompt));
39512
+ } catch (err) {
39513
+ emitControl(opts, { op: "error", chatId: opened.chat.id, message: phoneError(err) });
39514
+ }
39515
+ }
39516
+ const fresh = openPhoneChat(opened.chat.id) ?? opened;
39517
+ emitControl(opts, { op: "opened", ...fresh });
39518
+ if (prompt) deliverPhoneTurn(opts, fresh.chat.id);
39519
+ }).catch((err) => {
39520
+ emitControl(opts, { op: "error", message: phoneError(err) });
39521
+ });
39522
+ return;
39523
+ }
39524
+ const goal = cmd.goal?.trim() || cmd.prompt?.trim() || "";
39525
+ if (goal || cmd.files?.length || cmd.links?.length) {
39526
+ void createPhoneOrchestration({ ...cmd, goal }).then((opened) => {
39527
+ emitControl(opts, { op: "opened", ...opened });
39528
+ deliverPhoneTurn(opts, opened.chat.id);
39529
+ }).catch((err) => {
39530
+ emitControl(opts, { op: "error", message: phoneError(err) });
39531
+ });
39532
+ return;
39533
+ }
39534
+ try {
39535
+ const agent = coerceOrchestratorAgent(
39536
+ cmd.agent ?? opts.agent ?? resolveOrchestratorDefaults().agent
39537
+ );
39538
+ emitControl(opts, { op: "opened", ...createPhoneChat(agent) });
39539
+ } catch (err) {
39540
+ emitControl(opts, { op: "error", message: phoneError(err) });
39541
+ }
39542
+ return;
39543
+ }
39544
+ if (cmd.op === "open") {
39545
+ const opened = openPhoneChat(cmd.chatId);
39546
+ if (!opened) {
39547
+ emitControl(opts, { op: "error", message: "That agent is not on this Mac." });
39548
+ return;
39549
+ }
39550
+ emitControl(opts, { op: "opened", ...opened });
39551
+ return;
39552
+ }
39553
+ if (cmd.op === "archive") {
39554
+ const thread = readPhoneThread(cmd.chatId);
39555
+ if (!thread) {
39556
+ emitControl(opts, { op: "error", message: "That agent is not on this Mac." });
39557
+ return;
39558
+ }
39559
+ bumpChat(thread.id);
39560
+ interruptChat(thread.id, log);
39561
+ archivePhoneChat(thread.id);
39562
+ emitControl(opts, { op: "sidebar", ...listPhoneSidebar() });
39563
+ return;
39564
+ }
39565
+ if (cmd.op === "restore") {
39566
+ const chatId2 = cmd.chatId;
39567
+ void getOrchestrator().restore(chatId2).then(() => {
39568
+ emitControl(opts, { op: "sidebar", ...listPhoneSidebar() });
39569
+ emitControl(opts, { op: "restored", chatId: chatId2 });
39570
+ }).catch((err) => {
39571
+ emitControl(opts, {
39572
+ op: "error",
39573
+ chatId: chatId2,
39574
+ message: err instanceof Error ? err.message : String(err)
39575
+ });
39576
+ });
39577
+ return;
39578
+ }
39579
+ if (cmd.op === "stop") {
39580
+ if (!readPhoneThread(cmd.chatId)) {
39581
+ emitControl(opts, { op: "error", chatId: cmd.chatId, message: "That agent is not on this Mac." });
39582
+ return;
39583
+ }
39584
+ bumpChat(cmd.chatId);
39585
+ interruptChat(cmd.chatId, log);
39586
+ emitControl(opts, { op: "stopped", chatId: cmd.chatId });
39587
+ return;
39588
+ }
39589
+ const chatId = cmd.chatId;
39590
+ const generation = bumpChat(chatId);
39591
+ interruptChat(chatId, log);
39592
+ enqueueChat(chatId, async () => {
39593
+ if (chatGeneration.get(chatId) !== generation) return;
39594
+ const thread = readPhoneThread(chatId);
39595
+ if (!thread) {
39596
+ emitControl(opts, { op: "error", chatId, message: "That agent is not on this Mac." });
39597
+ return;
39598
+ }
39599
+ if (thread.status === "running" || thread.status === "queued") {
39600
+ try {
39601
+ getOrchestrator().stop(thread.id, { clearQueue: true });
39602
+ } catch {
39603
+ }
39604
+ }
39605
+ if (chatGeneration.get(chatId) !== generation) return;
39606
+ try {
39607
+ try {
39608
+ applyPhoneOptions(thread.id, cmd);
39609
+ } catch (err) {
39610
+ emitControl(opts, { op: "error", chatId, message: phoneError(err) });
39611
+ }
39612
+ const live = readPhoneThread(thread.id) ?? thread;
39613
+ const staged = stagePhoneTurnFiles(live.worktreePath, cmd);
39614
+ const text6 = cmd.text.trim() || (staged.length ? "See the attached files." : "");
39615
+ const formatted = formatRemotePrompt(text6);
39616
+ await getOrchestrator().send(thread.id, formatted, { attachments: staged });
39617
+ await getOrchestrator().waitForTurn(thread.id, 14 * 60 * 1e3);
39618
+ } catch (err) {
39619
+ if (chatGeneration.get(chatId) !== generation) return;
39620
+ const message = err instanceof Error ? err.message : String(err);
39621
+ emitControl(opts, { op: "error", chatId, message: `Sideboard failed: ${message}` });
39622
+ return;
39623
+ }
39624
+ if (chatGeneration.get(chatId) !== generation) return;
39625
+ emitChatTurn(opts, chatId, thread.id);
39626
+ });
39627
+ }
38792
39628
  function handleRemoteInbound(text6, opts) {
38793
- const generation = ++inboundGeneration;
38794
39629
  const log = opts.onLog ?? (() => void 0);
38795
39630
  const body = text6.trim();
39631
+ const control = takePhoneControl(body);
39632
+ if (control) {
39633
+ if (control === "invalid") {
39634
+ emitControl(opts, { op: "error", message: "invalid phone command" });
39635
+ return;
39636
+ }
39637
+ handlePhoneControl(control, opts);
39638
+ return;
39639
+ }
39640
+ const generation = ++inboundGeneration;
38796
39641
  if (body) interruptRemoteCoordinator(opts.deviceId, log);
38797
39642
  enqueue(async () => {
38798
39643
  if (generation !== inboundGeneration) return;
@@ -38907,7 +39752,8 @@ function runRemoteHost(opts) {
38907
39752
  type: "host_register",
38908
39753
  deviceId: opts.deviceId,
38909
39754
  deviceLabel,
38910
- hostSecret: opts.hostSecret
39755
+ hostSecret: opts.hostSecret,
39756
+ ...opts.accountToken?.trim() ? { accountToken: opts.accountToken.trim() } : {}
38911
39757
  });
38912
39758
  };
38913
39759
  const loop = async () => {
@@ -39023,7 +39869,8 @@ function runRemoteHost(opts) {
39023
39869
  handleRemoteInbound(msg.text, {
39024
39870
  deviceId: opts.deviceId,
39025
39871
  onLog: log,
39026
- onOutbound: emit
39872
+ onOutbound: emit,
39873
+ transcribeWav: opts.transcribeWav
39027
39874
  });
39028
39875
  }
39029
39876
  };
@@ -39050,6 +39897,606 @@ function runRemoteHost(opts) {
39050
39897
  };
39051
39898
  }
39052
39899
 
39900
+ // src/relay/account-login.ts
39901
+ var import_node_crypto27 = require("crypto");
39902
+
39903
+ // src/relay/accounts.ts
39904
+ var import_node_crypto26 = require("crypto");
39905
+ var import_node_fs70 = require("fs");
39906
+ var import_node_module7 = require("module");
39907
+ var import_node_path68 = require("path");
39908
+ var GITHUB_ACCOUNT_ISSUER = "https://github.com";
39909
+ var RELAY_DEVICE_CAP = 3;
39910
+ function thisModuleFile3() {
39911
+ const cjsFile = typeof __filename !== "undefined" ? __filename : "";
39912
+ return cjsFile || process.argv[1] || (0, import_node_path68.join)(process.cwd(), "package.json");
39913
+ }
39914
+ function openDatabase(filePath) {
39915
+ const req = (0, import_node_module7.createRequire)(thisModuleFile3());
39916
+ const specifier = "node:sqlite";
39917
+ const { DatabaseSync: SqliteDatabase } = req(specifier);
39918
+ const db = new SqliteDatabase(filePath);
39919
+ db.exec("PRAGMA journal_mode = WAL");
39920
+ return db;
39921
+ }
39922
+ function sha256(value) {
39923
+ return (0, import_node_crypto26.createHash)("sha256").update(value).digest("hex");
39924
+ }
39925
+ function sameHash(a, b) {
39926
+ const left = Buffer.from(a);
39927
+ const right = Buffer.from(b);
39928
+ return left.length === right.length && (0, import_node_crypto26.timingSafeEqual)(left, right);
39929
+ }
39930
+ function normalizeEmail(email) {
39931
+ const trimmed = email?.trim().toLowerCase() ?? "";
39932
+ return trimmed || null;
39933
+ }
39934
+ function longestMountPoint(dir, mounts) {
39935
+ let best = "/";
39936
+ for (const line of mounts.split("\n")) {
39937
+ const point = line.split(" ")[1]?.replace(/\\040/g, " ");
39938
+ if (!point) continue;
39939
+ if (dir === point || dir.startsWith(point.endsWith("/") ? point : `${point}/`)) {
39940
+ if (point.length > best.length) best = point;
39941
+ }
39942
+ }
39943
+ return best;
39944
+ }
39945
+ function assertRelayAccountsDurable(filePath) {
39946
+ if (process.env.SIDEBOARD_RELAY_ACCOUNTS_EPHEMERAL === "1") return;
39947
+ if (process.env.NODE_ENV !== "production") return;
39948
+ if (process.platform !== "linux") return;
39949
+ const dir = (0, import_node_fs70.realpathSync)((0, import_node_path68.dirname)(filePath));
39950
+ const mounts = (0, import_node_fs70.readFileSync)("/proc/mounts", "utf8");
39951
+ const point = longestMountPoint(dir, mounts);
39952
+ if (point === "/") {
39953
+ throw new Error(
39954
+ `Relay accounts at ${filePath} are on the machine root filesystem. Mount a Fly volume so sign-ins survive deploys.`
39955
+ );
39956
+ }
39957
+ }
39958
+ var RelayAccountStore = class {
39959
+ db;
39960
+ constructor(filePath) {
39961
+ (0, import_node_fs70.mkdirSync)((0, import_node_path68.dirname)(filePath), { recursive: true });
39962
+ this.db = openDatabase(filePath);
39963
+ this.db.exec(`
39964
+ CREATE TABLE IF NOT EXISTS accounts (
39965
+ id TEXT PRIMARY KEY,
39966
+ created_at INTEGER NOT NULL,
39967
+ device_cap INTEGER NOT NULL
39968
+ );
39969
+ CREATE TABLE IF NOT EXISTS identities (
39970
+ issuer TEXT NOT NULL,
39971
+ subject TEXT NOT NULL,
39972
+ account_id TEXT NOT NULL,
39973
+ email TEXT,
39974
+ email_verified INTEGER NOT NULL,
39975
+ PRIMARY KEY (issuer, subject)
39976
+ );
39977
+ CREATE INDEX IF NOT EXISTS identities_email ON identities(email);
39978
+ CREATE TABLE IF NOT EXISTS credentials (
39979
+ token_hash TEXT PRIMARY KEY,
39980
+ account_id TEXT NOT NULL,
39981
+ created_at INTEGER NOT NULL
39982
+ );
39983
+ CREATE TABLE IF NOT EXISTS devices (
39984
+ device_id TEXT PRIMARY KEY,
39985
+ account_id TEXT NOT NULL,
39986
+ host_secret_hash TEXT NOT NULL,
39987
+ device_label TEXT NOT NULL,
39988
+ created_at INTEGER NOT NULL
39989
+ );
39990
+ `);
39991
+ }
39992
+ close() {
39993
+ this.db.close();
39994
+ }
39995
+ /**
39996
+ * First sign-in creates an account. The same issuer+subject returns that
39997
+ * account. A verified email match attaches a second provider. `linkAccountId`
39998
+ * attaches while that account’s credential is already signed in.
39999
+ * Each call mints a new Mac credential. Older credentials stay valid.
40000
+ */
40001
+ signIn(identity, linkAccountId) {
40002
+ const issuer = identity.issuer.trim();
40003
+ const subject = identity.subject.trim();
40004
+ if (!issuer || !subject) throw new Error("Sign-in is missing an identity.");
40005
+ const email = normalizeEmail(identity.email);
40006
+ const verified = Boolean(identity.emailVerified && email);
40007
+ const link = linkAccountId?.trim() || null;
40008
+ this.db.exec("BEGIN");
40009
+ try {
40010
+ const existing = this.db.prepare("SELECT account_id, email FROM identities WHERE issuer = ? AND subject = ?").get(issuer, subject);
40011
+ let accountId;
40012
+ if (existing) {
40013
+ if (link && link !== existing.account_id) {
40014
+ throw new Error("That sign-in already belongs to another Sideboard account.");
40015
+ }
40016
+ accountId = existing.account_id;
40017
+ this.requireAccount(accountId);
40018
+ if (email) {
40019
+ this.db.prepare("UPDATE identities SET email = ?, email_verified = ? WHERE issuer = ? AND subject = ?").run(email, verified ? 1 : 0, issuer, subject);
40020
+ }
40021
+ } else if (link) {
40022
+ this.requireAccount(link);
40023
+ accountId = link;
40024
+ this.insertIdentity(issuer, subject, accountId, email, verified);
40025
+ } else {
40026
+ const byEmail = verified ? this.accountIdForVerifiedEmail(email) : void 0;
40027
+ accountId = byEmail ?? (0, import_node_crypto26.randomUUID)();
40028
+ if (!byEmail) {
40029
+ this.db.prepare("INSERT INTO accounts (id, created_at, device_cap) VALUES (?, ?, ?)").run(accountId, Date.now(), RELAY_DEVICE_CAP);
40030
+ }
40031
+ this.insertIdentity(issuer, subject, accountId, email, verified);
40032
+ }
40033
+ const credential = this.mintCredential(accountId);
40034
+ const stored = this.db.prepare("SELECT email FROM identities WHERE account_id = ? AND email IS NOT NULL ORDER BY email_verified DESC LIMIT 1").get(accountId);
40035
+ this.db.exec("COMMIT");
40036
+ return { accountId, credential, email: stored?.email ?? email };
40037
+ } catch (err) {
40038
+ this.db.exec("ROLLBACK");
40039
+ throw err;
40040
+ }
40041
+ }
40042
+ verifyCredential(token) {
40043
+ const trimmed = token.trim();
40044
+ if (!trimmed) return null;
40045
+ const row = this.db.prepare("SELECT account_id FROM credentials WHERE token_hash = ?").get(sha256(trimmed));
40046
+ return row?.account_id ?? null;
40047
+ }
40048
+ /** Drop one Mac credential. The account and its other Macs stay. */
40049
+ logout(token) {
40050
+ const trimmed = token.trim();
40051
+ if (!trimmed) return;
40052
+ this.db.prepare("DELETE FROM credentials WHERE token_hash = ?").run(sha256(trimmed));
40053
+ }
40054
+ /** Delete the account and every identity, credential, and Mac bound to it. */
40055
+ revokeAccount(accountId) {
40056
+ const id = accountId.trim();
40057
+ if (!id) return;
40058
+ this.db.exec("BEGIN");
40059
+ try {
40060
+ this.db.prepare("DELETE FROM devices WHERE account_id = ?").run(id);
40061
+ this.db.prepare("DELETE FROM credentials WHERE account_id = ?").run(id);
40062
+ this.db.prepare("DELETE FROM identities WHERE account_id = ?").run(id);
40063
+ this.db.prepare("DELETE FROM accounts WHERE id = ?").run(id);
40064
+ this.db.exec("COMMIT");
40065
+ } catch (err) {
40066
+ this.db.exec("ROLLBACK");
40067
+ throw err;
40068
+ }
40069
+ }
40070
+ /**
40071
+ * Accept `host_register` for a signed-in account. The device row survives
40072
+ * process restart, so a later register with a different host secret is rejected
40073
+ * and a new device id counts against the cap.
40074
+ */
40075
+ authorizeHost(input) {
40076
+ const deviceId = input.deviceId.trim();
40077
+ const hostSecret = input.hostSecret.trim();
40078
+ const deviceLabel = input.deviceLabel.trim() || "This Mac";
40079
+ if (!deviceId || !hostSecret) return { ok: false, message: "sign in required" };
40080
+ const accountId = this.verifyCredential(input.accountToken);
40081
+ if (!accountId) return { ok: false, message: "sign in required" };
40082
+ this.db.exec("BEGIN");
40083
+ try {
40084
+ const account = this.db.prepare("SELECT id, device_cap FROM accounts WHERE id = ?").get(accountId);
40085
+ if (!account) {
40086
+ this.db.exec("ROLLBACK");
40087
+ return { ok: false, message: "sign in required" };
40088
+ }
40089
+ const device = this.db.prepare("SELECT device_id, account_id, host_secret_hash FROM devices WHERE device_id = ?").get(deviceId);
40090
+ const secretHash = sha256(hostSecret);
40091
+ if (!device) {
40092
+ const count = this.db.prepare("SELECT COUNT(*) AS n FROM devices WHERE account_id = ?").get(accountId);
40093
+ if (count.n >= account.device_cap) {
40094
+ this.db.exec("ROLLBACK");
40095
+ return { ok: false, message: "This account already has the maximum number of Macs." };
40096
+ }
40097
+ this.db.prepare(
40098
+ "INSERT INTO devices (device_id, account_id, host_secret_hash, device_label, created_at) VALUES (?, ?, ?, ?, ?)"
40099
+ ).run(deviceId, accountId, secretHash, deviceLabel, Date.now());
40100
+ this.db.exec("COMMIT");
40101
+ return { ok: true, accountId };
40102
+ }
40103
+ if (device.account_id !== accountId) {
40104
+ this.db.exec("ROLLBACK");
40105
+ return { ok: false, message: "This Mac is registered to another account." };
40106
+ }
40107
+ if (!sameHash(device.host_secret_hash, secretHash)) {
40108
+ this.db.exec("ROLLBACK");
40109
+ return { ok: false, message: "host secret rejected" };
40110
+ }
40111
+ this.db.prepare("UPDATE devices SET device_label = ? WHERE device_id = ?").run(deviceLabel, deviceId);
40112
+ this.db.exec("COMMIT");
40113
+ return { ok: true, accountId };
40114
+ } catch (err) {
40115
+ this.db.exec("ROLLBACK");
40116
+ throw err;
40117
+ }
40118
+ }
40119
+ requireAccount(accountId) {
40120
+ const row = this.db.prepare("SELECT id FROM accounts WHERE id = ?").get(accountId);
40121
+ if (!row) throw new Error("That Sideboard account is no longer on the relay.");
40122
+ }
40123
+ accountIdForVerifiedEmail(email) {
40124
+ const row = this.db.prepare(
40125
+ "SELECT account_id FROM identities WHERE email_verified = 1 AND email = ? LIMIT 1"
40126
+ ).get(email);
40127
+ return row?.account_id;
40128
+ }
40129
+ insertIdentity(issuer, subject, accountId, email, verified) {
40130
+ this.db.prepare(
40131
+ "INSERT INTO identities (issuer, subject, account_id, email, email_verified) VALUES (?, ?, ?, ?, ?)"
40132
+ ).run(issuer, subject, accountId, email, verified ? 1 : 0);
40133
+ }
40134
+ mintCredential(accountId) {
40135
+ const credential = (0, import_node_crypto26.randomBytes)(32).toString("hex");
40136
+ this.db.prepare("INSERT INTO credentials (token_hash, account_id, created_at) VALUES (?, ?, ?)").run(sha256(credential), accountId, Date.now());
40137
+ return credential;
40138
+ }
40139
+ };
40140
+
40141
+ // src/relay/account-oauth.ts
40142
+ var RELAY_GIT_HOSTS = ["github"];
40143
+ var REMOTE_LOGIN_PATH = "/remote/login";
40144
+ var REMOTE_OAUTH_CALLBACK_PATH = "/remote/oauth/callback";
40145
+ var REMOTE_OAUTH_RESULT_PATH = "/remote/oauth/result";
40146
+ var REMOTE_LOGOUT_PATH = "/remote/logout";
40147
+ var STATE_RE = /^[A-Za-z0-9_-]{16,128}$/;
40148
+ var LOGIN_TTL_MS = 10 * 60 * 1e3;
40149
+ var RelayLoginSessions = class {
40150
+ waiting = /* @__PURE__ */ new Map();
40151
+ results = /* @__PURE__ */ new Map();
40152
+ begin(state, provider, accountId) {
40153
+ this.sweep();
40154
+ this.waiting.set(state, { provider, accountId, expiresAt: Date.now() + LOGIN_TTL_MS });
40155
+ }
40156
+ takeLogin(state) {
40157
+ this.sweep();
40158
+ const row = this.waiting.get(state);
40159
+ if (!row) return null;
40160
+ this.waiting.delete(state);
40161
+ if (row.expiresAt < Date.now()) return null;
40162
+ return row;
40163
+ }
40164
+ putResult(state, value) {
40165
+ this.results.set(state, { value, expiresAt: Date.now() + LOGIN_TTL_MS });
40166
+ }
40167
+ /** `undefined` means the browser flow has not finished. */
40168
+ takeResult(state) {
40169
+ this.sweep();
40170
+ const row = this.results.get(state);
40171
+ if (!row) return this.waiting.has(state) ? void 0 : null;
40172
+ this.results.delete(state);
40173
+ if (row.expiresAt < Date.now()) return null;
40174
+ return row.value;
40175
+ }
40176
+ sweep() {
40177
+ const now = Date.now();
40178
+ for (const [state, row] of this.waiting) {
40179
+ if (row.expiresAt < now) this.waiting.delete(state);
40180
+ }
40181
+ for (const [state, row] of this.results) {
40182
+ if (row.expiresAt < now) this.results.delete(state);
40183
+ }
40184
+ }
40185
+ };
40186
+ var GIT_HOST_AUTH = {
40187
+ github: {
40188
+ label: "GitHub",
40189
+ authorizeUrl: "https://github.com/login/oauth/authorize",
40190
+ scope: "user:email",
40191
+ fetchIdentity: fetchGithubIdentity
40192
+ }
40193
+ };
40194
+ function providerOf(value) {
40195
+ return RELAY_GIT_HOSTS.includes(value) ? value : null;
40196
+ }
40197
+ function relayAuthorizeUrl(provider, client, redirectUri, state) {
40198
+ const host = GIT_HOST_AUTH[provider];
40199
+ const url = new URL(host.authorizeUrl);
40200
+ url.searchParams.set("client_id", client.clientId);
40201
+ url.searchParams.set("redirect_uri", redirectUri);
40202
+ url.searchParams.set("scope", host.scope);
40203
+ url.searchParams.set("state", state);
40204
+ return url.toString();
40205
+ }
40206
+ async function readJson2(res) {
40207
+ const text6 = await res.text();
40208
+ if (!text6) return {};
40209
+ try {
40210
+ return JSON.parse(text6);
40211
+ } catch {
40212
+ throw new Error("Sign-in provider returned a non-JSON response.");
40213
+ }
40214
+ }
40215
+ function accessToken(body) {
40216
+ const token = body.access_token;
40217
+ if (typeof token !== "string" || !token.trim()) {
40218
+ const message = body.error_description;
40219
+ throw new Error(typeof message === "string" && message.trim() ? message : "Sign-in provider rejected the code.");
40220
+ }
40221
+ return token.trim();
40222
+ }
40223
+ async function fetchGithubIdentity(client, code, redirectUri, fetchImpl) {
40224
+ const tokenRes = await fetchImpl("https://github.com/login/oauth/access_token", {
40225
+ method: "POST",
40226
+ headers: { Accept: "application/json", "Content-Type": "application/json" },
40227
+ body: JSON.stringify({
40228
+ client_id: client.clientId,
40229
+ client_secret: client.clientSecret,
40230
+ code,
40231
+ redirect_uri: redirectUri
40232
+ })
40233
+ });
40234
+ const token = accessToken(await readJson2(tokenRes));
40235
+ const userRes = await fetchImpl("https://api.github.com/user", {
40236
+ headers: { Authorization: `Bearer ${token}`, Accept: "application/vnd.github+json", "User-Agent": "sideboard-relay" }
40237
+ });
40238
+ const user = await readJson2(userRes);
40239
+ const subject = typeof user.id === "number" ? String(user.id) : "";
40240
+ if (!subject) throw new Error("GitHub did not return an account id.");
40241
+ const emailRes = await fetchImpl("https://api.github.com/user/emails", {
40242
+ headers: { Authorization: `Bearer ${token}`, Accept: "application/vnd.github+json", "User-Agent": "sideboard-relay" }
40243
+ });
40244
+ const emails = await readJson2(emailRes);
40245
+ const list = Array.isArray(emails) ? emails : [];
40246
+ const primary = list.find((row) => row.primary === true && row.verified === true);
40247
+ const anyVerified = list.find((row) => row.verified === true);
40248
+ const chosen = primary ?? anyVerified;
40249
+ const email = chosen && typeof chosen.email === "string" ? chosen.email : null;
40250
+ return { issuer: GITHUB_ACCOUNT_ISSUER, subject, email, emailVerified: Boolean(email) };
40251
+ }
40252
+ async function fetchRelayIdentity(provider, client, code, redirectUri, fetchImpl) {
40253
+ return GIT_HOST_AUTH[provider].fetchIdentity(client, code, redirectUri, fetchImpl);
40254
+ }
40255
+ async function readBody(req) {
40256
+ const chunks = [];
40257
+ let size = 0;
40258
+ for await (const chunk of req) {
40259
+ const buf = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
40260
+ size += buf.length;
40261
+ if (size > 8192) throw new Error("body too large");
40262
+ chunks.push(buf);
40263
+ }
40264
+ const text6 = Buffer.concat(chunks).toString("utf8").trim();
40265
+ if (!text6) return {};
40266
+ return JSON.parse(text6);
40267
+ }
40268
+ function sendJson2(res, status, body) {
40269
+ res.writeHead(status, { "Content-Type": "application/json", "Cache-Control": "no-store" });
40270
+ res.end(JSON.stringify(body));
40271
+ }
40272
+ function sendHtml(res, status, title, body) {
40273
+ res.writeHead(status, { "Content-Type": "text/html; charset=utf-8", "Cache-Control": "no-store" });
40274
+ res.end(`<!DOCTYPE html><html><head><meta charset="utf-8"><title>${escapeHtml2(title)}</title></head><body>${body}</body></html>`);
40275
+ }
40276
+ async function handleRelayAccountHttp(opts) {
40277
+ const reqUrl = opts.req.url || "/";
40278
+ const path2 = reqUrl.split("?")[0];
40279
+ const method = opts.req.method || "GET";
40280
+ const origin = opts.publicOrigin.replace(/\/+$/, "");
40281
+ const redirectUri = `${origin}${REMOTE_OAUTH_CALLBACK_PATH}`;
40282
+ const fetchImpl = opts.fetchImpl ?? fetch;
40283
+ if (method === "POST" && path2 === REMOTE_LOGIN_PATH) {
40284
+ let body;
40285
+ try {
40286
+ body = await readBody(opts.req);
40287
+ } catch {
40288
+ sendJson2(opts.res, 400, { ok: false, error: "invalid login request" });
40289
+ return true;
40290
+ }
40291
+ const provider = providerOf(typeof body.provider === "string" ? body.provider : "");
40292
+ const state = typeof body.state === "string" ? body.state.trim() : "";
40293
+ if (!provider || !STATE_RE.test(state)) {
40294
+ sendJson2(opts.res, 400, { ok: false, error: "invalid login request" });
40295
+ return true;
40296
+ }
40297
+ const client = opts.oauth[provider];
40298
+ if (!client?.clientId || !client.clientSecret) {
40299
+ sendJson2(opts.res, 503, {
40300
+ ok: false,
40301
+ error: `${GIT_HOST_AUTH[provider].label} sign-in is not configured on this relay.`
40302
+ });
40303
+ return true;
40304
+ }
40305
+ let accountId = null;
40306
+ const linkToken = typeof body.accountToken === "string" ? body.accountToken.trim() : "";
40307
+ if (linkToken) {
40308
+ accountId = opts.store.verifyCredential(linkToken);
40309
+ if (!accountId) {
40310
+ sendJson2(opts.res, 401, { ok: false, error: "sign in required" });
40311
+ return true;
40312
+ }
40313
+ }
40314
+ opts.sessions.begin(state, provider, accountId);
40315
+ sendJson2(opts.res, 200, { ok: true, url: relayAuthorizeUrl(provider, client, redirectUri, state) });
40316
+ return true;
40317
+ }
40318
+ if (method === "GET" && path2 === REMOTE_OAUTH_CALLBACK_PATH) {
40319
+ const url = new URL(reqUrl, origin);
40320
+ const state = url.searchParams.get("state")?.trim() ?? "";
40321
+ const code = url.searchParams.get("code")?.trim() ?? "";
40322
+ const providerError = url.searchParams.get("error")?.trim() ?? "";
40323
+ const pending = STATE_RE.test(state) ? opts.sessions.takeLogin(state) : null;
40324
+ if (!pending) {
40325
+ sendHtml(opts.res, 400, "Sideboard", "<h1>Sign-in expired</h1><p>Return to Sideboard and try again.</p>");
40326
+ return true;
40327
+ }
40328
+ if (providerError || !code) {
40329
+ const message = providerError || "Sign-in was cancelled.";
40330
+ opts.sessions.putResult(state, { ok: false, error: message });
40331
+ sendHtml(opts.res, 400, "Sideboard", `<h1>Could not sign in</h1><p>${escapeHtml2(message)}</p>`);
40332
+ return true;
40333
+ }
40334
+ const client = opts.oauth[pending.provider];
40335
+ if (!client) {
40336
+ opts.sessions.putResult(state, { ok: false, error: "Sign-in is not configured on this relay." });
40337
+ sendHtml(opts.res, 503, "Sideboard", "<h1>Sign-in is not configured</h1>");
40338
+ return true;
40339
+ }
40340
+ try {
40341
+ const identity = await fetchRelayIdentity(pending.provider, client, code, redirectUri, fetchImpl);
40342
+ const signed = opts.store.signIn(identity, pending.accountId);
40343
+ opts.sessions.putResult(state, {
40344
+ ok: true,
40345
+ credential: signed.credential,
40346
+ email: signed.email,
40347
+ accountId: signed.accountId
40348
+ });
40349
+ sendHtml(
40350
+ opts.res,
40351
+ 200,
40352
+ "Sideboard",
40353
+ "<h1>Signed in</h1><p>Return to Sideboard. This window can close.</p>"
40354
+ );
40355
+ } catch (err) {
40356
+ const message = err instanceof Error ? err.message : String(err);
40357
+ opts.sessions.putResult(state, { ok: false, error: message });
40358
+ sendHtml(opts.res, 400, "Sideboard", `<h1>Could not sign in</h1><p>${escapeHtml2(message)}</p>`);
40359
+ }
40360
+ return true;
40361
+ }
40362
+ if (method === "GET" && path2 === REMOTE_OAUTH_RESULT_PATH) {
40363
+ const url = new URL(reqUrl, origin);
40364
+ const state = url.searchParams.get("state")?.trim() ?? "";
40365
+ if (!STATE_RE.test(state)) {
40366
+ sendJson2(opts.res, 400, { ok: false, error: "invalid login request" });
40367
+ return true;
40368
+ }
40369
+ const result = opts.sessions.takeResult(state);
40370
+ if (result === void 0 || result === null) {
40371
+ sendJson2(opts.res, 404, { ok: false, error: "pending" });
40372
+ return true;
40373
+ }
40374
+ if (!result.ok) {
40375
+ sendJson2(opts.res, 400, { ok: false, error: result.error });
40376
+ return true;
40377
+ }
40378
+ sendJson2(opts.res, 200, {
40379
+ ok: true,
40380
+ credential: result.credential,
40381
+ email: result.email,
40382
+ accountId: result.accountId
40383
+ });
40384
+ return true;
40385
+ }
40386
+ if (method === "POST" && path2 === REMOTE_LOGOUT_PATH) {
40387
+ let body;
40388
+ try {
40389
+ body = await readBody(opts.req);
40390
+ } catch {
40391
+ sendJson2(opts.res, 400, { ok: false, error: "invalid logout request" });
40392
+ return true;
40393
+ }
40394
+ const token = typeof body.accountToken === "string" ? body.accountToken : "";
40395
+ opts.store.logout(token);
40396
+ sendJson2(opts.res, 200, { ok: true });
40397
+ return true;
40398
+ }
40399
+ return false;
40400
+ }
40401
+
40402
+ // src/relay/account-login.ts
40403
+ var RemoteAccountLoginCancelled = class extends Error {
40404
+ constructor() {
40405
+ super("Sign-in cancelled");
40406
+ this.name = "RemoteAccountLoginCancelled";
40407
+ }
40408
+ };
40409
+ function isRemoteAccountLoginCancelled(err) {
40410
+ return err instanceof RemoteAccountLoginCancelled;
40411
+ }
40412
+ function remoteAccountHttpOrigin(wsUrl = remoteRelayUrl()) {
40413
+ const url = new URL(wsUrl);
40414
+ url.protocol = url.protocol === "wss:" ? "https:" : "http:";
40415
+ url.pathname = "";
40416
+ url.search = "";
40417
+ url.hash = "";
40418
+ return url.origin;
40419
+ }
40420
+ function sleep2(ms, signal) {
40421
+ return new Promise((resolve2, reject) => {
40422
+ if (signal?.aborted) {
40423
+ reject(new RemoteAccountLoginCancelled());
40424
+ return;
40425
+ }
40426
+ const timer = setTimeout(() => {
40427
+ signal?.removeEventListener("abort", onAbort);
40428
+ resolve2();
40429
+ }, ms);
40430
+ const onAbort = () => {
40431
+ clearTimeout(timer);
40432
+ reject(new RemoteAccountLoginCancelled());
40433
+ };
40434
+ signal?.addEventListener("abort", onAbort, { once: true });
40435
+ });
40436
+ }
40437
+ async function startRemoteAccountLogin(opts) {
40438
+ if (opts.signal?.aborted) throw new RemoteAccountLoginCancelled();
40439
+ const origin = (opts.origin ?? remoteAccountHttpOrigin()).replace(/\/+$/, "");
40440
+ const fetchImpl = opts.fetchImpl ?? fetch;
40441
+ const state = (0, import_node_crypto27.randomBytes)(16).toString("hex");
40442
+ const timeoutMs = opts.timeoutMs ?? 5 * 6e4;
40443
+ const pollIntervalMs = opts.pollIntervalMs ?? 400;
40444
+ const linkToken = opts.linkToken?.trim() ?? "";
40445
+ const started = await fetchImpl(`${origin}${REMOTE_LOGIN_PATH}`, {
40446
+ method: "POST",
40447
+ headers: { "Content-Type": "application/json" },
40448
+ body: JSON.stringify({
40449
+ provider: opts.provider,
40450
+ state,
40451
+ ...linkToken ? { accountToken: linkToken } : {}
40452
+ })
40453
+ });
40454
+ const startBody = await started.json();
40455
+ if (!started.ok || !startBody.url) {
40456
+ throw new Error(startBody.error || "Could not start sign-in.");
40457
+ }
40458
+ await Promise.resolve(opts.openUrl?.(startBody.url));
40459
+ const deadline = Date.now() + timeoutMs;
40460
+ const resultUrl = `${origin}${REMOTE_OAUTH_RESULT_PATH}?state=${encodeURIComponent(state)}`;
40461
+ while (Date.now() < deadline) {
40462
+ if (opts.signal?.aborted) throw new RemoteAccountLoginCancelled();
40463
+ let res;
40464
+ try {
40465
+ res = await fetchImpl(resultUrl);
40466
+ } catch {
40467
+ if (opts.signal?.aborted) throw new RemoteAccountLoginCancelled();
40468
+ await sleep2(pollIntervalMs, opts.signal);
40469
+ continue;
40470
+ }
40471
+ let data;
40472
+ try {
40473
+ data = await res.json();
40474
+ } catch {
40475
+ await sleep2(pollIntervalMs, opts.signal);
40476
+ continue;
40477
+ }
40478
+ if (data.ok && data.credential && data.accountId) {
40479
+ return {
40480
+ credential: data.credential,
40481
+ email: data.email ?? null,
40482
+ accountId: data.accountId
40483
+ };
40484
+ }
40485
+ if (data.error && data.error !== "pending") throw new Error(data.error);
40486
+ await sleep2(pollIntervalMs, opts.signal);
40487
+ }
40488
+ throw new Error("Sign-in timed out.");
40489
+ }
40490
+ async function disconnectRemoteAccount(opts) {
40491
+ const origin = (opts.origin ?? remoteAccountHttpOrigin()).replace(/\/+$/, "");
40492
+ const fetchImpl = opts.fetchImpl ?? fetch;
40493
+ await fetchImpl(`${origin}${REMOTE_LOGOUT_PATH}`, {
40494
+ method: "POST",
40495
+ headers: { "Content-Type": "application/json" },
40496
+ body: JSON.stringify({ accountToken: opts.accountToken })
40497
+ });
40498
+ }
40499
+
39053
40500
  // src/slack/app-home.ts
39054
40501
  var SLACK_APP_HOME_DOWNLOAD = "https://sideboard-downloads.t3.tigrisfiles.io/Sideboard-latest-arm64.dmg";
39055
40502
  var SLACK_APP_HOME_LANDING = "https://www.sideboard.cloud/slack/";
@@ -39431,9 +40878,9 @@ var import_node_http5 = require("http");
39431
40878
  var import_ws6 = require("ws");
39432
40879
 
39433
40880
  // src/relay/static.ts
39434
- var import_node_fs70 = require("fs");
40881
+ var import_node_fs71 = require("fs");
39435
40882
  var import_promises = require("fs/promises");
39436
- var import_node_path68 = __toESM(require("path"), 1);
40883
+ var import_node_path69 = __toESM(require("path"), 1);
39437
40884
  var TYPES = {
39438
40885
  ".css": "text/css; charset=utf-8",
39439
40886
  ".html": "text/html; charset=utf-8",
@@ -39464,9 +40911,9 @@ function resolveStaticPath(root, requestUrl) {
39464
40911
  return null;
39465
40912
  }
39466
40913
  if (!pathname.startsWith("/") || pathname.includes("\0")) return null;
39467
- const rootResolved = import_node_path68.default.resolve(root);
39468
- const candidate = import_node_path68.default.resolve(rootResolved, `.${pathname}`);
39469
- if (candidate !== rootResolved && !candidate.startsWith(rootResolved + import_node_path68.default.sep)) {
40914
+ const rootResolved = import_node_path69.default.resolve(root);
40915
+ const candidate = import_node_path69.default.resolve(rootResolved, `.${pathname}`);
40916
+ if (candidate !== rootResolved && !candidate.startsWith(rootResolved + import_node_path69.default.sep)) {
39470
40917
  return null;
39471
40918
  }
39472
40919
  return candidate;
@@ -39480,7 +40927,7 @@ async function fileSize(file) {
39480
40927
  }
39481
40928
  }
39482
40929
  function sendFile(req, res, file, size) {
39483
- const ext = import_node_path68.default.extname(file).toLowerCase();
40930
+ const ext = import_node_path69.default.extname(file).toLowerCase();
39484
40931
  res.writeHead(200, {
39485
40932
  "Content-Type": TYPES[ext] ?? "application/octet-stream",
39486
40933
  "Content-Length": size,
@@ -39490,7 +40937,7 @@ function sendFile(req, res, file, size) {
39490
40937
  res.end();
39491
40938
  return true;
39492
40939
  }
39493
- (0, import_node_fs70.createReadStream)(file).pipe(res);
40940
+ (0, import_node_fs71.createReadStream)(file).pipe(res);
39494
40941
  return true;
39495
40942
  }
39496
40943
  async function tryServeStatic(req, res, root) {
@@ -39499,7 +40946,7 @@ async function tryServeStatic(req, res, root) {
39499
40946
  if (!candidate) return false;
39500
40947
  const direct = await fileSize(candidate);
39501
40948
  if (direct != null) return sendFile(req, res, candidate, direct);
39502
- const asIndex = import_node_path68.default.join(candidate, "index.html");
40949
+ const asIndex = import_node_path69.default.join(candidate, "index.html");
39503
40950
  const indexSize = await fileSize(asIndex);
39504
40951
  if (indexSize != null) return sendFile(req, res, asIndex, indexSize);
39505
40952
  return false;
@@ -39507,14 +40954,19 @@ async function tryServeStatic(req, res, root) {
39507
40954
 
39508
40955
  // src/relay/server.ts
39509
40956
  init_abletime_oauth();
39510
- function sendHtml(res, status, title, body) {
40957
+ function sendHtml2(res, status, title, body) {
39511
40958
  res.writeHead(status, { "Content-Type": "text/html; charset=utf-8" });
39512
40959
  res.end(slackOAuthHtmlPage(title, body));
39513
40960
  }
39514
- function sendJson2(res, status, body) {
40961
+ function sendJson3(res, status, body) {
39515
40962
  res.writeHead(status, { "Content-Type": "application/json" });
39516
40963
  res.end(JSON.stringify(body));
39517
40964
  }
40965
+ function relayClientIp(req) {
40966
+ const fly = req.headers["fly-client-ip"];
40967
+ if (typeof fly === "string" && fly.trim()) return fly.trim();
40968
+ return req.socket.remoteAddress ?? "unknown";
40969
+ }
39518
40970
  function startRelaySocketPing(ws, intervalMs) {
39519
40971
  const ping = setInterval(() => {
39520
40972
  if (ws.readyState !== ws.OPEN) {
@@ -39542,11 +40994,26 @@ async function startRelayServer(opts) {
39542
40994
  const clientSecret = (opts.clientSecret ?? process.env.SIDEBOARD_SLACK_CLIENT_SECRET ?? "").trim();
39543
40995
  const redirectUri = (opts.oauthRedirectUri ?? slackOAuthRelayRedirectUri()).trim();
39544
40996
  const pending = new SlackOAuthPendingStore();
40997
+ let boundPort = 0;
39545
40998
  const hub = opts.hub ?? new SlackRelayHub({
39546
40999
  fetchImpl: opts.fetchImpl ?? fetch,
39547
41000
  onLog: log
39548
41001
  });
39549
- const remoteHub = new RemoteHub();
41002
+ const accountsPath = opts.accountsPath?.trim() ?? "";
41003
+ if (accountsPath) assertRelayAccountsDurable(accountsPath);
41004
+ const accountStore = accountsPath ? new RelayAccountStore(accountsPath) : null;
41005
+ const accountSessions = new RelayLoginSessions();
41006
+ const relayOAuth = opts.relayOAuth ?? {};
41007
+ const authMarks = /* @__PURE__ */ new Map();
41008
+ const remoteByIp = /* @__PURE__ */ new Map();
41009
+ let remoteSockets = 0;
41010
+ const remoteHub = new RemoteHub({
41011
+ authorizeHost: accountStore ? (input) => {
41012
+ const decision = accountStore.authorizeHost(input);
41013
+ return decision.ok ? { ok: true } : { ok: false, message: decision.message };
41014
+ } : void 0,
41015
+ onAuthorized: (socket) => authMarks.get(socket)?.()
41016
+ });
39550
41017
  const handleCallback = async (reqUrl, res) => {
39551
41018
  const url2 = parseSlackOAuthCallbackUrl(reqUrl);
39552
41019
  if (!url2) return false;
@@ -39555,7 +41022,7 @@ async function startRelayServer(opts) {
39555
41022
  const code = url2.searchParams.get("code")?.trim() ?? "";
39556
41023
  if (error) {
39557
41024
  if (state) pending.put(state, { ok: false, error: `Slack OAuth: ${error}` });
39558
- sendHtml(
41025
+ sendHtml2(
39559
41026
  res,
39560
41027
  400,
39561
41028
  "Slack",
@@ -39564,7 +41031,7 @@ async function startRelayServer(opts) {
39564
41031
  return true;
39565
41032
  }
39566
41033
  if (!state || !code) {
39567
- sendHtml(res, 400, "Slack", "<h1>Invalid callback</h1><p>State or code missing.</p>");
41034
+ sendHtml2(res, 400, "Slack", "<h1>Invalid callback</h1><p>State or code missing.</p>");
39568
41035
  return true;
39569
41036
  }
39570
41037
  if (!clientSecret) {
@@ -39572,7 +41039,7 @@ async function startRelayServer(opts) {
39572
41039
  ok: false,
39573
41040
  error: "Relay is missing SIDEBOARD_SLACK_CLIENT_SECRET"
39574
41041
  });
39575
- sendHtml(
41042
+ sendHtml2(
39576
41043
  res,
39577
41044
  500,
39578
41045
  "Slack",
@@ -39595,7 +41062,7 @@ async function startRelayServer(opts) {
39595
41062
  void hub.handleAppHomeOpened(payload.team_id, payload.user_id);
39596
41063
  }
39597
41064
  }
39598
- sendHtml(
41065
+ sendHtml2(
39599
41066
  res,
39600
41067
  200,
39601
41068
  "Slack connected",
@@ -39604,7 +41071,7 @@ async function startRelayServer(opts) {
39604
41071
  } catch (err) {
39605
41072
  const message = err instanceof Error ? err.message : String(err);
39606
41073
  pending.put(state, { ok: false, error: message });
39607
- sendHtml(res, 400, "Slack", `<h1>Could not connect Slack</h1><p>${escapeHtml2(message)}</p>`);
41074
+ sendHtml2(res, 400, "Slack", `<h1>Could not connect Slack</h1><p>${escapeHtml2(message)}</p>`);
39608
41075
  }
39609
41076
  return true;
39610
41077
  };
@@ -39614,19 +41081,30 @@ async function startRelayServer(opts) {
39614
41081
  const state = url2.searchParams.get("state")?.trim() ?? "";
39615
41082
  const value = pending.take(state);
39616
41083
  if (!value) {
39617
- sendJson2(res, 404, { ok: false, error: "pending" });
41084
+ sendJson3(res, 404, { ok: false, error: "pending" });
39618
41085
  return true;
39619
41086
  }
39620
41087
  if (!value.ok) {
39621
- sendJson2(res, 400, { ok: false, error: value.error });
41088
+ sendJson3(res, 400, { ok: false, error: value.error });
39622
41089
  return true;
39623
41090
  }
39624
- sendJson2(res, 200, { ok: true, ...value.payload });
41091
+ sendJson3(res, 200, { ok: true, ...value.payload });
39625
41092
  return true;
39626
41093
  };
39627
41094
  const httpServer = (0, import_node_http5.createServer)((req, res) => {
39628
41095
  void (async () => {
39629
41096
  const reqUrl = req.url || "/";
41097
+ if (accountStore && await handleRelayAccountHttp({
41098
+ req,
41099
+ res,
41100
+ store: accountStore,
41101
+ oauth: relayOAuth,
41102
+ sessions: accountSessions,
41103
+ publicOrigin: opts.relayPublicOrigin?.trim() || process.env.SIDEBOARD_RELAY_PUBLIC_ORIGIN?.trim() || `http://127.0.0.1:${boundPort}`,
41104
+ fetchImpl: opts.fetchImpl
41105
+ })) {
41106
+ return;
41107
+ }
39630
41108
  if (await handleCallback(reqUrl, res)) return;
39631
41109
  if (handleResult(reqUrl, res)) return;
39632
41110
  const ableTimeBounce = ableTimeOAuthLocalBounceUrl(reqUrl);
@@ -39640,7 +41118,7 @@ async function startRelayServer(opts) {
39640
41118
  return;
39641
41119
  }
39642
41120
  if (req.url === "/health") {
39643
- sendJson2(res, 200, {
41121
+ sendJson3(res, 200, {
39644
41122
  ok: true,
39645
41123
  service: "sideboard-relay",
39646
41124
  sessions: hub.listSessions().length,
@@ -39659,7 +41137,7 @@ async function startRelayServer(opts) {
39659
41137
  return;
39660
41138
  }
39661
41139
  if (req.url === "/") {
39662
- sendJson2(res, 200, {
41140
+ sendJson3(res, 200, {
39663
41141
  ok: true,
39664
41142
  service: "sideboard-relay",
39665
41143
  sessions: hub.listSessions().length,
@@ -39686,6 +41164,14 @@ async function startRelayServer(opts) {
39686
41164
  socket.destroy();
39687
41165
  return;
39688
41166
  }
41167
+ if (target === remoteWss) {
41168
+ const ip = relayClientIp(req);
41169
+ const count = remoteByIp.get(ip) ?? 0;
41170
+ if (count >= 20 || remoteSockets >= 4e3) {
41171
+ socket.destroy();
41172
+ return;
41173
+ }
41174
+ }
39689
41175
  target.handleUpgrade(req, socket, head, (ws) => {
39690
41176
  target.emit("connection", ws, req);
39691
41177
  });
@@ -39720,7 +41206,10 @@ async function startRelayServer(opts) {
39720
41206
  hub.detachSocket(socket);
39721
41207
  });
39722
41208
  });
39723
- remoteWss.on("connection", (ws) => {
41209
+ remoteWss.on("connection", (ws, req) => {
41210
+ const ip = relayClientIp(req);
41211
+ remoteByIp.set(ip, (remoteByIp.get(ip) ?? 0) + 1);
41212
+ remoteSockets += 1;
39724
41213
  const socket = {
39725
41214
  send: (data) => {
39726
41215
  if (ws.readyState === ws.OPEN) ws.send(data);
@@ -39732,14 +41221,35 @@ async function startRelayServer(opts) {
39732
41221
  }
39733
41222
  }
39734
41223
  };
41224
+ let released = false;
41225
+ const authTimer = accountStore ? setTimeout(() => {
41226
+ try {
41227
+ ws.close(4001, "sign in required");
41228
+ } catch {
41229
+ }
41230
+ }, 12e4) : null;
41231
+ authMarks.set(socket, () => {
41232
+ if (authTimer) clearTimeout(authTimer);
41233
+ });
41234
+ const release = () => {
41235
+ if (released) return;
41236
+ released = true;
41237
+ if (authTimer) clearTimeout(authTimer);
41238
+ authMarks.delete(socket);
41239
+ remoteSockets -= 1;
41240
+ const next = (remoteByIp.get(ip) ?? 1) - 1;
41241
+ if (next <= 0) remoteByIp.delete(ip);
41242
+ else remoteByIp.set(ip, next);
41243
+ remoteHub.detachSocket(socket);
41244
+ };
39735
41245
  startRelaySocketPing(ws, REMOTE_PING_INTERVAL_MS);
39736
41246
  log("remote client connected");
39737
41247
  ws.on("message", (data) => {
39738
41248
  const raw = typeof data === "string" ? data : data.toString("utf8");
39739
41249
  remoteHub.handleClientMessage(socket, raw);
39740
41250
  });
39741
- ws.on("close", () => remoteHub.detachSocket(socket));
39742
- ws.on("error", () => remoteHub.detachSocket(socket));
41251
+ ws.on("close", release);
41252
+ ws.on("error", release);
39743
41253
  });
39744
41254
  const port = opts.port ?? 0;
39745
41255
  const host = opts.host ?? "0.0.0.0";
@@ -39748,7 +41258,7 @@ async function startRelayServer(opts) {
39748
41258
  httpServer.listen(port, host, () => resolve2());
39749
41259
  });
39750
41260
  const address = httpServer.address();
39751
- const boundPort = typeof address === "object" && address ? address.port : typeof port === "number" ? port : 0;
41261
+ boundPort = typeof address === "object" && address ? address.port : typeof port === "number" ? port : 0;
39752
41262
  const url = `ws://${host === "0.0.0.0" ? "127.0.0.1" : host}:${boundPort}${SLACK_RELAY_DESKTOP_PATH}`;
39753
41263
  log(`relay listening on ${url}`);
39754
41264
  const ac = new AbortController();
@@ -39791,13 +41301,14 @@ async function startRelayServer(opts) {
39791
41301
  httpServer.close((err) => err ? reject(err) : resolve2());
39792
41302
  });
39793
41303
  await socketModeDone.catch(() => void 0);
41304
+ accountStore?.close();
39794
41305
  }
39795
41306
  };
39796
41307
  }
39797
41308
 
39798
41309
  // src/slack/outbound-watch.ts
39799
- var import_node_fs71 = require("fs");
39800
- var import_node_path69 = require("path");
41310
+ var import_node_fs72 = require("fs");
41311
+ var import_node_path70 = require("path");
39801
41312
  init_paths();
39802
41313
  init_private_file();
39803
41314
  init_secure_file();
@@ -39810,7 +41321,7 @@ var POLL_INTERVAL_MS = 12e3;
39810
41321
  var lastPollMs = 0;
39811
41322
  var nameCache = /* @__PURE__ */ new Map();
39812
41323
  function storePath4() {
39813
- return (0, import_node_path69.join)(appDataDir(), "slack-outbound-watch.json");
41324
+ return (0, import_node_path70.join)(appDataDir(), "slack-outbound-watch.json");
39814
41325
  }
39815
41326
  function watchId(teamId, channelId, ts) {
39816
41327
  return `${teamId}:${channelId}:${ts}`;
@@ -39823,9 +41334,9 @@ function tsNewer(a, b) {
39823
41334
  }
39824
41335
  function readStore4() {
39825
41336
  const path2 = storePath4();
39826
- if (!(0, import_node_fs71.existsSync)(path2)) return [];
41337
+ if (!(0, import_node_fs72.existsSync)(path2)) return [];
39827
41338
  try {
39828
- const parsed = isSecureFileEncrypted(path2) ? readSecureJson(path2) : JSON.parse((0, import_node_fs71.readFileSync)(path2, "utf8"));
41339
+ const parsed = isSecureFileEncrypted(path2) ? readSecureJson(path2) : JSON.parse((0, import_node_fs72.readFileSync)(path2, "utf8"));
39829
41340
  return Array.isArray(parsed?.watches) ? parsed.watches : [];
39830
41341
  } catch {
39831
41342
  return [];
@@ -40470,6 +41981,7 @@ async function pollSlackOutboundWatches(opts) {
40470
41981
  disconnectLinear,
40471
41982
  disconnectLinearConnection,
40472
41983
  disconnectOptionalService,
41984
+ disconnectRemoteAccount,
40473
41985
  disconnectSlackWorkspace,
40474
41986
  discoverSkills,
40475
41987
  downloadAbleTimeAttachment,
@@ -40726,6 +42238,7 @@ async function pollSlackOutboundWatches(opts) {
40726
42238
  isPrNotMergeableError,
40727
42239
  isPresentPlanToolName,
40728
42240
  isPrimaryCheckoutThread,
42241
+ isRemoteAccountLoginCancelled,
40729
42242
  isRemoteStopCommand,
40730
42243
  isSelfWrittenRecord,
40731
42244
  isSessionQuotaLimit,
@@ -41087,6 +42600,7 @@ async function pollSlackOutboundWatches(opts) {
41087
42600
  startMcpServer,
41088
42601
  startOrchestration,
41089
42602
  startRelayServer,
42603
+ startRemoteAccountLogin,
41090
42604
  startSlackOAuth,
41091
42605
  statWorktreePath,
41092
42606
  stopAgentRpcServer,