@sideboard-ai/core 0.1.274 → 0.1.276

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-BJ5T22L5.js → chunk-FELFTNH2.js} +22 -21
  14. package/dist/{chunk-GCSG3OGV.js → chunk-HOMZKYHF.js} +29 -27
  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-DVTEC4BF.js → chunk-MGS5T2A3.js} +29 -27
  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-XIVM7K77.js → chunk-SQOAFR2U.js} +23 -22
  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-B2BXSA7N.js → conductor-GBCE37K4.js} +4 -4
  31. package/dist/{conductor-DXBLL23V.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 +1989 -182
  37. package/dist/index.d.cts +121 -22
  38. package/dist/index.d.ts +121 -22
  39. package/dist/index.js +1872 -45
  40. package/dist/mcp/run-stdio.cjs +90 -136
  41. package/dist/mcp/run-stdio.js +11 -11
  42. package/dist/{orchestrator-6T35CJHB.js → orchestrator-6QJLM6Y4.js} +6 -6
  43. package/dist/{orchestrator-QRSEOHXI.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);
@@ -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;
@@ -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,11 +38731,802 @@ 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-media.ts
38742
+ init_diff();
38743
+ var PHONE_IMAGE_MAX_BYTES = 15e5;
38744
+ var IMAGE_MIME = {
38745
+ png: "image/png",
38746
+ jpg: "image/jpeg",
38747
+ jpeg: "image/jpeg",
38748
+ gif: "image/gif",
38749
+ webp: "image/webp",
38750
+ bmp: "image/bmp"
38751
+ };
38752
+ function phoneWorktreeRelative(worktreePath, raw) {
38753
+ let text6 = raw.trim();
38754
+ if (!text6) return null;
38755
+ if (/^file:\/\//i.test(text6)) {
38756
+ try {
38757
+ text6 = decodeURIComponent(new URL(text6).pathname);
38758
+ } catch {
38759
+ return null;
38760
+ }
38761
+ }
38762
+ text6 = (text6.split("#")[0] ?? text6).replace(/\\/g, "/");
38763
+ if (!text6 || text6.includes("\0") || text6.split("/").includes("..")) return null;
38764
+ const root = worktreePath.replace(/\/+$/, "");
38765
+ if (!root || root === "/") return null;
38766
+ if (text6.startsWith("/")) {
38767
+ if (text6 !== root && !text6.startsWith(`${root}/`)) return null;
38768
+ text6 = text6 === root ? "" : text6.slice(root.length + 1);
38769
+ }
38770
+ text6 = text6.replace(/^\.\//, "").replace(/\/+$/, "");
38771
+ if (!text6 || text6 === "." || text6.startsWith("/")) return null;
38772
+ return text6;
38773
+ }
38774
+ function phoneImageMime(relativePath) {
38775
+ const ext = relativePath.split(".").pop()?.toLowerCase() ?? "";
38776
+ return IMAGE_MIME[ext] ?? null;
38777
+ }
38778
+ function phoneImageDataUrl(worktreePath, raw) {
38779
+ const relative = phoneWorktreeRelative(worktreePath, raw);
38780
+ if (!relative) return null;
38781
+ const mime = phoneImageMime(relative);
38782
+ if (!mime) return null;
38783
+ try {
38784
+ const file = readWorktreeFileForUpload(worktreePath, relative, { maxBytes: PHONE_IMAGE_MAX_BYTES });
38785
+ return `data:${mime};base64,${file.contentBase64}`;
38786
+ } catch {
38787
+ return null;
38788
+ }
38789
+ }
38790
+
38791
+ // src/remote/phone-chats.ts
38792
+ var import_node_crypto25 = require("crypto");
38793
+ init_list_models();
38794
+ init_home_board();
38795
+ init_stage_files();
38796
+ init_worktree_labels();
38797
+ init_thinking_effort();
38798
+ init_app_settings();
38799
+ init_global_workspace();
38800
+ init_thread_store();
38801
+ init_workspaces();
38802
+ init_chat_tabs();
38803
+ init_create();
38804
+ init_orchestrator();
38805
+ var PHONE_CONTROL_PREFIX = "\0sb.phone\n";
38806
+ var PHONE_USER_PREFIX = "Phone\n\n";
38807
+ var TRANSCRIPT_LIMIT = 40;
38808
+ var MESSAGE_LIMIT = 4e3;
38809
+ var PREVIEW_LIMIT = 90;
38810
+ var HISTORY_PAGE_SIZE = 40;
38811
+ var AGENT_KINDS = /* @__PURE__ */ new Set(["claude", "codex", "opencode", "brightsy", "cursor"]);
38812
+ var MAX_PHONE_FILES = 8;
38813
+ var MAX_PHONE_FILE_CHARS = Math.ceil(8 * 1024 * 1024 * 4 / 3);
38814
+ var MAX_PHONE_FILES_CHARS = Math.ceil(20 * 1024 * 1024 * 4 / 3);
38815
+ var MAX_DICTATION_CHARS = Math.ceil(12 * 1024 * 1024 * 4 / 3);
38816
+ function encodePhoneControl(payload) {
38817
+ return PHONE_CONTROL_PREFIX + JSON.stringify(payload);
38818
+ }
38819
+ function takePhoneControl(text6) {
38820
+ if (!text6.startsWith(PHONE_CONTROL_PREFIX)) return null;
38821
+ let parsed;
38822
+ try {
38823
+ parsed = JSON.parse(text6.slice(PHONE_CONTROL_PREFIX.length));
38824
+ } catch {
38825
+ return "invalid";
38826
+ }
38827
+ return parsePhoneControl(parsed);
38828
+ }
38829
+ function parsePhoneControl(value) {
38830
+ if (!value || typeof value !== "object") return "invalid";
38831
+ const op = value.op;
38832
+ if (op === "list") return { op: "list" };
38833
+ if (op === "history") {
38834
+ const query = textField(value, "query");
38835
+ const after = textField(value, "after");
38836
+ return { op: "history", ...query ? { query } : {}, ...after ? { after } : {} };
38837
+ }
38838
+ if (op === "sources") {
38839
+ const repoPath = textField(value, "repoPath");
38840
+ if (!repoPath || !isProjectPath(repoPath)) return "invalid";
38841
+ const query = textField(value, "query");
38842
+ return { op: "sources", repoPath, ...query ? { query } : {} };
38843
+ }
38844
+ if (op === "models") {
38845
+ const agent = agentField(value);
38846
+ if (!agent) return "invalid";
38847
+ return { op: "models", agent };
38848
+ }
38849
+ if (op === "dictate") {
38850
+ const id = textField(value, "id");
38851
+ const audioBase64 = base64Field(value, "audioBase64");
38852
+ if (!id || !audioBase64 || audioBase64.length > MAX_DICTATION_CHARS) return "invalid";
38853
+ return { op: "dictate", id, audioBase64 };
38854
+ }
38855
+ if (op === "create") return parsePhoneCreate(value);
38856
+ const chatId = typeof value.chatId === "string" ? value.chatId.trim() : "";
38857
+ if (!chatId) return "invalid";
38858
+ if (op === "open" || op === "archive" || op === "restore" || op === "stop") return { op, chatId };
38859
+ if (op === "open-file" || op === "media") {
38860
+ const path2 = textField(value, "path");
38861
+ if (!path2 || path2.length > 2e3) return "invalid";
38862
+ if (op === "media") return { op, chatId, path: path2 };
38863
+ const startLine = lineField(value, "startLine");
38864
+ const endLine = lineField(value, "endLine");
38865
+ return {
38866
+ op,
38867
+ chatId,
38868
+ path: path2,
38869
+ ...startLine != null ? { startLine } : {},
38870
+ ...endLine != null ? { endLine } : {}
38871
+ };
38872
+ }
38873
+ const draft = parsePhoneDraft(value);
38874
+ if (draft === "invalid") return "invalid";
38875
+ if (op === "options") return { op: "options", chatId, ...draft };
38876
+ if (op === "prompt") {
38877
+ const text6 = typeof value.text === "string" ? value.text.trim() : "";
38878
+ const hasPayload = Boolean(text6 || draft.files?.length || draft.links?.length);
38879
+ if (!hasPayload) return "invalid";
38880
+ return { op: "prompt", chatId, text: text6, ...draft };
38881
+ }
38882
+ return "invalid";
38883
+ }
38884
+ function textField(value, key) {
38885
+ const raw = value[key];
38886
+ return typeof raw === "string" ? raw.trim() : "";
38887
+ }
38888
+ function lineField(value, key) {
38889
+ const raw = value[key];
38890
+ if (typeof raw !== "number" || !Number.isInteger(raw) || raw < 1 || raw > 1e6) return void 0;
38891
+ return raw;
38892
+ }
38893
+ function agentField(value) {
38894
+ const agent = textField(value, "agent");
38895
+ if (!agent) return void 0;
38896
+ return AGENT_KINDS.has(agent) ? agent : void 0;
38897
+ }
38898
+ function base64Field(value, key) {
38899
+ const raw = value[key];
38900
+ if (typeof raw !== "string") return "";
38901
+ return raw.replace(/\s/g, "");
38902
+ }
38903
+ function parsePhoneDraft(value) {
38904
+ const draft = {};
38905
+ if (textField(value, "agent")) {
38906
+ const agent = agentField(value);
38907
+ if (!agent) return "invalid";
38908
+ draft.agent = agent;
38909
+ }
38910
+ if ("model" in value) {
38911
+ const model = value.model;
38912
+ if (model === null || model === "") draft.model = null;
38913
+ else if (typeof model === "string") draft.model = model.trim().slice(0, 200);
38914
+ else return "invalid";
38915
+ }
38916
+ if ("effort" in value && value.effort != null) {
38917
+ const effort = value.effort;
38918
+ if (!isThinkingEffort(effort)) return "invalid";
38919
+ draft.effort = effort;
38920
+ }
38921
+ if ("fast" in value) {
38922
+ const fast = value.fast;
38923
+ if (typeof fast !== "boolean") return "invalid";
38924
+ draft.fast = fast;
38925
+ }
38926
+ if ("planMode" in value) {
38927
+ const planMode = value.planMode;
38928
+ if (typeof planMode !== "boolean") return "invalid";
38929
+ draft.planMode = planMode;
38930
+ }
38931
+ if ("autonomy" in value && value.autonomy != null) {
38932
+ const autonomy = value.autonomy;
38933
+ if (autonomy !== "default" && autonomy !== "full") return "invalid";
38934
+ draft.autonomy = autonomy;
38935
+ }
38936
+ const files = parsePhoneFiles(value.files);
38937
+ if (files === "invalid") return "invalid";
38938
+ if (files.length) draft.files = files;
38939
+ const links = parsePhoneLinks(value.links);
38940
+ if (links === "invalid") return "invalid";
38941
+ if (links.length) draft.links = links;
38942
+ const prompt = textField(value, "prompt");
38943
+ if (prompt) draft.prompt = prompt.slice(0, 2e4);
38944
+ return draft;
38945
+ }
38946
+ function parsePhoneFiles(value) {
38947
+ if (value == null) return [];
38948
+ if (!Array.isArray(value) || value.length > MAX_PHONE_FILES) return "invalid";
38949
+ const files = [];
38950
+ let total = 0;
38951
+ for (const item of value) {
38952
+ if (!item || typeof item !== "object") return "invalid";
38953
+ const name = (textField(item, "name").replace(/[/\\]/g, "_") || "file").slice(0, 180);
38954
+ const dataBase64 = base64Field(item, "dataBase64");
38955
+ if (!dataBase64 || !/^[A-Za-z0-9+/=]+$/.test(dataBase64)) return "invalid";
38956
+ if (dataBase64.length > MAX_PHONE_FILE_CHARS) return "invalid";
38957
+ total += dataBase64.length;
38958
+ if (total > MAX_PHONE_FILES_CHARS) return "invalid";
38959
+ files.push({ name, dataBase64 });
38960
+ }
38961
+ return files;
38962
+ }
38963
+ function parsePhoneLinks(value) {
38964
+ if (value == null) return [];
38965
+ if (!Array.isArray(value) || value.length > 8) return "invalid";
38966
+ const links = [];
38967
+ for (const item of value) {
38968
+ if (!item || typeof item !== "object") return "invalid";
38969
+ const ref = textField(item, "ref").slice(0, 80);
38970
+ if (!ref) return "invalid";
38971
+ const title = (textField(item, "title") || ref).slice(0, 200);
38972
+ const url = textField(item, "url").slice(0, 500);
38973
+ links.push({ ref, title, ...url ? { url } : {} });
38974
+ }
38975
+ return links;
38976
+ }
38977
+ function parsePhoneCreate(value) {
38978
+ const draft = parsePhoneDraft(value);
38979
+ if (draft === "invalid") return "invalid";
38980
+ const goal = textField(value, "goal") || draft.prompt || "";
38981
+ const where = value.where;
38982
+ if (where == null || where === "orchestration") {
38983
+ return {
38984
+ op: "create",
38985
+ where: "orchestration",
38986
+ ...draft,
38987
+ ...goal ? { goal, prompt: goal } : {}
38988
+ };
38989
+ }
38990
+ if (where === "worktree") {
38991
+ const chatId = textField(value, "chatId");
38992
+ if (!chatId) return "invalid";
38993
+ return { op: "create", where: "worktree", chatId, ...draft };
38994
+ }
38995
+ if (where === "project") {
38996
+ const repoPath = textField(value, "repoPath");
38997
+ if (!repoPath || !isProjectPath(repoPath)) return "invalid";
38998
+ const sourceRaw = textField(value, "sourceType");
38999
+ let sourceType;
39000
+ if (sourceRaw === "branch" || sourceRaw === "pr" || sourceRaw === "ticket") sourceType = sourceRaw;
39001
+ else if (sourceRaw) return "invalid";
39002
+ const sourceRef = textField(value, "sourceRef");
39003
+ if ((sourceType === "pr" || sourceType === "ticket") && !sourceRef) return "invalid";
39004
+ const title = textField(value, "title").slice(0, 200);
39005
+ const cowboy = value.cowboy === true;
39006
+ return {
39007
+ op: "create",
39008
+ where: "project",
39009
+ repoPath,
39010
+ ...draft,
39011
+ ...sourceType ? { sourceType } : {},
39012
+ ...sourceRef ? { sourceRef } : {},
39013
+ ...title ? { title } : {},
39014
+ ...cowboy ? { cowboy: true } : {}
39015
+ };
39016
+ }
39017
+ return "invalid";
39018
+ }
39019
+ function phoneVisibleText(role, text6) {
39020
+ const trimmed = text6.trim();
39021
+ if (role === "user" && trimmed.startsWith(PHONE_USER_PREFIX)) {
39022
+ return trimmed.slice(PHONE_USER_PREFIX.length).trim();
39023
+ }
39024
+ return trimmed;
39025
+ }
39026
+ function clip(text6, limit) {
39027
+ if (text6.length <= limit) return text6;
39028
+ return `${text6.slice(0, limit - 1)}\u2026`;
39029
+ }
39030
+ function summarize(thread) {
39031
+ return {
39032
+ id: thread.id,
39033
+ title: thread.title?.trim() || "Untitled",
39034
+ status: thread.status,
39035
+ preview: previewOf(thread),
39036
+ updatedAt: thread.updatedAt
39037
+ };
39038
+ }
39039
+ function previewOf(thread) {
39040
+ const messages = listPhoneMessages(thread);
39041
+ const last = messages[messages.length - 1];
39042
+ if (!last) return "";
39043
+ return clip(last.text.replace(/\s+/g, " ").trim(), PREVIEW_LIMIT);
39044
+ }
39045
+ function listPhoneMessages(thread) {
39046
+ const out = [];
39047
+ for (const message of thread.messages) {
39048
+ if (message.role !== "user" && message.role !== "agent") continue;
39049
+ const names = (message.attachments ?? []).map((item) => item.name.trim()).filter(Boolean);
39050
+ const text6 = clip(
39051
+ [phoneVisibleText(message.role, message.text), names.length ? names.join(", ") : ""].filter(Boolean).join("\n"),
39052
+ MESSAGE_LIMIT
39053
+ );
39054
+ if (!text6) continue;
39055
+ out.push({ role: message.role, text: text6 });
39056
+ }
39057
+ for (const prompt of thread.queue) {
39058
+ const text6 = clip(phoneVisibleText("user", prompt), MESSAGE_LIMIT);
39059
+ if (text6) out.push({ role: "user", text: text6 });
39060
+ }
39061
+ return out.slice(-TRANSCRIPT_LIMIT);
39062
+ }
39063
+ function repoName(repoPath) {
39064
+ const parts = repoPath.replace(/\/$/, "").split("/");
39065
+ return parts[parts.length - 1] || repoPath;
39066
+ }
39067
+ function isProjectPath(path2) {
39068
+ if (!path2 || path2 === "/" || path2 === ".") return false;
39069
+ return !isGlobalThread({ repoPath: path2 });
39070
+ }
39071
+ function listPhoneChats() {
39072
+ return listGlobalThreads().slice().sort((a, b) => a.updatedAt < b.updatedAt ? 1 : a.updatedAt > b.updatedAt ? -1 : 0).map(summarize);
39073
+ }
39074
+ function listPhoneSidebar() {
39075
+ const threads = listThreads();
39076
+ const byRepo = /* @__PURE__ */ new Map();
39077
+ for (const thread of threads) {
39078
+ if (!isProjectPath(thread.repoPath)) continue;
39079
+ const list = byRepo.get(thread.repoPath) ?? [];
39080
+ list.push(thread);
39081
+ byRepo.set(thread.repoPath, list);
39082
+ }
39083
+ for (const workspace of listWorkspaces()) {
39084
+ if (!isProjectPath(workspace.path) || byRepo.has(workspace.path)) continue;
39085
+ byRepo.set(workspace.path, []);
39086
+ }
39087
+ const projects = [...byRepo.entries()].sort(([a], [b]) => repoName(a).localeCompare(repoName(b))).map(([path2, repoThreads]) => ({
39088
+ name: repoName(path2),
39089
+ path: path2,
39090
+ worktrees: groupHomeBoardWorktrees(repoThreads).map((group) => ({
39091
+ label: worktreeDisplayLabelForGroup(group),
39092
+ chats: group.map(summarize)
39093
+ }))
39094
+ }));
39095
+ return { orchestration: listPhoneChats(), projects };
39096
+ }
39097
+ function historyStamp(thread) {
39098
+ return thread.archivedAt || thread.updatedAt;
39099
+ }
39100
+ function historyCursor(chat) {
39101
+ return `${chat.updatedAt} ${chat.id}`;
39102
+ }
39103
+ function historyQueryMatch(chat, query) {
39104
+ if (!query) return true;
39105
+ return `${chat.title} ${chat.where} ${chat.agent} ${chat.preview}`.toLowerCase().includes(query);
39106
+ }
39107
+ function listPhoneHistory(opts) {
39108
+ const query = opts?.query?.trim().toLowerCase() ?? "";
39109
+ const after = opts?.after?.trim() ?? "";
39110
+ 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) => ({
39111
+ id: thread.id,
39112
+ title: thread.title?.trim() || "Untitled",
39113
+ preview: previewOf(thread),
39114
+ updatedAt: historyStamp(thread),
39115
+ where: isGlobalThread(thread) ? "Orchestration" : repoName(thread.repoPath),
39116
+ agent: thread.agent
39117
+ })).filter((chat) => historyQueryMatch(chat, query));
39118
+ let start = 0;
39119
+ if (after) {
39120
+ const index = all.findIndex((chat) => historyCursor(chat) === after);
39121
+ start = index >= 0 ? index + 1 : all.length;
39122
+ }
39123
+ const chats = all.slice(start, start + HISTORY_PAGE_SIZE);
39124
+ const last = chats.at(-1);
39125
+ const more = start + chats.length < all.length;
39126
+ return {
39127
+ chats,
39128
+ total: all.length,
39129
+ ...more && last ? { next: historyCursor(last) } : {}
39130
+ };
39131
+ }
39132
+ function matchPhoneThread(chatId) {
39133
+ const id = chatId.trim();
39134
+ if (!id) return null;
39135
+ const exact = readThread(id);
39136
+ if (exact) return exact;
39137
+ const match = findThreadByRef(id);
39138
+ if (!match) return null;
39139
+ if (match.id !== id && !match.id.startsWith(id)) return null;
39140
+ return match;
39141
+ }
39142
+ function visiblePhoneThread(thread) {
39143
+ if (isGlobalThread(thread)) return true;
39144
+ return isProjectPath(thread.repoPath);
39145
+ }
39146
+ function findPhoneChat(chatId) {
39147
+ const thread = matchPhoneThread(chatId);
39148
+ if (!thread || !visiblePhoneThread(thread)) return null;
39149
+ return thread;
39150
+ }
39151
+ function readPhoneThread(chatId) {
39152
+ const thread = findPhoneChat(chatId);
39153
+ if (!thread || thread.status === "archived") return null;
39154
+ return thread;
39155
+ }
39156
+ function phoneComposerOptions(thread) {
39157
+ return {
39158
+ agent: thread.agent,
39159
+ model: thread.model ?? null,
39160
+ effort: thread.effort,
39161
+ fast: Boolean(thread.fast),
39162
+ planMode: Boolean(thread.planMode),
39163
+ autonomy: thread.autonomy ?? "default"
39164
+ };
39165
+ }
39166
+ function phoneTurnAttachments(draft) {
39167
+ const links = (draft.links ?? []).map((link) => ({
39168
+ id: (0, import_node_crypto25.randomUUID)(),
39169
+ name: link.ref,
39170
+ kind: "issue",
39171
+ content: [`Linked issue: ${link.ref} \u2014 ${link.title}`, link.url ? `URL: ${link.url}` : null].filter(Boolean).join("\n")
39172
+ }));
39173
+ const files = (draft.files ?? []).map((file) => ({
39174
+ id: (0, import_node_crypto25.randomUUID)(),
39175
+ name: file.name,
39176
+ kind: "file",
39177
+ previewDataUrl: `data:application/octet-stream;base64,${file.dataBase64}`,
39178
+ content: `File attached: ${file.name}`
39179
+ }));
39180
+ return [...links, ...files];
39181
+ }
39182
+ function openedFrom(thread, place) {
39183
+ const fresh = readThread(thread.id) ?? thread;
39184
+ return {
39185
+ chat: summarize(fresh),
39186
+ messages: listPhoneMessages(fresh),
39187
+ place,
39188
+ options: phoneComposerOptions(fresh)
39189
+ };
39190
+ }
39191
+ function openPhoneChat(chatId) {
39192
+ const thread = readPhoneThread(chatId);
39193
+ if (!thread) return null;
39194
+ const place = isGlobalThread(thread) ? { kind: "orchestration" } : projectPlace(thread);
39195
+ return openedFrom(thread, place);
39196
+ }
39197
+ function sameRepoPath3(a, b) {
39198
+ return a.replace(/\/+$/, "") === b.replace(/\/+$/, "");
39199
+ }
39200
+ function projectPlace(thread) {
39201
+ const group = threadsSharingWorktree(thread.worktreePath);
39202
+ return {
39203
+ kind: "project",
39204
+ repoPath: thread.repoPath,
39205
+ worktree: worktreeDisplayLabelForGroup(group.length > 0 ? group : [thread])
39206
+ };
39207
+ }
39208
+ function assertKnownProject(repoPath) {
39209
+ const path2 = repoPath.trim();
39210
+ if (!isProjectPath(path2)) throw new Error("Pick a project on this Mac.");
39211
+ const known = listWorkspaces().some((workspace) => sameRepoPath3(workspace.path, path2)) || listThreads().some(
39212
+ (thread) => thread.status !== "archived" && sameRepoPath3(thread.repoPath, path2)
39213
+ );
39214
+ if (!known) throw new Error("That project is not on this Mac.");
39215
+ return path2;
39216
+ }
39217
+ function draftAgentOptions(draft, defaults) {
39218
+ return {
39219
+ agent: draft?.agent ?? defaults.agent,
39220
+ model: draft && "model" in draft ? draft.model ?? null : defaults.model,
39221
+ effort: draft?.effort ?? defaults.effort,
39222
+ fast: draft?.fast ?? defaults.fast,
39223
+ planMode: Boolean(draft?.planMode),
39224
+ autonomy: draft?.autonomy
39225
+ };
39226
+ }
39227
+ function createPhoneChat(agent) {
39228
+ const defaults = resolveOrchestratorDefaults();
39229
+ const thread = createGlobalChat({
39230
+ agent: agent ?? defaults.agent,
39231
+ model: defaults.model,
39232
+ effort: defaults.effort,
39233
+ fast: defaults.fast
39234
+ });
39235
+ return openedFrom(thread, { kind: "orchestration" });
39236
+ }
39237
+ async function createPhoneOrchestration(draft) {
39238
+ const goal = (draft?.goal ?? draft?.prompt ?? "").trim();
39239
+ const attachments = phoneTurnAttachments(draft ?? {});
39240
+ if (!goal && attachments.length === 0) return createPhoneChat(draft?.agent);
39241
+ const defaults = resolveOrchestratorDefaults();
39242
+ const options = draftAgentOptions(draft, defaults);
39243
+ const thread = await startOrchestration({
39244
+ goal: goal || "See the attached files.",
39245
+ agent: options.agent,
39246
+ model: options.model,
39247
+ effort: options.effort,
39248
+ fast: options.fast,
39249
+ planMode: options.planMode,
39250
+ autonomy: options.autonomy,
39251
+ attachments
39252
+ });
39253
+ return openedFrom(thread, { kind: "orchestration" });
39254
+ }
39255
+ function createPhoneWorktreeAgent(fromChatId, draft) {
39256
+ const source = readPhoneThread(fromChatId);
39257
+ if (!source || isGlobalThread(source)) throw new Error("That worktree is not on this Mac.");
39258
+ const defaults = resolveNewThreadOptions();
39259
+ const options = draftAgentOptions(draft, defaults);
39260
+ const attachments = persistPendingFileAttachments(
39261
+ source.worktreePath,
39262
+ phoneTurnAttachments(draft ?? {})
39263
+ );
39264
+ const thread = createChatTab({
39265
+ fromThreadId: source.id,
39266
+ agent: options.agent,
39267
+ model: options.model,
39268
+ effort: options.effort,
39269
+ fast: options.fast,
39270
+ autonomy: options.autonomy,
39271
+ attachments
39272
+ });
39273
+ if (draft?.planMode !== void 0) updateThread(thread.id, { planMode: draft.planMode });
39274
+ return openedFrom(thread, projectPlace(readThread(thread.id) ?? thread));
39275
+ }
39276
+ async function createPhoneProjectWorktree(repoPathOrInput) {
39277
+ const input = typeof repoPathOrInput === "string" ? { repoPath: repoPathOrInput } : repoPathOrInput;
39278
+ const path2 = assertKnownProject(input.repoPath);
39279
+ const defaults = resolveNewThreadOptions();
39280
+ const options = draftAgentOptions(input, defaults);
39281
+ const thread = await createThread({
39282
+ sourceType: input.sourceType ?? "branch",
39283
+ sourceRef: input.sourceRef || "default",
39284
+ repoPath: path2,
39285
+ title: input.title,
39286
+ cowboy: input.cowboy,
39287
+ agent: options.agent,
39288
+ model: options.model,
39289
+ effort: options.effort,
39290
+ fast: options.fast,
39291
+ planMode: options.planMode,
39292
+ autonomy: options.autonomy,
39293
+ attachments: phoneTurnAttachments(input),
39294
+ prompt: input.prompt,
39295
+ reuseExisting: false
39296
+ });
39297
+ return openedFrom(thread, projectPlace(thread));
39298
+ }
39299
+ async function listPhoneModels(agent) {
39300
+ const catalogs = await listModelsForAgent(agent);
39301
+ return (catalogs[0]?.models ?? []).slice(0, 40).map((model) => ({
39302
+ id: model.id,
39303
+ label: model.displayName || model.id
39304
+ }));
39305
+ }
39306
+ function archivePhoneChat(chatId) {
39307
+ const thread = readPhoneThread(chatId);
39308
+ if (!thread) return false;
39309
+ updateThread(thread.id, { status: "archived" });
39310
+ return true;
39311
+ }
39312
+
39313
+ // src/remote/phone-sources.ts
39314
+ init_worktree();
39315
+ function sourceWarning(label, err) {
39316
+ const message = err instanceof Error ? err.message : String(err);
39317
+ return message ? `${label}: ${message}` : `${label} failed.`;
39318
+ }
39319
+ async function listPhoneSources(repoPath, query) {
39320
+ const path2 = assertKnownProject(repoPath);
39321
+ const q = query?.trim() ?? "";
39322
+ const warnings = [];
39323
+ const [prs, branches, issues] = await Promise.all([
39324
+ listPrs(path2, { state: "open", limit: 40, ...q ? { query: q } : {} }).catch((err) => {
39325
+ warnings.push(sourceWarning("Pull requests", err));
39326
+ return [];
39327
+ }),
39328
+ listBranches(path2).catch((err) => {
39329
+ warnings.push(sourceWarning("Branches", err));
39330
+ return [];
39331
+ }),
39332
+ listIssues(path2, { limit: 40, ...q ? { query: q } : {} }).catch((err) => {
39333
+ warnings.push(sourceWarning("Issues", err));
39334
+ return { issues: [] };
39335
+ })
39336
+ ]);
39337
+ const needle = q.toLowerCase();
39338
+ const branchRows = branches.filter((branch) => !needle || branch.name.toLowerCase().includes(needle)).slice(0, 50).map((branch) => ({
39339
+ ref: branch.name,
39340
+ title: branch.current ? `${branch.name} (current)` : branch.name
39341
+ }));
39342
+ return {
39343
+ prs: prs.slice(0, 40).map((pr) => ({
39344
+ ref: String(pr.number),
39345
+ title: clip(pr.title || `PR #${pr.number}`, 140),
39346
+ ...pr.url ? { url: pr.url } : {}
39347
+ })),
39348
+ branches: branchRows,
39349
+ issues: (issues.issues ?? []).slice(0, 40).map((issue) => ({
39350
+ ref: issue.identifier,
39351
+ title: clip(issue.title || issue.identifier, 140),
39352
+ ...issue.url ? { url: issue.url } : {}
39353
+ })),
39354
+ ...warnings.length ? { warnings } : {}
39355
+ };
39356
+ }
39357
+
39358
+ // src/remote/phone-live.ts
39359
+ init_orchestrator();
39360
+ init_thread_store();
39361
+
39362
+ // src/remote/phone-stream.ts
39363
+ init_message_parts();
39364
+ var PHONE_STREAM_FLUSH_MS = 80;
39365
+ function phoneStreamFrame(parts) {
39366
+ return {
39367
+ text: visibleAssistantText(void 0, parts),
39368
+ activity: liveActivitySummary(parts)
39369
+ };
39370
+ }
39371
+ var SKIPPED = /* @__PURE__ */ new Set(["session_id", "usage", "stderr", "exit"]);
39372
+ function phoneStreamParts(parts, event) {
39373
+ if (SKIPPED.has(event.type)) return null;
39374
+ return applyAgentEvent(parts, event);
39375
+ }
39376
+ function phoneStreamFlushNow(event) {
39377
+ return event.type === "tool_use" || event.type === "tool_result" && !event.partial;
39378
+ }
39379
+
39380
+ // src/remote/phone-live.ts
38760
39381
  var REMOTE_STOPPED_REPLY = "Sideboard stopped the in-progress turn.";
39382
+ var chatChains = /* @__PURE__ */ new Map();
39383
+ var chatGeneration = /* @__PURE__ */ new Map();
39384
+ var phoneTurnGen = /* @__PURE__ */ new Map();
39385
+ function bumpChat(chatId) {
39386
+ const next = (chatGeneration.get(chatId) ?? 0) + 1;
39387
+ chatGeneration.set(chatId, next);
39388
+ return next;
39389
+ }
39390
+ function finishPhoneTurn(chatId, generation) {
39391
+ if (phoneTurnGen.get(chatId) === generation) phoneTurnGen.delete(chatId);
39392
+ }
39393
+ function enqueueChat(chatId, fn) {
39394
+ const prev = chatChains.get(chatId) ?? Promise.resolve();
39395
+ const run2 = prev.then(fn, fn);
39396
+ chatChains.set(
39397
+ chatId,
39398
+ run2.then(
39399
+ () => void 0,
39400
+ () => void 0
39401
+ )
39402
+ );
39403
+ }
39404
+ function emitControl(opts, reply) {
39405
+ opts.onOutbound({ type: "assistant", text: encodePhoneControl(reply) });
39406
+ }
39407
+ function watchPhoneStream(opts, chatId, threadId, generation) {
39408
+ let parts = [];
39409
+ let timer = null;
39410
+ let lastKey = "";
39411
+ let closed = false;
39412
+ const alive = () => !closed && chatGeneration.get(chatId) === generation;
39413
+ const flush2 = () => {
39414
+ timer = null;
39415
+ if (!alive()) return;
39416
+ const frame = phoneStreamFrame(parts);
39417
+ const key = `${frame.activity}
39418
+ ${frame.text}`;
39419
+ if (key === lastKey) return;
39420
+ if (!frame.text && !frame.activity) return;
39421
+ lastKey = key;
39422
+ emitControl(opts, { op: "stream", chatId, text: frame.text, activity: frame.activity });
39423
+ };
39424
+ const schedule = (event) => {
39425
+ if (phoneStreamFlushNow(event) || lastKey === "") {
39426
+ if (timer) {
39427
+ clearTimeout(timer);
39428
+ timer = null;
39429
+ }
39430
+ flush2();
39431
+ return;
39432
+ }
39433
+ if (timer) return;
39434
+ timer = setTimeout(flush2, PHONE_STREAM_FLUSH_MS);
39435
+ };
39436
+ const off = getOrchestrator().on((event) => {
39437
+ if (!alive()) return;
39438
+ if (event.type !== "turn_output" || event.threadId !== threadId) return;
39439
+ const next = phoneStreamParts(parts, event.event);
39440
+ if (!next) return;
39441
+ parts = next;
39442
+ schedule(event.event);
39443
+ });
39444
+ return () => {
39445
+ closed = true;
39446
+ if (timer) clearTimeout(timer);
39447
+ off();
39448
+ };
39449
+ }
39450
+ function interruptChat(chatId, log) {
39451
+ try {
39452
+ const thread = readPhoneThread(chatId);
39453
+ if (!thread) return false;
39454
+ if (thread.status !== "running" && thread.status !== "queued") return false;
39455
+ getOrchestrator().stop(thread.id, { clearQueue: true });
39456
+ log(`interrupt phone \u2192 chat ${thread.id.slice(0, 8)} (${thread.status})`);
39457
+ return true;
39458
+ } catch (err) {
39459
+ log(`interrupt chat: ${err instanceof Error ? err.message : String(err)}`);
39460
+ return false;
39461
+ }
39462
+ }
39463
+ function emitChatTurn(opts, chatId, threadId) {
39464
+ const thread = readThread(threadId);
39465
+ if (!thread) {
39466
+ emitControl(opts, { op: "error", chatId, message: "That agent is not on this Mac." });
39467
+ return;
39468
+ }
39469
+ if (thread.status === "stopped") {
39470
+ emitControl(opts, { op: "stopped", chatId });
39471
+ return;
39472
+ }
39473
+ const result = getOrchestrator().getTurnResult(thread.id);
39474
+ const pending = extractPendingPlanQuestions(
39475
+ [...thread.messages].reverse().find((m) => m.role === "agent")?.parts
39476
+ );
39477
+ if (result.taskState === "input-required" && pending) {
39478
+ emitControl(opts, {
39479
+ op: "ask",
39480
+ chatId,
39481
+ text: result.text.trim(),
39482
+ questions: pending.questions.map((q) => ({
39483
+ question: q.question,
39484
+ options: q.options.map(
39485
+ (o) => o.description ? { label: o.label, description: o.description } : { label: o.label }
39486
+ )
39487
+ }))
39488
+ });
39489
+ return;
39490
+ }
39491
+ const reply = outboundReplyFromTurn(result, {
39492
+ inputRequired: "Sideboard is waiting for an answer.",
39493
+ canceled: REMOTE_STOPPED_REPLY,
39494
+ completedEmpty: () => "",
39495
+ failed: (detail) => detail ? `Sideboard failed: ${detail}` : "Sideboard failed before producing a result."
39496
+ });
39497
+ emitControl(opts, { op: "assistant", chatId, text: reply.trim() });
39498
+ }
39499
+ function phoneError(err) {
39500
+ return err instanceof Error ? err.message : String(err);
39501
+ }
39502
+ function deliverPhoneTurn(opts, chatId) {
39503
+ const thread = readPhoneThread(chatId);
39504
+ if (!thread || thread.status !== "running" && thread.status !== "queued") return;
39505
+ const id = thread.id;
39506
+ const claimed = phoneTurnGen.get(id);
39507
+ if (claimed != null && claimed === chatGeneration.get(id)) return;
39508
+ const generation = bumpChat(id);
39509
+ phoneTurnGen.set(id, generation);
39510
+ enqueueChat(id, async () => {
39511
+ if (chatGeneration.get(id) !== generation) return;
39512
+ const stopStream = watchPhoneStream(opts, id, id, generation);
39513
+ try {
39514
+ await getOrchestrator().waitForTurn(id, 14 * 60 * 1e3);
39515
+ } catch (err) {
39516
+ stopStream();
39517
+ finishPhoneTurn(id, generation);
39518
+ if (chatGeneration.get(id) !== generation) return;
39519
+ emitControl(opts, { op: "error", chatId: id, message: `Sideboard failed: ${phoneError(err)}` });
39520
+ return;
39521
+ }
39522
+ stopStream();
39523
+ finishPhoneTurn(id, generation);
39524
+ if (chatGeneration.get(id) !== generation) return;
39525
+ emitChatTurn(opts, id, id);
39526
+ });
39527
+ }
39528
+
39529
+ // src/remote/listen.ts
38761
39530
  function formatRemotePrompt(text6) {
38762
39531
  return `Phone
38763
39532
 
@@ -38789,10 +39558,312 @@ function interruptRemoteCoordinator(deviceId, log = () => void 0) {
38789
39558
  return false;
38790
39559
  }
38791
39560
  }
39561
+ function publishOpenedChat(opts, chatId) {
39562
+ const opened = openPhoneChat(chatId);
39563
+ if (!opened) {
39564
+ emitControl(opts, { op: "error", message: "That agent is not on this Mac." });
39565
+ return;
39566
+ }
39567
+ emitControl(opts, { op: "opened", ...opened });
39568
+ if (opened.chat.status === "running" || opened.chat.status === "queued") {
39569
+ deliverPhoneTurn(opts, opened.chat.id);
39570
+ }
39571
+ }
39572
+ function applyPhoneOptions(chatId, draft) {
39573
+ const patch = {};
39574
+ if (draft.agent) patch.agent = draft.agent;
39575
+ if ("model" in draft) patch.model = draft.model ?? null;
39576
+ if (draft.effort) patch.effort = draft.effort;
39577
+ if (draft.fast !== void 0) patch.fast = draft.fast;
39578
+ if (draft.planMode !== void 0) patch.planMode = draft.planMode;
39579
+ if (draft.autonomy) patch.autonomy = draft.autonomy;
39580
+ if (Object.keys(patch).length === 0) return;
39581
+ getOrchestrator().setThreadOptions(chatId, patch);
39582
+ }
39583
+ function stagePhoneTurnFiles(worktreePath, draft) {
39584
+ const links = phoneTurnAttachments({ links: draft.links });
39585
+ const files = stageBuffersAsAttachments(
39586
+ worktreePath,
39587
+ (draft.files ?? []).map((file) => ({ name: file.name, dataBase64: file.dataBase64 }))
39588
+ );
39589
+ return [...links, ...files];
39590
+ }
39591
+ function handlePhoneControl(cmd, opts) {
39592
+ const log = opts.onLog ?? (() => void 0);
39593
+ if (cmd.op === "list") {
39594
+ emitControl(opts, { op: "sidebar", ...listPhoneSidebar() });
39595
+ return;
39596
+ }
39597
+ if (cmd.op === "history") {
39598
+ const page = listPhoneHistory({ query: cmd.query, after: cmd.after });
39599
+ emitControl(opts, {
39600
+ op: "history",
39601
+ chats: page.chats,
39602
+ total: page.total,
39603
+ ...page.next ? { next: page.next } : {},
39604
+ ...cmd.query ? { query: cmd.query } : {},
39605
+ ...cmd.after ? { after: cmd.after } : {}
39606
+ });
39607
+ return;
39608
+ }
39609
+ if (cmd.op === "sources") {
39610
+ const repoPath = cmd.repoPath;
39611
+ const query = cmd.query;
39612
+ void listPhoneSources(repoPath, query).then(
39613
+ (page) => emitControl(opts, {
39614
+ op: "sources",
39615
+ repoPath,
39616
+ ...query ? { query } : {},
39617
+ ...page
39618
+ })
39619
+ ).catch((err) => {
39620
+ emitControl(opts, { op: "error", message: phoneError(err) });
39621
+ });
39622
+ return;
39623
+ }
39624
+ if (cmd.op === "models") {
39625
+ const agent = cmd.agent;
39626
+ void listPhoneModels(agent).then((models) => emitControl(opts, { op: "models", agent, models })).catch((err) => {
39627
+ emitControl(opts, { op: "error", message: phoneError(err) });
39628
+ });
39629
+ return;
39630
+ }
39631
+ if (cmd.op === "dictate") {
39632
+ const id = cmd.id;
39633
+ const transcribe = opts.transcribeWav;
39634
+ if (!transcribe) {
39635
+ emitControl(opts, { op: "error", message: "Speech is not available on this Mac." });
39636
+ return;
39637
+ }
39638
+ void transcribe(cmd.audioBase64).then((text6) => emitControl(opts, { op: "dictated", id, text: text6.trim() })).catch((err) => {
39639
+ emitControl(opts, { op: "error", message: phoneError(err) });
39640
+ });
39641
+ return;
39642
+ }
39643
+ if (cmd.op === "options") {
39644
+ const thread = readPhoneThread(cmd.chatId);
39645
+ if (!thread) {
39646
+ emitControl(opts, { op: "error", chatId: cmd.chatId, message: "That agent is not on this Mac." });
39647
+ return;
39648
+ }
39649
+ try {
39650
+ applyPhoneOptions(thread.id, cmd);
39651
+ const fresh = readPhoneThread(thread.id) ?? thread;
39652
+ emitControl(opts, { op: "options", chatId: fresh.id, ...phoneComposerOptions(fresh) });
39653
+ } catch (err) {
39654
+ emitControl(opts, { op: "error", chatId: cmd.chatId, message: phoneError(err) });
39655
+ }
39656
+ return;
39657
+ }
39658
+ if (cmd.op === "create") {
39659
+ if (cmd.where === "worktree") {
39660
+ void (async () => {
39661
+ try {
39662
+ const opened = createPhoneWorktreeAgent(cmd.chatId, cmd);
39663
+ const prompt = cmd.prompt?.trim() || (cmd.files?.length || cmd.links?.length ? "See the attached files." : "");
39664
+ if (prompt) {
39665
+ try {
39666
+ await getOrchestrator().send(opened.chat.id, formatRemotePrompt(prompt));
39667
+ } catch (err) {
39668
+ emitControl(opts, { op: "error", chatId: opened.chat.id, message: phoneError(err) });
39669
+ }
39670
+ }
39671
+ const fresh = openPhoneChat(opened.chat.id) ?? opened;
39672
+ emitControl(opts, { op: "opened", ...fresh });
39673
+ if (prompt) deliverPhoneTurn(opts, fresh.chat.id);
39674
+ } catch (err) {
39675
+ emitControl(opts, { op: "error", message: phoneError(err) });
39676
+ }
39677
+ })();
39678
+ return;
39679
+ }
39680
+ if (cmd.where === "project") {
39681
+ void createPhoneProjectWorktree(cmd).then(async (opened) => {
39682
+ const prompt = resolveCreateFirstPrompt({
39683
+ sourceType: cmd.sourceType ?? "branch",
39684
+ prompt: cmd.prompt
39685
+ }) || (cmd.files?.length || cmd.links?.length ? "See the attached files." : "");
39686
+ if (prompt) {
39687
+ try {
39688
+ await getOrchestrator().send(opened.chat.id, formatRemotePrompt(prompt));
39689
+ } catch (err) {
39690
+ emitControl(opts, { op: "error", chatId: opened.chat.id, message: phoneError(err) });
39691
+ }
39692
+ }
39693
+ const fresh = openPhoneChat(opened.chat.id) ?? opened;
39694
+ emitControl(opts, { op: "opened", ...fresh });
39695
+ if (prompt) deliverPhoneTurn(opts, fresh.chat.id);
39696
+ }).catch((err) => {
39697
+ emitControl(opts, { op: "error", message: phoneError(err) });
39698
+ });
39699
+ return;
39700
+ }
39701
+ const goal = cmd.goal?.trim() || cmd.prompt?.trim() || "";
39702
+ if (goal || cmd.files?.length || cmd.links?.length) {
39703
+ void createPhoneOrchestration({ ...cmd, goal }).then((opened) => {
39704
+ emitControl(opts, { op: "opened", ...opened });
39705
+ deliverPhoneTurn(opts, opened.chat.id);
39706
+ }).catch((err) => {
39707
+ emitControl(opts, { op: "error", message: phoneError(err) });
39708
+ });
39709
+ return;
39710
+ }
39711
+ try {
39712
+ const agent = coerceOrchestratorAgent(
39713
+ cmd.agent ?? opts.agent ?? resolveOrchestratorDefaults().agent
39714
+ );
39715
+ emitControl(opts, { op: "opened", ...createPhoneChat(agent) });
39716
+ } catch (err) {
39717
+ emitControl(opts, { op: "error", message: phoneError(err) });
39718
+ }
39719
+ return;
39720
+ }
39721
+ if (cmd.op === "open") {
39722
+ const thread = findPhoneChat(cmd.chatId);
39723
+ if (!thread) {
39724
+ emitControl(opts, { op: "error", message: "That agent is not on this Mac." });
39725
+ return;
39726
+ }
39727
+ if (thread.status === "archived") {
39728
+ void getOrchestrator().restore(thread.id).then(() => {
39729
+ emitControl(opts, { op: "sidebar", ...listPhoneSidebar() });
39730
+ publishOpenedChat(opts, thread.id);
39731
+ }).catch((err) => {
39732
+ emitControl(opts, { op: "error", message: phoneError(err) });
39733
+ });
39734
+ return;
39735
+ }
39736
+ publishOpenedChat(opts, thread.id);
39737
+ return;
39738
+ }
39739
+ if (cmd.op === "archive") {
39740
+ const thread = readPhoneThread(cmd.chatId);
39741
+ if (!thread) {
39742
+ emitControl(opts, { op: "error", message: "That agent is not on this Mac." });
39743
+ return;
39744
+ }
39745
+ bumpChat(thread.id);
39746
+ interruptChat(thread.id, log);
39747
+ archivePhoneChat(thread.id);
39748
+ emitControl(opts, { op: "sidebar", ...listPhoneSidebar() });
39749
+ return;
39750
+ }
39751
+ if (cmd.op === "restore") {
39752
+ const chatId2 = cmd.chatId;
39753
+ void getOrchestrator().restore(chatId2).then(() => {
39754
+ emitControl(opts, { op: "sidebar", ...listPhoneSidebar() });
39755
+ emitControl(opts, { op: "restored", chatId: chatId2 });
39756
+ }).catch((err) => {
39757
+ emitControl(opts, {
39758
+ op: "error",
39759
+ chatId: chatId2,
39760
+ message: err instanceof Error ? err.message : String(err)
39761
+ });
39762
+ });
39763
+ return;
39764
+ }
39765
+ if (cmd.op === "media") {
39766
+ const thread = readPhoneThread(cmd.chatId);
39767
+ const dataUrl = thread ? phoneImageDataUrl(thread.worktreePath, cmd.path) : null;
39768
+ emitControl(opts, {
39769
+ op: "media",
39770
+ chatId: cmd.chatId,
39771
+ path: cmd.path,
39772
+ ...dataUrl ? { dataUrl } : {}
39773
+ });
39774
+ return;
39775
+ }
39776
+ if (cmd.op === "open-file") {
39777
+ const thread = readPhoneThread(cmd.chatId);
39778
+ const relative = thread ? phoneWorktreeRelative(thread.worktreePath, cmd.path) : null;
39779
+ if (!thread || !relative) {
39780
+ emitControl(opts, { op: "error", chatId: cmd.chatId, message: "That file is not in this worktree." });
39781
+ return;
39782
+ }
39783
+ void getOrchestrator().statPath(thread.id, relative).then((kind) => {
39784
+ if (kind === "missing") {
39785
+ emitControl(opts, { op: "error", chatId: cmd.chatId, message: "That file is not in this worktree." });
39786
+ return;
39787
+ }
39788
+ opts.onOpenWorktreeFile?.({
39789
+ threadId: thread.id,
39790
+ path: relative,
39791
+ ...kind === "dir" ? { directory: true } : {},
39792
+ ...cmd.startLine != null ? { startLine: cmd.startLine } : {},
39793
+ ...cmd.endLine != null ? { endLine: cmd.endLine } : {}
39794
+ });
39795
+ }).catch(() => {
39796
+ emitControl(opts, { op: "error", chatId: cmd.chatId, message: "That file is not in this worktree." });
39797
+ });
39798
+ return;
39799
+ }
39800
+ if (cmd.op === "stop") {
39801
+ if (!readPhoneThread(cmd.chatId)) {
39802
+ emitControl(opts, { op: "error", chatId: cmd.chatId, message: "That agent is not on this Mac." });
39803
+ return;
39804
+ }
39805
+ bumpChat(cmd.chatId);
39806
+ interruptChat(cmd.chatId, log);
39807
+ emitControl(opts, { op: "stopped", chatId: cmd.chatId });
39808
+ return;
39809
+ }
39810
+ const chatId = cmd.chatId;
39811
+ const generation = bumpChat(chatId);
39812
+ phoneTurnGen.set(chatId, generation);
39813
+ interruptChat(chatId, log);
39814
+ enqueueChat(chatId, async () => {
39815
+ if (chatGeneration.get(chatId) !== generation) return;
39816
+ const thread = readPhoneThread(chatId);
39817
+ if (!thread) {
39818
+ emitControl(opts, { op: "error", chatId, message: "That agent is not on this Mac." });
39819
+ return;
39820
+ }
39821
+ if (thread.status === "running" || thread.status === "queued") {
39822
+ try {
39823
+ getOrchestrator().stop(thread.id, { clearQueue: true });
39824
+ } catch {
39825
+ }
39826
+ }
39827
+ if (chatGeneration.get(chatId) !== generation) return;
39828
+ const stopStream = watchPhoneStream(opts, chatId, thread.id, generation);
39829
+ try {
39830
+ try {
39831
+ applyPhoneOptions(thread.id, cmd);
39832
+ } catch (err) {
39833
+ emitControl(opts, { op: "error", chatId, message: phoneError(err) });
39834
+ }
39835
+ const live = readPhoneThread(thread.id) ?? thread;
39836
+ const staged = stagePhoneTurnFiles(live.worktreePath, cmd);
39837
+ const text6 = cmd.text.trim() || (staged.length ? "See the attached files." : "");
39838
+ const formatted = formatRemotePrompt(text6);
39839
+ await getOrchestrator().send(thread.id, formatted, { attachments: staged });
39840
+ await getOrchestrator().waitForTurn(thread.id, 14 * 60 * 1e3);
39841
+ } catch (err) {
39842
+ if (chatGeneration.get(chatId) !== generation) return;
39843
+ const message = err instanceof Error ? err.message : String(err);
39844
+ emitControl(opts, { op: "error", chatId, message: `Sideboard failed: ${message}` });
39845
+ return;
39846
+ } finally {
39847
+ stopStream();
39848
+ finishPhoneTurn(chatId, generation);
39849
+ }
39850
+ if (chatGeneration.get(chatId) !== generation) return;
39851
+ emitChatTurn(opts, chatId, thread.id);
39852
+ });
39853
+ }
38792
39854
  function handleRemoteInbound(text6, opts) {
38793
- const generation = ++inboundGeneration;
38794
39855
  const log = opts.onLog ?? (() => void 0);
38795
39856
  const body = text6.trim();
39857
+ const control = takePhoneControl(body);
39858
+ if (control) {
39859
+ if (control === "invalid") {
39860
+ emitControl(opts, { op: "error", message: "invalid phone command" });
39861
+ return;
39862
+ }
39863
+ handlePhoneControl(control, opts);
39864
+ return;
39865
+ }
39866
+ const generation = ++inboundGeneration;
38796
39867
  if (body) interruptRemoteCoordinator(opts.deviceId, log);
38797
39868
  enqueue(async () => {
38798
39869
  if (generation !== inboundGeneration) return;
@@ -38907,7 +39978,8 @@ function runRemoteHost(opts) {
38907
39978
  type: "host_register",
38908
39979
  deviceId: opts.deviceId,
38909
39980
  deviceLabel,
38910
- hostSecret: opts.hostSecret
39981
+ hostSecret: opts.hostSecret,
39982
+ ...opts.accountToken?.trim() ? { accountToken: opts.accountToken.trim() } : {}
38911
39983
  });
38912
39984
  };
38913
39985
  const loop = async () => {
@@ -39015,7 +40087,8 @@ function runRemoteHost(opts) {
39015
40087
  handleRemoteInbound("stop", {
39016
40088
  deviceId: opts.deviceId,
39017
40089
  onLog: log,
39018
- onOutbound: emit
40090
+ onOutbound: emit,
40091
+ onOpenWorktreeFile: opts.onOpenWorktreeFile
39019
40092
  });
39020
40093
  return;
39021
40094
  }
@@ -39023,7 +40096,9 @@ function runRemoteHost(opts) {
39023
40096
  handleRemoteInbound(msg.text, {
39024
40097
  deviceId: opts.deviceId,
39025
40098
  onLog: log,
39026
- onOutbound: emit
40099
+ onOutbound: emit,
40100
+ transcribeWav: opts.transcribeWav,
40101
+ onOpenWorktreeFile: opts.onOpenWorktreeFile
39027
40102
  });
39028
40103
  }
39029
40104
  };
@@ -39050,6 +40125,671 @@ function runRemoteHost(opts) {
39050
40125
  };
39051
40126
  }
39052
40127
 
40128
+ // src/relay/account-login.ts
40129
+ var import_node_crypto27 = require("crypto");
40130
+
40131
+ // src/relay/accounts.ts
40132
+ var import_node_crypto26 = require("crypto");
40133
+ var import_node_fs70 = require("fs");
40134
+ var import_node_module7 = require("module");
40135
+ var import_node_path68 = require("path");
40136
+ var GITHUB_ACCOUNT_ISSUER = "https://github.com";
40137
+ var RELAY_DEVICE_CAP = 3;
40138
+ function thisModuleFile3() {
40139
+ const cjsFile = typeof __filename !== "undefined" ? __filename : "";
40140
+ return cjsFile || process.argv[1] || (0, import_node_path68.join)(process.cwd(), "package.json");
40141
+ }
40142
+ function openDatabase(filePath) {
40143
+ const req = (0, import_node_module7.createRequire)(thisModuleFile3());
40144
+ const specifier = "node:sqlite";
40145
+ const { DatabaseSync: SqliteDatabase } = req(specifier);
40146
+ const db = new SqliteDatabase(filePath);
40147
+ db.exec("PRAGMA journal_mode = WAL");
40148
+ return db;
40149
+ }
40150
+ function sha256(value) {
40151
+ return (0, import_node_crypto26.createHash)("sha256").update(value).digest("hex");
40152
+ }
40153
+ function sameHash(a, b) {
40154
+ const left = Buffer.from(a);
40155
+ const right = Buffer.from(b);
40156
+ return left.length === right.length && (0, import_node_crypto26.timingSafeEqual)(left, right);
40157
+ }
40158
+ function normalizeEmail(email) {
40159
+ const trimmed = email?.trim().toLowerCase() ?? "";
40160
+ return trimmed || null;
40161
+ }
40162
+ function longestMountPoint(dir, mounts) {
40163
+ let best = "/";
40164
+ for (const line of mounts.split("\n")) {
40165
+ const point = line.split(" ")[1]?.replace(/\\040/g, " ");
40166
+ if (!point) continue;
40167
+ if (dir === point || dir.startsWith(point.endsWith("/") ? point : `${point}/`)) {
40168
+ if (point.length > best.length) best = point;
40169
+ }
40170
+ }
40171
+ return best;
40172
+ }
40173
+ function assertRelayAccountsDurable(filePath) {
40174
+ if (process.env.SIDEBOARD_RELAY_ACCOUNTS_EPHEMERAL === "1") return;
40175
+ if (process.env.NODE_ENV !== "production") return;
40176
+ if (process.platform !== "linux") return;
40177
+ const dir = (0, import_node_fs70.realpathSync)((0, import_node_path68.dirname)(filePath));
40178
+ const mounts = (0, import_node_fs70.readFileSync)("/proc/mounts", "utf8");
40179
+ const point = longestMountPoint(dir, mounts);
40180
+ if (point === "/") {
40181
+ throw new Error(
40182
+ `Relay accounts at ${filePath} are on the machine root filesystem. Mount a Fly volume so sign-ins survive deploys.`
40183
+ );
40184
+ }
40185
+ }
40186
+ var RelayAccountStore = class {
40187
+ db;
40188
+ constructor(filePath) {
40189
+ (0, import_node_fs70.mkdirSync)((0, import_node_path68.dirname)(filePath), { recursive: true });
40190
+ this.db = openDatabase(filePath);
40191
+ this.db.exec(`
40192
+ CREATE TABLE IF NOT EXISTS accounts (
40193
+ id TEXT PRIMARY KEY,
40194
+ created_at INTEGER NOT NULL,
40195
+ device_cap INTEGER NOT NULL
40196
+ );
40197
+ CREATE TABLE IF NOT EXISTS identities (
40198
+ issuer TEXT NOT NULL,
40199
+ subject TEXT NOT NULL,
40200
+ account_id TEXT NOT NULL,
40201
+ email TEXT,
40202
+ email_verified INTEGER NOT NULL,
40203
+ PRIMARY KEY (issuer, subject)
40204
+ );
40205
+ CREATE INDEX IF NOT EXISTS identities_email ON identities(email);
40206
+ CREATE TABLE IF NOT EXISTS credentials (
40207
+ token_hash TEXT PRIMARY KEY,
40208
+ account_id TEXT NOT NULL,
40209
+ created_at INTEGER NOT NULL
40210
+ );
40211
+ CREATE TABLE IF NOT EXISTS devices (
40212
+ device_id TEXT PRIMARY KEY,
40213
+ account_id TEXT NOT NULL,
40214
+ host_secret_hash TEXT NOT NULL,
40215
+ device_label TEXT NOT NULL,
40216
+ created_at INTEGER NOT NULL
40217
+ );
40218
+ `);
40219
+ }
40220
+ close() {
40221
+ this.db.close();
40222
+ }
40223
+ /**
40224
+ * First sign-in creates an account. The same issuer+subject returns that
40225
+ * account. A verified email match attaches a second provider. `linkAccountId`
40226
+ * attaches while that account’s credential is already signed in.
40227
+ * Each call mints a new Mac credential. Older credentials stay valid.
40228
+ */
40229
+ signIn(identity, linkAccountId) {
40230
+ const issuer = identity.issuer.trim();
40231
+ const subject = identity.subject.trim();
40232
+ if (!issuer || !subject) throw new Error("Sign-in is missing an identity.");
40233
+ const email = normalizeEmail(identity.email);
40234
+ const verified = Boolean(identity.emailVerified && email);
40235
+ const link = linkAccountId?.trim() || null;
40236
+ this.db.exec("BEGIN");
40237
+ try {
40238
+ const existing = this.db.prepare("SELECT account_id, email FROM identities WHERE issuer = ? AND subject = ?").get(issuer, subject);
40239
+ let accountId;
40240
+ if (existing) {
40241
+ if (link && link !== existing.account_id) {
40242
+ throw new Error("That sign-in already belongs to another Sideboard account.");
40243
+ }
40244
+ accountId = existing.account_id;
40245
+ this.requireAccount(accountId);
40246
+ if (email) {
40247
+ this.db.prepare("UPDATE identities SET email = ?, email_verified = ? WHERE issuer = ? AND subject = ?").run(email, verified ? 1 : 0, issuer, subject);
40248
+ }
40249
+ } else if (link) {
40250
+ this.requireAccount(link);
40251
+ accountId = link;
40252
+ this.insertIdentity(issuer, subject, accountId, email, verified);
40253
+ } else {
40254
+ const byEmail = verified ? this.accountIdForVerifiedEmail(email) : void 0;
40255
+ accountId = byEmail ?? (0, import_node_crypto26.randomUUID)();
40256
+ if (!byEmail) {
40257
+ this.db.prepare("INSERT INTO accounts (id, created_at, device_cap) VALUES (?, ?, ?)").run(accountId, Date.now(), RELAY_DEVICE_CAP);
40258
+ }
40259
+ this.insertIdentity(issuer, subject, accountId, email, verified);
40260
+ }
40261
+ const credential = this.mintCredential(accountId);
40262
+ 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);
40263
+ this.db.exec("COMMIT");
40264
+ return { accountId, credential, email: stored?.email ?? email };
40265
+ } catch (err) {
40266
+ this.db.exec("ROLLBACK");
40267
+ throw err;
40268
+ }
40269
+ }
40270
+ verifyCredential(token) {
40271
+ const trimmed = token.trim();
40272
+ if (!trimmed) return null;
40273
+ const row = this.db.prepare("SELECT account_id FROM credentials WHERE token_hash = ?").get(sha256(trimmed));
40274
+ return row?.account_id ?? null;
40275
+ }
40276
+ /** Drop one Mac credential. The account and its other Macs stay. */
40277
+ logout(token) {
40278
+ const trimmed = token.trim();
40279
+ if (!trimmed) return;
40280
+ this.db.prepare("DELETE FROM credentials WHERE token_hash = ?").run(sha256(trimmed));
40281
+ }
40282
+ /** Delete the account and every identity, credential, and Mac bound to it. */
40283
+ revokeAccount(accountId) {
40284
+ const id = accountId.trim();
40285
+ if (!id) return;
40286
+ this.db.exec("BEGIN");
40287
+ try {
40288
+ this.db.prepare("DELETE FROM devices WHERE account_id = ?").run(id);
40289
+ this.db.prepare("DELETE FROM credentials WHERE account_id = ?").run(id);
40290
+ this.db.prepare("DELETE FROM identities WHERE account_id = ?").run(id);
40291
+ this.db.prepare("DELETE FROM accounts WHERE id = ?").run(id);
40292
+ this.db.exec("COMMIT");
40293
+ } catch (err) {
40294
+ this.db.exec("ROLLBACK");
40295
+ throw err;
40296
+ }
40297
+ }
40298
+ /**
40299
+ * Accept `host_register` for a signed-in account. The device row survives
40300
+ * process restart, so a later register with a different host secret is rejected
40301
+ * and a new device id counts against the cap.
40302
+ */
40303
+ authorizeHost(input) {
40304
+ const deviceId = input.deviceId.trim();
40305
+ const hostSecret = input.hostSecret.trim();
40306
+ const deviceLabel = input.deviceLabel.trim() || "This Mac";
40307
+ if (!deviceId || !hostSecret) return { ok: false, message: "sign in required" };
40308
+ const accountId = this.verifyCredential(input.accountToken);
40309
+ if (!accountId) return { ok: false, message: "sign in required" };
40310
+ this.db.exec("BEGIN");
40311
+ try {
40312
+ const account = this.db.prepare("SELECT id, device_cap FROM accounts WHERE id = ?").get(accountId);
40313
+ if (!account) {
40314
+ this.db.exec("ROLLBACK");
40315
+ return { ok: false, message: "sign in required" };
40316
+ }
40317
+ const device = this.db.prepare("SELECT device_id, account_id, host_secret_hash FROM devices WHERE device_id = ?").get(deviceId);
40318
+ const secretHash = sha256(hostSecret);
40319
+ if (!device) {
40320
+ const count = this.db.prepare("SELECT COUNT(*) AS n FROM devices WHERE account_id = ?").get(accountId);
40321
+ if (count.n >= account.device_cap) {
40322
+ this.db.exec("ROLLBACK");
40323
+ return { ok: false, message: "This account already has the maximum number of Macs." };
40324
+ }
40325
+ this.db.prepare(
40326
+ "INSERT INTO devices (device_id, account_id, host_secret_hash, device_label, created_at) VALUES (?, ?, ?, ?, ?)"
40327
+ ).run(deviceId, accountId, secretHash, deviceLabel, Date.now());
40328
+ this.db.exec("COMMIT");
40329
+ return { ok: true, accountId };
40330
+ }
40331
+ if (device.account_id !== accountId) {
40332
+ this.db.exec("ROLLBACK");
40333
+ return { ok: false, message: "This Mac is registered to another account." };
40334
+ }
40335
+ if (!sameHash(device.host_secret_hash, secretHash)) {
40336
+ this.db.exec("ROLLBACK");
40337
+ return { ok: false, message: "host secret rejected" };
40338
+ }
40339
+ this.db.prepare("UPDATE devices SET device_label = ? WHERE device_id = ?").run(deviceLabel, deviceId);
40340
+ this.db.exec("COMMIT");
40341
+ return { ok: true, accountId };
40342
+ } catch (err) {
40343
+ this.db.exec("ROLLBACK");
40344
+ throw err;
40345
+ }
40346
+ }
40347
+ requireAccount(accountId) {
40348
+ const row = this.db.prepare("SELECT id FROM accounts WHERE id = ?").get(accountId);
40349
+ if (!row) throw new Error("That Sideboard account is no longer on the relay.");
40350
+ }
40351
+ accountIdForVerifiedEmail(email) {
40352
+ const row = this.db.prepare(
40353
+ "SELECT account_id FROM identities WHERE email_verified = 1 AND email = ? LIMIT 1"
40354
+ ).get(email);
40355
+ return row?.account_id;
40356
+ }
40357
+ insertIdentity(issuer, subject, accountId, email, verified) {
40358
+ this.db.prepare(
40359
+ "INSERT INTO identities (issuer, subject, account_id, email, email_verified) VALUES (?, ?, ?, ?, ?)"
40360
+ ).run(issuer, subject, accountId, email, verified ? 1 : 0);
40361
+ }
40362
+ mintCredential(accountId) {
40363
+ const credential = (0, import_node_crypto26.randomBytes)(32).toString("hex");
40364
+ this.db.prepare("INSERT INTO credentials (token_hash, account_id, created_at) VALUES (?, ?, ?)").run(sha256(credential), accountId, Date.now());
40365
+ return credential;
40366
+ }
40367
+ };
40368
+
40369
+ // src/relay/account-oauth.ts
40370
+ var RELAY_GIT_HOSTS = ["github"];
40371
+ var REMOTE_LOGIN_PATH = "/remote/login";
40372
+ var REMOTE_OAUTH_CALLBACK_PATH = "/remote/oauth/callback";
40373
+ var REMOTE_OAUTH_RESULT_PATH = "/remote/oauth/result";
40374
+ var REMOTE_LOGOUT_PATH = "/remote/logout";
40375
+ var STATE_RE = /^[A-Za-z0-9_-]{16,128}$/;
40376
+ var LOGIN_TTL_MS = 10 * 60 * 1e3;
40377
+ var RelayLoginSessions = class {
40378
+ waiting = /* @__PURE__ */ new Map();
40379
+ results = /* @__PURE__ */ new Map();
40380
+ begin(state, provider, accountId) {
40381
+ this.sweep();
40382
+ this.waiting.set(state, { provider, accountId, expiresAt: Date.now() + LOGIN_TTL_MS });
40383
+ }
40384
+ takeLogin(state) {
40385
+ this.sweep();
40386
+ const row = this.waiting.get(state);
40387
+ if (!row) return null;
40388
+ this.waiting.delete(state);
40389
+ if (row.expiresAt < Date.now()) return null;
40390
+ return row;
40391
+ }
40392
+ putResult(state, value) {
40393
+ this.results.set(state, { value, expiresAt: Date.now() + LOGIN_TTL_MS });
40394
+ }
40395
+ /** `undefined` means the browser flow has not finished. */
40396
+ takeResult(state) {
40397
+ this.sweep();
40398
+ const row = this.results.get(state);
40399
+ if (!row) return this.waiting.has(state) ? void 0 : null;
40400
+ this.results.delete(state);
40401
+ if (row.expiresAt < Date.now()) return null;
40402
+ return row.value;
40403
+ }
40404
+ sweep() {
40405
+ const now = Date.now();
40406
+ for (const [state, row] of this.waiting) {
40407
+ if (row.expiresAt < now) this.waiting.delete(state);
40408
+ }
40409
+ for (const [state, row] of this.results) {
40410
+ if (row.expiresAt < now) this.results.delete(state);
40411
+ }
40412
+ }
40413
+ };
40414
+ var GIT_HOST_AUTH = {
40415
+ github: {
40416
+ label: "GitHub",
40417
+ authorizeUrl: "https://github.com/login/oauth/authorize",
40418
+ scope: "user:email",
40419
+ fetchIdentity: fetchGithubIdentity
40420
+ }
40421
+ };
40422
+ function providerOf(value) {
40423
+ return RELAY_GIT_HOSTS.includes(value) ? value : null;
40424
+ }
40425
+ function relayAuthorizeUrl(provider, client, redirectUri, state) {
40426
+ const host = GIT_HOST_AUTH[provider];
40427
+ const url = new URL(host.authorizeUrl);
40428
+ url.searchParams.set("client_id", client.clientId);
40429
+ url.searchParams.set("redirect_uri", redirectUri);
40430
+ url.searchParams.set("scope", host.scope);
40431
+ url.searchParams.set("state", state);
40432
+ return url.toString();
40433
+ }
40434
+ async function readJson2(res) {
40435
+ const text6 = await res.text();
40436
+ if (!text6) return {};
40437
+ try {
40438
+ return JSON.parse(text6);
40439
+ } catch {
40440
+ throw new Error("Sign-in provider returned a non-JSON response.");
40441
+ }
40442
+ }
40443
+ function accessToken(body) {
40444
+ const token = body.access_token;
40445
+ if (typeof token !== "string" || !token.trim()) {
40446
+ const message = body.error_description;
40447
+ throw new Error(typeof message === "string" && message.trim() ? message : "Sign-in provider rejected the code.");
40448
+ }
40449
+ return token.trim();
40450
+ }
40451
+ async function fetchGithubIdentity(client, code, redirectUri, fetchImpl) {
40452
+ const tokenRes = await fetchImpl("https://github.com/login/oauth/access_token", {
40453
+ method: "POST",
40454
+ headers: { Accept: "application/json", "Content-Type": "application/json" },
40455
+ body: JSON.stringify({
40456
+ client_id: client.clientId,
40457
+ client_secret: client.clientSecret,
40458
+ code,
40459
+ redirect_uri: redirectUri
40460
+ })
40461
+ });
40462
+ const token = accessToken(await readJson2(tokenRes));
40463
+ const userRes = await fetchImpl("https://api.github.com/user", {
40464
+ headers: { Authorization: `Bearer ${token}`, Accept: "application/vnd.github+json", "User-Agent": "sideboard-relay" }
40465
+ });
40466
+ const user = await readJson2(userRes);
40467
+ const subject = typeof user.id === "number" ? String(user.id) : "";
40468
+ if (!subject) throw new Error("GitHub did not return an account id.");
40469
+ const emailRes = await fetchImpl("https://api.github.com/user/emails", {
40470
+ headers: { Authorization: `Bearer ${token}`, Accept: "application/vnd.github+json", "User-Agent": "sideboard-relay" }
40471
+ });
40472
+ const emails = await readJson2(emailRes);
40473
+ const list = Array.isArray(emails) ? emails : [];
40474
+ const primary = list.find((row) => row.primary === true && row.verified === true);
40475
+ const anyVerified = list.find((row) => row.verified === true);
40476
+ const chosen = primary ?? anyVerified;
40477
+ const email = chosen && typeof chosen.email === "string" ? chosen.email : null;
40478
+ return { issuer: GITHUB_ACCOUNT_ISSUER, subject, email, emailVerified: Boolean(email) };
40479
+ }
40480
+ async function fetchRelayIdentity(provider, client, code, redirectUri, fetchImpl) {
40481
+ return GIT_HOST_AUTH[provider].fetchIdentity(client, code, redirectUri, fetchImpl);
40482
+ }
40483
+ async function readBody(req) {
40484
+ const chunks = [];
40485
+ let size = 0;
40486
+ for await (const chunk of req) {
40487
+ const buf = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
40488
+ size += buf.length;
40489
+ if (size > 8192) throw new Error("body too large");
40490
+ chunks.push(buf);
40491
+ }
40492
+ const text6 = Buffer.concat(chunks).toString("utf8").trim();
40493
+ if (!text6) return {};
40494
+ return JSON.parse(text6);
40495
+ }
40496
+ function sendJson2(res, status, body) {
40497
+ res.writeHead(status, { "Content-Type": "application/json", "Cache-Control": "no-store" });
40498
+ res.end(JSON.stringify(body));
40499
+ }
40500
+ function relayOAuthReturnPage(title, body) {
40501
+ return `<!DOCTYPE html>
40502
+ <html lang="en">
40503
+ <head>
40504
+ <meta charset="utf-8">
40505
+ <meta name="viewport" content="width=device-width, initial-scale=1">
40506
+ <meta name="robots" content="noindex">
40507
+ <title>${escapeHtml2(title)}</title>
40508
+ <style>
40509
+ :root {
40510
+ --bg: #09090b;
40511
+ --card: #16171b;
40512
+ --line: rgba(255, 255, 255, 0.08);
40513
+ --text: #f4f5f7;
40514
+ --muted: #9aa3ad;
40515
+ --blue: #3b9eff;
40516
+ --blue-fill: #0f7ed4;
40517
+ }
40518
+ * { box-sizing: border-box }
40519
+ body {
40520
+ margin: 0;
40521
+ min-height: 100vh;
40522
+ display: grid;
40523
+ place-items: center;
40524
+ padding: 48px 24px;
40525
+ background:
40526
+ radial-gradient(closest-side, rgba(15, 126, 212, 0.22), transparent 72%) center -120px / 720px 480px no-repeat,
40527
+ var(--bg);
40528
+ color: var(--text);
40529
+ font-family: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", "Helvetica Neue", sans-serif;
40530
+ -webkit-font-smoothing: antialiased;
40531
+ }
40532
+ main {
40533
+ width: min(28rem, 100%);
40534
+ background: var(--card);
40535
+ border: 1px solid var(--line);
40536
+ border-radius: 16px;
40537
+ padding: 28px 28px 32px;
40538
+ }
40539
+ .brand {
40540
+ display: flex;
40541
+ align-items: center;
40542
+ gap: 10px;
40543
+ margin: 0 0 22px;
40544
+ font-weight: 650;
40545
+ font-size: 17px;
40546
+ letter-spacing: -0.02em;
40547
+ }
40548
+ .mark { position: relative; display: inline-block; width: 26px; height: 26px; flex: none }
40549
+ .mark i { position: absolute; width: 18px; height: 18px; border-radius: 3px; transform: rotate(14deg) }
40550
+ .mark i:first-child { left: 6px; top: 6px; background: var(--blue-fill) }
40551
+ .mark i:last-child { left: 1px; top: 1px; border: 2px solid #d7dbdf }
40552
+ h1 { margin: 0; font-size: 1.5rem; line-height: 1.2; letter-spacing: -0.03em; font-weight: 650 }
40553
+ p { margin: 10px 0 0; line-height: 1.55; color: var(--muted) }
40554
+ a { color: var(--blue) }
40555
+ </style>
40556
+ </head>
40557
+ <body>
40558
+ <main>
40559
+ <div class="brand"><span class="mark" aria-hidden="true"><i></i><i></i></span>Sideboard</div>
40560
+ ${body}
40561
+ </main>
40562
+ </body>
40563
+ </html>`;
40564
+ }
40565
+ function sendHtml(res, status, title, body) {
40566
+ res.writeHead(status, { "Content-Type": "text/html; charset=utf-8", "Cache-Control": "no-store" });
40567
+ res.end(relayOAuthReturnPage(title, body));
40568
+ }
40569
+ async function handleRelayAccountHttp(opts) {
40570
+ const reqUrl = opts.req.url || "/";
40571
+ const path2 = reqUrl.split("?")[0];
40572
+ const method = opts.req.method || "GET";
40573
+ const origin = opts.publicOrigin.replace(/\/+$/, "");
40574
+ const redirectUri = `${origin}${REMOTE_OAUTH_CALLBACK_PATH}`;
40575
+ const fetchImpl = opts.fetchImpl ?? fetch;
40576
+ if (method === "POST" && path2 === REMOTE_LOGIN_PATH) {
40577
+ let body;
40578
+ try {
40579
+ body = await readBody(opts.req);
40580
+ } catch {
40581
+ sendJson2(opts.res, 400, { ok: false, error: "invalid login request" });
40582
+ return true;
40583
+ }
40584
+ const provider = providerOf(typeof body.provider === "string" ? body.provider : "");
40585
+ const state = typeof body.state === "string" ? body.state.trim() : "";
40586
+ if (!provider || !STATE_RE.test(state)) {
40587
+ sendJson2(opts.res, 400, { ok: false, error: "invalid login request" });
40588
+ return true;
40589
+ }
40590
+ const client = opts.oauth[provider];
40591
+ if (!client?.clientId || !client.clientSecret) {
40592
+ sendJson2(opts.res, 503, {
40593
+ ok: false,
40594
+ error: `${GIT_HOST_AUTH[provider].label} sign-in is not configured on this relay.`
40595
+ });
40596
+ return true;
40597
+ }
40598
+ let accountId = null;
40599
+ const linkToken = typeof body.accountToken === "string" ? body.accountToken.trim() : "";
40600
+ if (linkToken) {
40601
+ accountId = opts.store.verifyCredential(linkToken);
40602
+ if (!accountId) {
40603
+ sendJson2(opts.res, 401, { ok: false, error: "sign in required" });
40604
+ return true;
40605
+ }
40606
+ }
40607
+ opts.sessions.begin(state, provider, accountId);
40608
+ sendJson2(opts.res, 200, { ok: true, url: relayAuthorizeUrl(provider, client, redirectUri, state) });
40609
+ return true;
40610
+ }
40611
+ if (method === "GET" && path2 === REMOTE_OAUTH_CALLBACK_PATH) {
40612
+ const url = new URL(reqUrl, origin);
40613
+ const state = url.searchParams.get("state")?.trim() ?? "";
40614
+ const code = url.searchParams.get("code")?.trim() ?? "";
40615
+ const providerError = url.searchParams.get("error")?.trim() ?? "";
40616
+ const pending = STATE_RE.test(state) ? opts.sessions.takeLogin(state) : null;
40617
+ if (!pending) {
40618
+ sendHtml(opts.res, 400, "Sideboard", "<h1>Sign-in expired</h1><p>Return to Sideboard and try again.</p>");
40619
+ return true;
40620
+ }
40621
+ if (providerError || !code) {
40622
+ const message = providerError || "Sign-in was cancelled.";
40623
+ opts.sessions.putResult(state, { ok: false, error: message });
40624
+ sendHtml(opts.res, 400, "Sideboard", `<h1>Could not sign in</h1><p>${escapeHtml2(message)}</p>`);
40625
+ return true;
40626
+ }
40627
+ const client = opts.oauth[pending.provider];
40628
+ if (!client) {
40629
+ opts.sessions.putResult(state, { ok: false, error: "Sign-in is not configured on this relay." });
40630
+ sendHtml(opts.res, 503, "Sideboard", "<h1>Sign-in is not configured</h1>");
40631
+ return true;
40632
+ }
40633
+ try {
40634
+ const identity = await fetchRelayIdentity(pending.provider, client, code, redirectUri, fetchImpl);
40635
+ const signed = opts.store.signIn(identity, pending.accountId);
40636
+ opts.sessions.putResult(state, {
40637
+ ok: true,
40638
+ credential: signed.credential,
40639
+ email: signed.email,
40640
+ accountId: signed.accountId
40641
+ });
40642
+ sendHtml(
40643
+ opts.res,
40644
+ 200,
40645
+ "Sideboard",
40646
+ "<h1>Signed in</h1><p>Return to Sideboard. This window can close.</p>"
40647
+ );
40648
+ } catch (err) {
40649
+ const message = err instanceof Error ? err.message : String(err);
40650
+ opts.sessions.putResult(state, { ok: false, error: message });
40651
+ sendHtml(opts.res, 400, "Sideboard", `<h1>Could not sign in</h1><p>${escapeHtml2(message)}</p>`);
40652
+ }
40653
+ return true;
40654
+ }
40655
+ if (method === "GET" && path2 === REMOTE_OAUTH_RESULT_PATH) {
40656
+ const url = new URL(reqUrl, origin);
40657
+ const state = url.searchParams.get("state")?.trim() ?? "";
40658
+ if (!STATE_RE.test(state)) {
40659
+ sendJson2(opts.res, 400, { ok: false, error: "invalid login request" });
40660
+ return true;
40661
+ }
40662
+ const result = opts.sessions.takeResult(state);
40663
+ if (result === void 0 || result === null) {
40664
+ sendJson2(opts.res, 404, { ok: false, error: "pending" });
40665
+ return true;
40666
+ }
40667
+ if (!result.ok) {
40668
+ sendJson2(opts.res, 400, { ok: false, error: result.error });
40669
+ return true;
40670
+ }
40671
+ sendJson2(opts.res, 200, {
40672
+ ok: true,
40673
+ credential: result.credential,
40674
+ email: result.email,
40675
+ accountId: result.accountId
40676
+ });
40677
+ return true;
40678
+ }
40679
+ if (method === "POST" && path2 === REMOTE_LOGOUT_PATH) {
40680
+ let body;
40681
+ try {
40682
+ body = await readBody(opts.req);
40683
+ } catch {
40684
+ sendJson2(opts.res, 400, { ok: false, error: "invalid logout request" });
40685
+ return true;
40686
+ }
40687
+ const token = typeof body.accountToken === "string" ? body.accountToken : "";
40688
+ opts.store.logout(token);
40689
+ sendJson2(opts.res, 200, { ok: true });
40690
+ return true;
40691
+ }
40692
+ return false;
40693
+ }
40694
+
40695
+ // src/relay/account-login.ts
40696
+ var RemoteAccountLoginCancelled = class extends Error {
40697
+ constructor() {
40698
+ super("Sign-in cancelled");
40699
+ this.name = "RemoteAccountLoginCancelled";
40700
+ }
40701
+ };
40702
+ function isRemoteAccountLoginCancelled(err) {
40703
+ return err instanceof RemoteAccountLoginCancelled;
40704
+ }
40705
+ function remoteAccountHttpOrigin(wsUrl = remoteRelayUrl()) {
40706
+ const url = new URL(wsUrl);
40707
+ url.protocol = url.protocol === "wss:" ? "https:" : "http:";
40708
+ url.pathname = "";
40709
+ url.search = "";
40710
+ url.hash = "";
40711
+ return url.origin;
40712
+ }
40713
+ function sleep2(ms, signal) {
40714
+ return new Promise((resolve2, reject) => {
40715
+ if (signal?.aborted) {
40716
+ reject(new RemoteAccountLoginCancelled());
40717
+ return;
40718
+ }
40719
+ const timer = setTimeout(() => {
40720
+ signal?.removeEventListener("abort", onAbort);
40721
+ resolve2();
40722
+ }, ms);
40723
+ const onAbort = () => {
40724
+ clearTimeout(timer);
40725
+ reject(new RemoteAccountLoginCancelled());
40726
+ };
40727
+ signal?.addEventListener("abort", onAbort, { once: true });
40728
+ });
40729
+ }
40730
+ async function startRemoteAccountLogin(opts) {
40731
+ if (opts.signal?.aborted) throw new RemoteAccountLoginCancelled();
40732
+ const origin = (opts.origin ?? remoteAccountHttpOrigin()).replace(/\/+$/, "");
40733
+ const fetchImpl = opts.fetchImpl ?? fetch;
40734
+ const state = (0, import_node_crypto27.randomBytes)(16).toString("hex");
40735
+ const timeoutMs = opts.timeoutMs ?? 5 * 6e4;
40736
+ const pollIntervalMs = opts.pollIntervalMs ?? 400;
40737
+ const linkToken = opts.linkToken?.trim() ?? "";
40738
+ const started = await fetchImpl(`${origin}${REMOTE_LOGIN_PATH}`, {
40739
+ method: "POST",
40740
+ headers: { "Content-Type": "application/json" },
40741
+ body: JSON.stringify({
40742
+ provider: opts.provider,
40743
+ state,
40744
+ ...linkToken ? { accountToken: linkToken } : {}
40745
+ })
40746
+ });
40747
+ const startBody = await started.json();
40748
+ if (!started.ok || !startBody.url) {
40749
+ throw new Error(startBody.error || "Could not start sign-in.");
40750
+ }
40751
+ await Promise.resolve(opts.openUrl?.(startBody.url));
40752
+ const deadline = Date.now() + timeoutMs;
40753
+ const resultUrl = `${origin}${REMOTE_OAUTH_RESULT_PATH}?state=${encodeURIComponent(state)}`;
40754
+ while (Date.now() < deadline) {
40755
+ if (opts.signal?.aborted) throw new RemoteAccountLoginCancelled();
40756
+ let res;
40757
+ try {
40758
+ res = await fetchImpl(resultUrl);
40759
+ } catch {
40760
+ if (opts.signal?.aborted) throw new RemoteAccountLoginCancelled();
40761
+ await sleep2(pollIntervalMs, opts.signal);
40762
+ continue;
40763
+ }
40764
+ let data;
40765
+ try {
40766
+ data = await res.json();
40767
+ } catch {
40768
+ await sleep2(pollIntervalMs, opts.signal);
40769
+ continue;
40770
+ }
40771
+ if (data.ok && data.credential && data.accountId) {
40772
+ return {
40773
+ credential: data.credential,
40774
+ email: data.email ?? null,
40775
+ accountId: data.accountId
40776
+ };
40777
+ }
40778
+ if (data.error && data.error !== "pending") throw new Error(data.error);
40779
+ await sleep2(pollIntervalMs, opts.signal);
40780
+ }
40781
+ throw new Error("Sign-in timed out.");
40782
+ }
40783
+ async function disconnectRemoteAccount(opts) {
40784
+ const origin = (opts.origin ?? remoteAccountHttpOrigin()).replace(/\/+$/, "");
40785
+ const fetchImpl = opts.fetchImpl ?? fetch;
40786
+ await fetchImpl(`${origin}${REMOTE_LOGOUT_PATH}`, {
40787
+ method: "POST",
40788
+ headers: { "Content-Type": "application/json" },
40789
+ body: JSON.stringify({ accountToken: opts.accountToken })
40790
+ });
40791
+ }
40792
+
39053
40793
  // src/slack/app-home.ts
39054
40794
  var SLACK_APP_HOME_DOWNLOAD = "https://sideboard-downloads.t3.tigrisfiles.io/Sideboard-latest-arm64.dmg";
39055
40795
  var SLACK_APP_HOME_LANDING = "https://www.sideboard.cloud/slack/";
@@ -39431,9 +41171,9 @@ var import_node_http5 = require("http");
39431
41171
  var import_ws6 = require("ws");
39432
41172
 
39433
41173
  // src/relay/static.ts
39434
- var import_node_fs70 = require("fs");
41174
+ var import_node_fs71 = require("fs");
39435
41175
  var import_promises = require("fs/promises");
39436
- var import_node_path68 = __toESM(require("path"), 1);
41176
+ var import_node_path69 = __toESM(require("path"), 1);
39437
41177
  var TYPES = {
39438
41178
  ".css": "text/css; charset=utf-8",
39439
41179
  ".html": "text/html; charset=utf-8",
@@ -39464,9 +41204,9 @@ function resolveStaticPath(root, requestUrl) {
39464
41204
  return null;
39465
41205
  }
39466
41206
  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)) {
41207
+ const rootResolved = import_node_path69.default.resolve(root);
41208
+ const candidate = import_node_path69.default.resolve(rootResolved, `.${pathname}`);
41209
+ if (candidate !== rootResolved && !candidate.startsWith(rootResolved + import_node_path69.default.sep)) {
39470
41210
  return null;
39471
41211
  }
39472
41212
  return candidate;
@@ -39480,7 +41220,7 @@ async function fileSize(file) {
39480
41220
  }
39481
41221
  }
39482
41222
  function sendFile(req, res, file, size) {
39483
- const ext = import_node_path68.default.extname(file).toLowerCase();
41223
+ const ext = import_node_path69.default.extname(file).toLowerCase();
39484
41224
  res.writeHead(200, {
39485
41225
  "Content-Type": TYPES[ext] ?? "application/octet-stream",
39486
41226
  "Content-Length": size,
@@ -39490,7 +41230,7 @@ function sendFile(req, res, file, size) {
39490
41230
  res.end();
39491
41231
  return true;
39492
41232
  }
39493
- (0, import_node_fs70.createReadStream)(file).pipe(res);
41233
+ (0, import_node_fs71.createReadStream)(file).pipe(res);
39494
41234
  return true;
39495
41235
  }
39496
41236
  async function tryServeStatic(req, res, root) {
@@ -39499,7 +41239,7 @@ async function tryServeStatic(req, res, root) {
39499
41239
  if (!candidate) return false;
39500
41240
  const direct = await fileSize(candidate);
39501
41241
  if (direct != null) return sendFile(req, res, candidate, direct);
39502
- const asIndex = import_node_path68.default.join(candidate, "index.html");
41242
+ const asIndex = import_node_path69.default.join(candidate, "index.html");
39503
41243
  const indexSize = await fileSize(asIndex);
39504
41244
  if (indexSize != null) return sendFile(req, res, asIndex, indexSize);
39505
41245
  return false;
@@ -39507,14 +41247,19 @@ async function tryServeStatic(req, res, root) {
39507
41247
 
39508
41248
  // src/relay/server.ts
39509
41249
  init_abletime_oauth();
39510
- function sendHtml(res, status, title, body) {
41250
+ function sendHtml2(res, status, title, body) {
39511
41251
  res.writeHead(status, { "Content-Type": "text/html; charset=utf-8" });
39512
41252
  res.end(slackOAuthHtmlPage(title, body));
39513
41253
  }
39514
- function sendJson2(res, status, body) {
41254
+ function sendJson3(res, status, body) {
39515
41255
  res.writeHead(status, { "Content-Type": "application/json" });
39516
41256
  res.end(JSON.stringify(body));
39517
41257
  }
41258
+ function relayClientIp(req) {
41259
+ const fly = req.headers["fly-client-ip"];
41260
+ if (typeof fly === "string" && fly.trim()) return fly.trim();
41261
+ return req.socket.remoteAddress ?? "unknown";
41262
+ }
39518
41263
  function startRelaySocketPing(ws, intervalMs) {
39519
41264
  const ping = setInterval(() => {
39520
41265
  if (ws.readyState !== ws.OPEN) {
@@ -39542,11 +41287,26 @@ async function startRelayServer(opts) {
39542
41287
  const clientSecret = (opts.clientSecret ?? process.env.SIDEBOARD_SLACK_CLIENT_SECRET ?? "").trim();
39543
41288
  const redirectUri = (opts.oauthRedirectUri ?? slackOAuthRelayRedirectUri()).trim();
39544
41289
  const pending = new SlackOAuthPendingStore();
41290
+ let boundPort = 0;
39545
41291
  const hub = opts.hub ?? new SlackRelayHub({
39546
41292
  fetchImpl: opts.fetchImpl ?? fetch,
39547
41293
  onLog: log
39548
41294
  });
39549
- const remoteHub = new RemoteHub();
41295
+ const accountsPath = opts.accountsPath?.trim() ?? "";
41296
+ if (accountsPath) assertRelayAccountsDurable(accountsPath);
41297
+ const accountStore = accountsPath ? new RelayAccountStore(accountsPath) : null;
41298
+ const accountSessions = new RelayLoginSessions();
41299
+ const relayOAuth = opts.relayOAuth ?? {};
41300
+ const authMarks = /* @__PURE__ */ new Map();
41301
+ const remoteByIp = /* @__PURE__ */ new Map();
41302
+ let remoteSockets = 0;
41303
+ const remoteHub = new RemoteHub({
41304
+ authorizeHost: accountStore ? (input) => {
41305
+ const decision = accountStore.authorizeHost(input);
41306
+ return decision.ok ? { ok: true } : { ok: false, message: decision.message };
41307
+ } : void 0,
41308
+ onAuthorized: (socket) => authMarks.get(socket)?.()
41309
+ });
39550
41310
  const handleCallback = async (reqUrl, res) => {
39551
41311
  const url2 = parseSlackOAuthCallbackUrl(reqUrl);
39552
41312
  if (!url2) return false;
@@ -39555,7 +41315,7 @@ async function startRelayServer(opts) {
39555
41315
  const code = url2.searchParams.get("code")?.trim() ?? "";
39556
41316
  if (error) {
39557
41317
  if (state) pending.put(state, { ok: false, error: `Slack OAuth: ${error}` });
39558
- sendHtml(
41318
+ sendHtml2(
39559
41319
  res,
39560
41320
  400,
39561
41321
  "Slack",
@@ -39564,7 +41324,7 @@ async function startRelayServer(opts) {
39564
41324
  return true;
39565
41325
  }
39566
41326
  if (!state || !code) {
39567
- sendHtml(res, 400, "Slack", "<h1>Invalid callback</h1><p>State or code missing.</p>");
41327
+ sendHtml2(res, 400, "Slack", "<h1>Invalid callback</h1><p>State or code missing.</p>");
39568
41328
  return true;
39569
41329
  }
39570
41330
  if (!clientSecret) {
@@ -39572,7 +41332,7 @@ async function startRelayServer(opts) {
39572
41332
  ok: false,
39573
41333
  error: "Relay is missing SIDEBOARD_SLACK_CLIENT_SECRET"
39574
41334
  });
39575
- sendHtml(
41335
+ sendHtml2(
39576
41336
  res,
39577
41337
  500,
39578
41338
  "Slack",
@@ -39595,7 +41355,7 @@ async function startRelayServer(opts) {
39595
41355
  void hub.handleAppHomeOpened(payload.team_id, payload.user_id);
39596
41356
  }
39597
41357
  }
39598
- sendHtml(
41358
+ sendHtml2(
39599
41359
  res,
39600
41360
  200,
39601
41361
  "Slack connected",
@@ -39604,7 +41364,7 @@ async function startRelayServer(opts) {
39604
41364
  } catch (err) {
39605
41365
  const message = err instanceof Error ? err.message : String(err);
39606
41366
  pending.put(state, { ok: false, error: message });
39607
- sendHtml(res, 400, "Slack", `<h1>Could not connect Slack</h1><p>${escapeHtml2(message)}</p>`);
41367
+ sendHtml2(res, 400, "Slack", `<h1>Could not connect Slack</h1><p>${escapeHtml2(message)}</p>`);
39608
41368
  }
39609
41369
  return true;
39610
41370
  };
@@ -39614,19 +41374,30 @@ async function startRelayServer(opts) {
39614
41374
  const state = url2.searchParams.get("state")?.trim() ?? "";
39615
41375
  const value = pending.take(state);
39616
41376
  if (!value) {
39617
- sendJson2(res, 404, { ok: false, error: "pending" });
41377
+ sendJson3(res, 404, { ok: false, error: "pending" });
39618
41378
  return true;
39619
41379
  }
39620
41380
  if (!value.ok) {
39621
- sendJson2(res, 400, { ok: false, error: value.error });
41381
+ sendJson3(res, 400, { ok: false, error: value.error });
39622
41382
  return true;
39623
41383
  }
39624
- sendJson2(res, 200, { ok: true, ...value.payload });
41384
+ sendJson3(res, 200, { ok: true, ...value.payload });
39625
41385
  return true;
39626
41386
  };
39627
41387
  const httpServer = (0, import_node_http5.createServer)((req, res) => {
39628
41388
  void (async () => {
39629
41389
  const reqUrl = req.url || "/";
41390
+ if (accountStore && await handleRelayAccountHttp({
41391
+ req,
41392
+ res,
41393
+ store: accountStore,
41394
+ oauth: relayOAuth,
41395
+ sessions: accountSessions,
41396
+ publicOrigin: opts.relayPublicOrigin?.trim() || process.env.SIDEBOARD_RELAY_PUBLIC_ORIGIN?.trim() || `http://127.0.0.1:${boundPort}`,
41397
+ fetchImpl: opts.fetchImpl
41398
+ })) {
41399
+ return;
41400
+ }
39630
41401
  if (await handleCallback(reqUrl, res)) return;
39631
41402
  if (handleResult(reqUrl, res)) return;
39632
41403
  const ableTimeBounce = ableTimeOAuthLocalBounceUrl(reqUrl);
@@ -39640,7 +41411,7 @@ async function startRelayServer(opts) {
39640
41411
  return;
39641
41412
  }
39642
41413
  if (req.url === "/health") {
39643
- sendJson2(res, 200, {
41414
+ sendJson3(res, 200, {
39644
41415
  ok: true,
39645
41416
  service: "sideboard-relay",
39646
41417
  sessions: hub.listSessions().length,
@@ -39659,7 +41430,7 @@ async function startRelayServer(opts) {
39659
41430
  return;
39660
41431
  }
39661
41432
  if (req.url === "/") {
39662
- sendJson2(res, 200, {
41433
+ sendJson3(res, 200, {
39663
41434
  ok: true,
39664
41435
  service: "sideboard-relay",
39665
41436
  sessions: hub.listSessions().length,
@@ -39686,6 +41457,14 @@ async function startRelayServer(opts) {
39686
41457
  socket.destroy();
39687
41458
  return;
39688
41459
  }
41460
+ if (target === remoteWss) {
41461
+ const ip = relayClientIp(req);
41462
+ const count = remoteByIp.get(ip) ?? 0;
41463
+ if (count >= 20 || remoteSockets >= 4e3) {
41464
+ socket.destroy();
41465
+ return;
41466
+ }
41467
+ }
39689
41468
  target.handleUpgrade(req, socket, head, (ws) => {
39690
41469
  target.emit("connection", ws, req);
39691
41470
  });
@@ -39720,7 +41499,10 @@ async function startRelayServer(opts) {
39720
41499
  hub.detachSocket(socket);
39721
41500
  });
39722
41501
  });
39723
- remoteWss.on("connection", (ws) => {
41502
+ remoteWss.on("connection", (ws, req) => {
41503
+ const ip = relayClientIp(req);
41504
+ remoteByIp.set(ip, (remoteByIp.get(ip) ?? 0) + 1);
41505
+ remoteSockets += 1;
39724
41506
  const socket = {
39725
41507
  send: (data) => {
39726
41508
  if (ws.readyState === ws.OPEN) ws.send(data);
@@ -39732,14 +41514,35 @@ async function startRelayServer(opts) {
39732
41514
  }
39733
41515
  }
39734
41516
  };
41517
+ let released = false;
41518
+ const authTimer = accountStore ? setTimeout(() => {
41519
+ try {
41520
+ ws.close(4001, "sign in required");
41521
+ } catch {
41522
+ }
41523
+ }, 12e4) : null;
41524
+ authMarks.set(socket, () => {
41525
+ if (authTimer) clearTimeout(authTimer);
41526
+ });
41527
+ const release = () => {
41528
+ if (released) return;
41529
+ released = true;
41530
+ if (authTimer) clearTimeout(authTimer);
41531
+ authMarks.delete(socket);
41532
+ remoteSockets -= 1;
41533
+ const next = (remoteByIp.get(ip) ?? 1) - 1;
41534
+ if (next <= 0) remoteByIp.delete(ip);
41535
+ else remoteByIp.set(ip, next);
41536
+ remoteHub.detachSocket(socket);
41537
+ };
39735
41538
  startRelaySocketPing(ws, REMOTE_PING_INTERVAL_MS);
39736
41539
  log("remote client connected");
39737
41540
  ws.on("message", (data) => {
39738
41541
  const raw = typeof data === "string" ? data : data.toString("utf8");
39739
41542
  remoteHub.handleClientMessage(socket, raw);
39740
41543
  });
39741
- ws.on("close", () => remoteHub.detachSocket(socket));
39742
- ws.on("error", () => remoteHub.detachSocket(socket));
41544
+ ws.on("close", release);
41545
+ ws.on("error", release);
39743
41546
  });
39744
41547
  const port = opts.port ?? 0;
39745
41548
  const host = opts.host ?? "0.0.0.0";
@@ -39748,7 +41551,7 @@ async function startRelayServer(opts) {
39748
41551
  httpServer.listen(port, host, () => resolve2());
39749
41552
  });
39750
41553
  const address = httpServer.address();
39751
- const boundPort = typeof address === "object" && address ? address.port : typeof port === "number" ? port : 0;
41554
+ boundPort = typeof address === "object" && address ? address.port : typeof port === "number" ? port : 0;
39752
41555
  const url = `ws://${host === "0.0.0.0" ? "127.0.0.1" : host}:${boundPort}${SLACK_RELAY_DESKTOP_PATH}`;
39753
41556
  log(`relay listening on ${url}`);
39754
41557
  const ac = new AbortController();
@@ -39791,13 +41594,14 @@ async function startRelayServer(opts) {
39791
41594
  httpServer.close((err) => err ? reject(err) : resolve2());
39792
41595
  });
39793
41596
  await socketModeDone.catch(() => void 0);
41597
+ accountStore?.close();
39794
41598
  }
39795
41599
  };
39796
41600
  }
39797
41601
 
39798
41602
  // src/slack/outbound-watch.ts
39799
- var import_node_fs71 = require("fs");
39800
- var import_node_path69 = require("path");
41603
+ var import_node_fs72 = require("fs");
41604
+ var import_node_path70 = require("path");
39801
41605
  init_paths();
39802
41606
  init_private_file();
39803
41607
  init_secure_file();
@@ -39810,7 +41614,7 @@ var POLL_INTERVAL_MS = 12e3;
39810
41614
  var lastPollMs = 0;
39811
41615
  var nameCache = /* @__PURE__ */ new Map();
39812
41616
  function storePath4() {
39813
- return (0, import_node_path69.join)(appDataDir(), "slack-outbound-watch.json");
41617
+ return (0, import_node_path70.join)(appDataDir(), "slack-outbound-watch.json");
39814
41618
  }
39815
41619
  function watchId(teamId, channelId, ts) {
39816
41620
  return `${teamId}:${channelId}:${ts}`;
@@ -39823,9 +41627,9 @@ function tsNewer(a, b) {
39823
41627
  }
39824
41628
  function readStore4() {
39825
41629
  const path2 = storePath4();
39826
- if (!(0, import_node_fs71.existsSync)(path2)) return [];
41630
+ if (!(0, import_node_fs72.existsSync)(path2)) return [];
39827
41631
  try {
39828
- const parsed = isSecureFileEncrypted(path2) ? readSecureJson(path2) : JSON.parse((0, import_node_fs71.readFileSync)(path2, "utf8"));
41632
+ const parsed = isSecureFileEncrypted(path2) ? readSecureJson(path2) : JSON.parse((0, import_node_fs72.readFileSync)(path2, "utf8"));
39829
41633
  return Array.isArray(parsed?.watches) ? parsed.watches : [];
39830
41634
  } catch {
39831
41635
  return [];
@@ -40470,6 +42274,7 @@ async function pollSlackOutboundWatches(opts) {
40470
42274
  disconnectLinear,
40471
42275
  disconnectLinearConnection,
40472
42276
  disconnectOptionalService,
42277
+ disconnectRemoteAccount,
40473
42278
  disconnectSlackWorkspace,
40474
42279
  discoverSkills,
40475
42280
  downloadAbleTimeAttachment,
@@ -40726,6 +42531,7 @@ async function pollSlackOutboundWatches(opts) {
40726
42531
  isPrNotMergeableError,
40727
42532
  isPresentPlanToolName,
40728
42533
  isPrimaryCheckoutThread,
42534
+ isRemoteAccountLoginCancelled,
40729
42535
  isRemoteStopCommand,
40730
42536
  isSelfWrittenRecord,
40731
42537
  isSessionQuotaLimit,
@@ -41087,6 +42893,7 @@ async function pollSlackOutboundWatches(opts) {
41087
42893
  startMcpServer,
41088
42894
  startOrchestration,
41089
42895
  startRelayServer,
42896
+ startRemoteAccountLogin,
41090
42897
  startSlackOAuth,
41091
42898
  statWorktreePath,
41092
42899
  stopAgentRpcServer,