@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.js CHANGED
@@ -173,7 +173,7 @@ import {
173
173
  worktreeBoardStatus,
174
174
  worktreeCleanupSettings,
175
175
  writeWorktreeFile
176
- } from "./chunk-XIVM7K77.js";
176
+ } from "./chunk-SQOAFR2U.js";
177
177
  import {
178
178
  BASE_ALLOWED_TOOLS,
179
179
  BRIGHTSY_MCP_ALLOWED_TOOLS,
@@ -263,7 +263,7 @@ import {
263
263
  whichOnPath,
264
264
  worktreeMcpToolNames,
265
265
  writeInjectedMcpConfig
266
- } from "./chunk-ADWNK2JH.js";
266
+ } from "./chunk-NIB2IZ6J.js";
267
267
  import {
268
268
  brightsyMcpServerName,
269
269
  connectBrightsyTeam,
@@ -380,7 +380,7 @@ import {
380
380
  waitForTurnToolResult,
381
381
  worktreeRunPortsPath,
382
382
  wrapReviewSkillMarkdown
383
- } from "./chunk-DVTEC4BF.js";
383
+ } from "./chunk-MGS5T2A3.js";
384
384
  import {
385
385
  STDERR_EVENT_MAX_CHARS,
386
386
  STDOUT_EVENT_MAX_CHARS,
@@ -446,7 +446,7 @@ import {
446
446
  parseAbleTimeOAuthCallbackUrl,
447
447
  resolveAbleTimeOAuthHost,
448
448
  startAbleTimeOAuth
449
- } from "./chunk-LAIBYKZS.js";
449
+ } from "./chunk-QINJKNLR.js";
450
450
  import {
451
451
  ableTimeDependencyTarget,
452
452
  commentAbleTimeTask,
@@ -471,8 +471,8 @@ import {
471
471
  toAbleTimeIssueInfo,
472
472
  updateAbleTimeTask,
473
473
  verifyAbleTimeConnection
474
- } from "./chunk-UXI52MLJ.js";
475
- import "./chunk-AGXZAMOF.js";
474
+ } from "./chunk-2J34G3WJ.js";
475
+ import "./chunk-NQ7JMVSU.js";
476
476
  import {
477
477
  ABLETIME_MCP_PATH,
478
478
  ABLETIME_MCP_PM_PATH,
@@ -493,7 +493,7 @@ import {
493
493
  normalizeAbleTimeCredential,
494
494
  normalizeAbleTimeHost,
495
495
  rewriteAbleTimeError
496
- } from "./chunk-L6CYOON4.js";
496
+ } from "./chunk-PZURG3XN.js";
497
497
  import {
498
498
  LEGACY_PLAN_FILE_REL,
499
499
  PLAN_FILE_NAME,
@@ -557,7 +557,7 @@ import {
557
557
  stageBuffersAsAttachments,
558
558
  syncWorkspacesFromThreads,
559
559
  takenTeamSlugsForOrchestration
560
- } from "./chunk-PTNGMA5C.js";
560
+ } from "./chunk-A2G3ZBJS.js";
561
561
  import {
562
562
  FAMOUS_SOCCER_TEAMS,
563
563
  GITHUB_PR_BODY_MAX_CHARS,
@@ -657,7 +657,7 @@ import {
657
657
  worktreeDisplayLabelForGroup,
658
658
  worktreeNameFromPath,
659
659
  worktreeSlugForTicket
660
- } from "./chunk-USBZ7PH4.js";
660
+ } from "./chunk-IPPY3FG5.js";
661
661
  import {
662
662
  appendMessage,
663
663
  createEmptyThread,
@@ -910,7 +910,7 @@ import {
910
910
  withTurnScopedCost,
911
911
  writeSecureJson,
912
912
  writeViewerContext
913
- } from "./chunk-PAH7KDL2.js";
913
+ } from "./chunk-YNTXWFXT.js";
914
914
  import {
915
915
  THINKING_EFFORTS,
916
916
  isThinkingEffort,
@@ -1384,12 +1384,12 @@ async function persistLinearTokens(data, viewer) {
1384
1384
  organizationName: viewer?.organizationName
1385
1385
  });
1386
1386
  }
1387
- async function fetchLinearViewer(accessToken) {
1387
+ async function fetchLinearViewer(accessToken2) {
1388
1388
  const res = await httpFetch(LINEAR_GRAPHQL, {
1389
1389
  method: "POST",
1390
1390
  headers: {
1391
1391
  "Content-Type": "application/json",
1392
- Authorization: linearAuthorizationHeader(accessToken)
1392
+ Authorization: linearAuthorizationHeader(accessToken2)
1393
1393
  },
1394
1394
  body: JSON.stringify({
1395
1395
  query: "{ viewer { name organization { name } } }"
@@ -5139,7 +5139,7 @@ async function handleJobStop(params, _ctx) {
5139
5139
  return stopDetachedJob(jobCwd(rec), id, reason ? { reason } : void 0);
5140
5140
  }
5141
5141
  async function defaultTurnWait(ref, timeoutMs) {
5142
- const { getOrchestrator: getOrchestrator2 } = await import("./orchestrator-QRSEOHXI.js");
5142
+ const { getOrchestrator: getOrchestrator2 } = await import("./orchestrator-QMKVXVUR.js");
5143
5143
  const orch = getOrchestrator2();
5144
5144
  const thread = await orch.waitForTurn(ref, timeoutMs, {
5145
5145
  resolveIfStillRunning: true
@@ -5633,7 +5633,7 @@ async function startMcpServer() {
5633
5633
  );
5634
5634
  }
5635
5635
  try {
5636
- const { maxConcurrentAgents: maxConcurrentAgents2 } = await import("./app-settings-L5HGYXWJ.js");
5636
+ const { maxConcurrentAgents: maxConcurrentAgents2 } = await import("./app-settings-NI4X5HW3.js");
5637
5637
  orch.setMaxConcurrent(maxConcurrentAgents2());
5638
5638
  } catch {
5639
5639
  }
@@ -6121,7 +6121,7 @@ async function startMcpServer() {
6121
6121
  if (!worktreeProfile) {
6122
6122
  registerConnectedIssueVendorTools(server);
6123
6123
  const { getCaffeinateHold: getCaffeinateHold2, setCaffeinateHold: setCaffeinateHold2 } = await import("./caffeinate-hold-DYJGWK6Y.js");
6124
- const { resolveNewThreadOptions: resolveNewThreadOptions2, resolveThreadDefaults: resolveThreadDefaults2 } = await import("./app-settings-L5HGYXWJ.js");
6124
+ const { resolveNewThreadOptions: resolveNewThreadOptions2, resolveThreadDefaults: resolveThreadDefaults2 } = await import("./app-settings-NI4X5HW3.js");
6125
6125
  const accountDefaults = resolveThreadDefaults2();
6126
6126
  const accountDefaultsHint = `Account defaults: agent=${accountDefaults.agent}, model=${accountDefaults.model?.trim() || "Auto"}, effort=${accountDefaults.effort}`;
6127
6127
  server.tool(
@@ -8992,7 +8992,14 @@ function parseRemoteClientMessage(raw) {
8992
8992
  const hostSecret = str2(parsed.hostSecret);
8993
8993
  const deviceLabel = str2(parsed.deviceLabel);
8994
8994
  if (!deviceId || !hostSecret || !deviceLabel) return null;
8995
- return { type: "host_register", deviceId, hostSecret, deviceLabel };
8995
+ const accountToken = str2(parsed.accountToken);
8996
+ return {
8997
+ type: "host_register",
8998
+ deviceId,
8999
+ hostSecret,
9000
+ deviceLabel,
9001
+ ...accountToken ? { accountToken } : {}
9002
+ };
8996
9003
  }
8997
9004
  if (msg.type === "pair") {
8998
9005
  const code = str2(parsed.code).toUpperCase();
@@ -9121,6 +9128,10 @@ function newCode() {
9121
9128
  return code;
9122
9129
  }
9123
9130
  var RemoteHub = class {
9131
+ constructor(opts = {}) {
9132
+ this.opts = opts;
9133
+ }
9134
+ opts;
9124
9135
  hosts = /* @__PURE__ */ new Map();
9125
9136
  remembered = /* @__PURE__ */ new Map();
9126
9137
  bySocket = /* @__PURE__ */ new Map();
@@ -9159,7 +9170,7 @@ var RemoteHub = class {
9159
9170
  }
9160
9171
  dispatch(socket, msg) {
9161
9172
  if (msg.type === "host_register") {
9162
- this.registerHost(socket, msg.deviceId, msg.deviceLabel, msg.hostSecret);
9173
+ this.registerHost(socket, msg.deviceId, msg.deviceLabel, msg.hostSecret, msg.accountToken ?? "");
9163
9174
  return;
9164
9175
  }
9165
9176
  if (msg.type === "host_pair") {
@@ -9186,7 +9197,14 @@ var RemoteHub = class {
9186
9197
  this.fromHost(socket, msg);
9187
9198
  }
9188
9199
  }
9189
- registerHost(socket, deviceId, deviceLabel, hostSecret) {
9200
+ registerHost(socket, deviceId, deviceLabel, hostSecret, accountToken) {
9201
+ if (this.opts.authorizeHost) {
9202
+ const decision = this.opts.authorizeHost({ accountToken, deviceId, deviceLabel, hostSecret });
9203
+ if (!decision.ok) {
9204
+ send2(socket, { type: "error", message: decision.message });
9205
+ return;
9206
+ }
9207
+ }
9190
9208
  const prior = this.remembered.get(deviceId);
9191
9209
  const existing = this.hosts.get(deviceId);
9192
9210
  if (prior && prior.hostSecret !== hostSecret || existing && existing.hostSecret !== hostSecret) {
@@ -9218,6 +9236,7 @@ var RemoteHub = class {
9218
9236
  this.bySocket.set(socket, { role: "host", deviceId });
9219
9237
  send2(socket, { type: "registered", deviceId, deviceLabel });
9220
9238
  if (host.phone) send2(socket, { type: "phone_joined" });
9239
+ this.opts.onAuthorized?.(socket);
9221
9240
  }
9222
9241
  issueCode(socket) {
9223
9242
  const bound = this.bySocket.get(socket);
@@ -9279,6 +9298,7 @@ var RemoteHub = class {
9279
9298
  online: Boolean(host)
9280
9299
  };
9281
9300
  });
9301
+ if (hosts.some((host) => host.deviceId)) this.opts.onAuthorized?.(socket);
9282
9302
  send2(socket, { type: "hosts", hosts });
9283
9303
  }
9284
9304
  releasePhoneSocket(socket) {
@@ -9318,6 +9338,7 @@ var RemoteHub = class {
9318
9338
  deviceLabel: host.deviceLabel
9319
9339
  });
9320
9340
  this.bySocket.set(socket, { role: "phone", deviceId: host.deviceId });
9341
+ this.opts.onAuthorized?.(socket);
9321
9342
  send2(socket, {
9322
9343
  type: "paired",
9323
9344
  deviceId: host.deviceId,
@@ -9354,8 +9375,776 @@ var RemoteHub = class {
9354
9375
  // src/remote/client.ts
9355
9376
  import { WebSocket as WsWebSocket3 } from "ws";
9356
9377
 
9357
- // src/remote/listen.ts
9378
+ // src/remote/phone-media.ts
9379
+ var PHONE_IMAGE_MAX_BYTES = 15e5;
9380
+ var IMAGE_MIME = {
9381
+ png: "image/png",
9382
+ jpg: "image/jpeg",
9383
+ jpeg: "image/jpeg",
9384
+ gif: "image/gif",
9385
+ webp: "image/webp",
9386
+ bmp: "image/bmp"
9387
+ };
9388
+ function phoneWorktreeRelative(worktreePath, raw) {
9389
+ let text6 = raw.trim();
9390
+ if (!text6) return null;
9391
+ if (/^file:\/\//i.test(text6)) {
9392
+ try {
9393
+ text6 = decodeURIComponent(new URL(text6).pathname);
9394
+ } catch {
9395
+ return null;
9396
+ }
9397
+ }
9398
+ text6 = (text6.split("#")[0] ?? text6).replace(/\\/g, "/");
9399
+ if (!text6 || text6.includes("\0") || text6.split("/").includes("..")) return null;
9400
+ const root = worktreePath.replace(/\/+$/, "");
9401
+ if (!root || root === "/") return null;
9402
+ if (text6.startsWith("/")) {
9403
+ if (text6 !== root && !text6.startsWith(`${root}/`)) return null;
9404
+ text6 = text6 === root ? "" : text6.slice(root.length + 1);
9405
+ }
9406
+ text6 = text6.replace(/^\.\//, "").replace(/\/+$/, "");
9407
+ if (!text6 || text6 === "." || text6.startsWith("/")) return null;
9408
+ return text6;
9409
+ }
9410
+ function phoneImageMime(relativePath) {
9411
+ const ext = relativePath.split(".").pop()?.toLowerCase() ?? "";
9412
+ return IMAGE_MIME[ext] ?? null;
9413
+ }
9414
+ function phoneImageDataUrl(worktreePath, raw) {
9415
+ const relative = phoneWorktreeRelative(worktreePath, raw);
9416
+ if (!relative) return null;
9417
+ const mime = phoneImageMime(relative);
9418
+ if (!mime) return null;
9419
+ try {
9420
+ const file = readWorktreeFileForUpload(worktreePath, relative, { maxBytes: PHONE_IMAGE_MAX_BYTES });
9421
+ return `data:${mime};base64,${file.contentBase64}`;
9422
+ } catch {
9423
+ return null;
9424
+ }
9425
+ }
9426
+
9427
+ // src/remote/phone-chats.ts
9428
+ import { randomUUID as randomUUID7 } from "crypto";
9429
+ var PHONE_CONTROL_PREFIX = "\0sb.phone\n";
9430
+ var PHONE_USER_PREFIX = "Phone\n\n";
9431
+ var TRANSCRIPT_LIMIT = 40;
9432
+ var MESSAGE_LIMIT = 4e3;
9433
+ var PREVIEW_LIMIT = 90;
9434
+ var HISTORY_PAGE_SIZE = 40;
9435
+ var AGENT_KINDS = /* @__PURE__ */ new Set(["claude", "codex", "opencode", "brightsy", "cursor"]);
9436
+ var MAX_PHONE_FILES = 8;
9437
+ var MAX_PHONE_FILE_CHARS = Math.ceil(8 * 1024 * 1024 * 4 / 3);
9438
+ var MAX_PHONE_FILES_CHARS = Math.ceil(20 * 1024 * 1024 * 4 / 3);
9439
+ var MAX_DICTATION_CHARS = Math.ceil(12 * 1024 * 1024 * 4 / 3);
9440
+ function encodePhoneControl(payload) {
9441
+ return PHONE_CONTROL_PREFIX + JSON.stringify(payload);
9442
+ }
9443
+ function takePhoneControl(text6) {
9444
+ if (!text6.startsWith(PHONE_CONTROL_PREFIX)) return null;
9445
+ let parsed;
9446
+ try {
9447
+ parsed = JSON.parse(text6.slice(PHONE_CONTROL_PREFIX.length));
9448
+ } catch {
9449
+ return "invalid";
9450
+ }
9451
+ return parsePhoneControl(parsed);
9452
+ }
9453
+ function parsePhoneControl(value) {
9454
+ if (!value || typeof value !== "object") return "invalid";
9455
+ const op = value.op;
9456
+ if (op === "list") return { op: "list" };
9457
+ if (op === "history") {
9458
+ const query = textField(value, "query");
9459
+ const after = textField(value, "after");
9460
+ return { op: "history", ...query ? { query } : {}, ...after ? { after } : {} };
9461
+ }
9462
+ if (op === "sources") {
9463
+ const repoPath = textField(value, "repoPath");
9464
+ if (!repoPath || !isProjectPath(repoPath)) return "invalid";
9465
+ const query = textField(value, "query");
9466
+ return { op: "sources", repoPath, ...query ? { query } : {} };
9467
+ }
9468
+ if (op === "models") {
9469
+ const agent = agentField(value);
9470
+ if (!agent) return "invalid";
9471
+ return { op: "models", agent };
9472
+ }
9473
+ if (op === "dictate") {
9474
+ const id = textField(value, "id");
9475
+ const audioBase64 = base64Field(value, "audioBase64");
9476
+ if (!id || !audioBase64 || audioBase64.length > MAX_DICTATION_CHARS) return "invalid";
9477
+ return { op: "dictate", id, audioBase64 };
9478
+ }
9479
+ if (op === "create") return parsePhoneCreate(value);
9480
+ const chatId = typeof value.chatId === "string" ? value.chatId.trim() : "";
9481
+ if (!chatId) return "invalid";
9482
+ if (op === "open" || op === "archive" || op === "restore" || op === "stop") return { op, chatId };
9483
+ if (op === "open-file" || op === "media") {
9484
+ const path2 = textField(value, "path");
9485
+ if (!path2 || path2.length > 2e3) return "invalid";
9486
+ if (op === "media") return { op, chatId, path: path2 };
9487
+ const startLine = lineField(value, "startLine");
9488
+ const endLine = lineField(value, "endLine");
9489
+ return {
9490
+ op,
9491
+ chatId,
9492
+ path: path2,
9493
+ ...startLine != null ? { startLine } : {},
9494
+ ...endLine != null ? { endLine } : {}
9495
+ };
9496
+ }
9497
+ const draft = parsePhoneDraft(value);
9498
+ if (draft === "invalid") return "invalid";
9499
+ if (op === "options") return { op: "options", chatId, ...draft };
9500
+ if (op === "prompt") {
9501
+ const text6 = typeof value.text === "string" ? value.text.trim() : "";
9502
+ const hasPayload = Boolean(text6 || draft.files?.length || draft.links?.length);
9503
+ if (!hasPayload) return "invalid";
9504
+ return { op: "prompt", chatId, text: text6, ...draft };
9505
+ }
9506
+ return "invalid";
9507
+ }
9508
+ function textField(value, key) {
9509
+ const raw = value[key];
9510
+ return typeof raw === "string" ? raw.trim() : "";
9511
+ }
9512
+ function lineField(value, key) {
9513
+ const raw = value[key];
9514
+ if (typeof raw !== "number" || !Number.isInteger(raw) || raw < 1 || raw > 1e6) return void 0;
9515
+ return raw;
9516
+ }
9517
+ function agentField(value) {
9518
+ const agent = textField(value, "agent");
9519
+ if (!agent) return void 0;
9520
+ return AGENT_KINDS.has(agent) ? agent : void 0;
9521
+ }
9522
+ function base64Field(value, key) {
9523
+ const raw = value[key];
9524
+ if (typeof raw !== "string") return "";
9525
+ return raw.replace(/\s/g, "");
9526
+ }
9527
+ function parsePhoneDraft(value) {
9528
+ const draft = {};
9529
+ if (textField(value, "agent")) {
9530
+ const agent = agentField(value);
9531
+ if (!agent) return "invalid";
9532
+ draft.agent = agent;
9533
+ }
9534
+ if ("model" in value) {
9535
+ const model = value.model;
9536
+ if (model === null || model === "") draft.model = null;
9537
+ else if (typeof model === "string") draft.model = model.trim().slice(0, 200);
9538
+ else return "invalid";
9539
+ }
9540
+ if ("effort" in value && value.effort != null) {
9541
+ const effort = value.effort;
9542
+ if (!isThinkingEffort(effort)) return "invalid";
9543
+ draft.effort = effort;
9544
+ }
9545
+ if ("fast" in value) {
9546
+ const fast = value.fast;
9547
+ if (typeof fast !== "boolean") return "invalid";
9548
+ draft.fast = fast;
9549
+ }
9550
+ if ("planMode" in value) {
9551
+ const planMode = value.planMode;
9552
+ if (typeof planMode !== "boolean") return "invalid";
9553
+ draft.planMode = planMode;
9554
+ }
9555
+ if ("autonomy" in value && value.autonomy != null) {
9556
+ const autonomy = value.autonomy;
9557
+ if (autonomy !== "default" && autonomy !== "full") return "invalid";
9558
+ draft.autonomy = autonomy;
9559
+ }
9560
+ const files = parsePhoneFiles(value.files);
9561
+ if (files === "invalid") return "invalid";
9562
+ if (files.length) draft.files = files;
9563
+ const links = parsePhoneLinks(value.links);
9564
+ if (links === "invalid") return "invalid";
9565
+ if (links.length) draft.links = links;
9566
+ const prompt = textField(value, "prompt");
9567
+ if (prompt) draft.prompt = prompt.slice(0, 2e4);
9568
+ return draft;
9569
+ }
9570
+ function parsePhoneFiles(value) {
9571
+ if (value == null) return [];
9572
+ if (!Array.isArray(value) || value.length > MAX_PHONE_FILES) return "invalid";
9573
+ const files = [];
9574
+ let total = 0;
9575
+ for (const item of value) {
9576
+ if (!item || typeof item !== "object") return "invalid";
9577
+ const name = (textField(item, "name").replace(/[/\\]/g, "_") || "file").slice(0, 180);
9578
+ const dataBase64 = base64Field(item, "dataBase64");
9579
+ if (!dataBase64 || !/^[A-Za-z0-9+/=]+$/.test(dataBase64)) return "invalid";
9580
+ if (dataBase64.length > MAX_PHONE_FILE_CHARS) return "invalid";
9581
+ total += dataBase64.length;
9582
+ if (total > MAX_PHONE_FILES_CHARS) return "invalid";
9583
+ files.push({ name, dataBase64 });
9584
+ }
9585
+ return files;
9586
+ }
9587
+ function parsePhoneLinks(value) {
9588
+ if (value == null) return [];
9589
+ if (!Array.isArray(value) || value.length > 8) return "invalid";
9590
+ const links = [];
9591
+ for (const item of value) {
9592
+ if (!item || typeof item !== "object") return "invalid";
9593
+ const ref = textField(item, "ref").slice(0, 80);
9594
+ if (!ref) return "invalid";
9595
+ const title = (textField(item, "title") || ref).slice(0, 200);
9596
+ const url = textField(item, "url").slice(0, 500);
9597
+ links.push({ ref, title, ...url ? { url } : {} });
9598
+ }
9599
+ return links;
9600
+ }
9601
+ function parsePhoneCreate(value) {
9602
+ const draft = parsePhoneDraft(value);
9603
+ if (draft === "invalid") return "invalid";
9604
+ const goal = textField(value, "goal") || draft.prompt || "";
9605
+ const where = value.where;
9606
+ if (where == null || where === "orchestration") {
9607
+ return {
9608
+ op: "create",
9609
+ where: "orchestration",
9610
+ ...draft,
9611
+ ...goal ? { goal, prompt: goal } : {}
9612
+ };
9613
+ }
9614
+ if (where === "worktree") {
9615
+ const chatId = textField(value, "chatId");
9616
+ if (!chatId) return "invalid";
9617
+ return { op: "create", where: "worktree", chatId, ...draft };
9618
+ }
9619
+ if (where === "project") {
9620
+ const repoPath = textField(value, "repoPath");
9621
+ if (!repoPath || !isProjectPath(repoPath)) return "invalid";
9622
+ const sourceRaw = textField(value, "sourceType");
9623
+ let sourceType;
9624
+ if (sourceRaw === "branch" || sourceRaw === "pr" || sourceRaw === "ticket") sourceType = sourceRaw;
9625
+ else if (sourceRaw) return "invalid";
9626
+ const sourceRef = textField(value, "sourceRef");
9627
+ if ((sourceType === "pr" || sourceType === "ticket") && !sourceRef) return "invalid";
9628
+ const title = textField(value, "title").slice(0, 200);
9629
+ const cowboy = value.cowboy === true;
9630
+ return {
9631
+ op: "create",
9632
+ where: "project",
9633
+ repoPath,
9634
+ ...draft,
9635
+ ...sourceType ? { sourceType } : {},
9636
+ ...sourceRef ? { sourceRef } : {},
9637
+ ...title ? { title } : {},
9638
+ ...cowboy ? { cowboy: true } : {}
9639
+ };
9640
+ }
9641
+ return "invalid";
9642
+ }
9643
+ function phoneVisibleText(role, text6) {
9644
+ const trimmed = text6.trim();
9645
+ if (role === "user" && trimmed.startsWith(PHONE_USER_PREFIX)) {
9646
+ return trimmed.slice(PHONE_USER_PREFIX.length).trim();
9647
+ }
9648
+ return trimmed;
9649
+ }
9650
+ function clip(text6, limit) {
9651
+ if (text6.length <= limit) return text6;
9652
+ return `${text6.slice(0, limit - 1)}\u2026`;
9653
+ }
9654
+ function summarize(thread) {
9655
+ return {
9656
+ id: thread.id,
9657
+ title: thread.title?.trim() || "Untitled",
9658
+ status: thread.status,
9659
+ preview: previewOf(thread),
9660
+ updatedAt: thread.updatedAt
9661
+ };
9662
+ }
9663
+ function previewOf(thread) {
9664
+ const messages = listPhoneMessages(thread);
9665
+ const last = messages[messages.length - 1];
9666
+ if (!last) return "";
9667
+ return clip(last.text.replace(/\s+/g, " ").trim(), PREVIEW_LIMIT);
9668
+ }
9669
+ function listPhoneMessages(thread) {
9670
+ const out = [];
9671
+ for (const message of thread.messages) {
9672
+ if (message.role !== "user" && message.role !== "agent") continue;
9673
+ const names = (message.attachments ?? []).map((item) => item.name.trim()).filter(Boolean);
9674
+ const text6 = clip(
9675
+ [phoneVisibleText(message.role, message.text), names.length ? names.join(", ") : ""].filter(Boolean).join("\n"),
9676
+ MESSAGE_LIMIT
9677
+ );
9678
+ if (!text6) continue;
9679
+ out.push({ role: message.role, text: text6 });
9680
+ }
9681
+ for (const prompt of thread.queue) {
9682
+ const text6 = clip(phoneVisibleText("user", prompt), MESSAGE_LIMIT);
9683
+ if (text6) out.push({ role: "user", text: text6 });
9684
+ }
9685
+ return out.slice(-TRANSCRIPT_LIMIT);
9686
+ }
9687
+ function repoName(repoPath) {
9688
+ const parts = repoPath.replace(/\/$/, "").split("/");
9689
+ return parts[parts.length - 1] || repoPath;
9690
+ }
9691
+ function isProjectPath(path2) {
9692
+ if (!path2 || path2 === "/" || path2 === ".") return false;
9693
+ return !isGlobalThread({ repoPath: path2 });
9694
+ }
9695
+ function listPhoneChats() {
9696
+ return listGlobalThreads().slice().sort((a, b) => a.updatedAt < b.updatedAt ? 1 : a.updatedAt > b.updatedAt ? -1 : 0).map(summarize);
9697
+ }
9698
+ function listPhoneSidebar() {
9699
+ const threads = listThreads();
9700
+ const byRepo = /* @__PURE__ */ new Map();
9701
+ for (const thread of threads) {
9702
+ if (!isProjectPath(thread.repoPath)) continue;
9703
+ const list = byRepo.get(thread.repoPath) ?? [];
9704
+ list.push(thread);
9705
+ byRepo.set(thread.repoPath, list);
9706
+ }
9707
+ for (const workspace of listWorkspaces()) {
9708
+ if (!isProjectPath(workspace.path) || byRepo.has(workspace.path)) continue;
9709
+ byRepo.set(workspace.path, []);
9710
+ }
9711
+ const projects = [...byRepo.entries()].sort(([a], [b]) => repoName(a).localeCompare(repoName(b))).map(([path2, repoThreads]) => ({
9712
+ name: repoName(path2),
9713
+ path: path2,
9714
+ worktrees: groupHomeBoardWorktrees(repoThreads).map((group) => ({
9715
+ label: worktreeDisplayLabelForGroup(group),
9716
+ chats: group.map(summarize)
9717
+ }))
9718
+ }));
9719
+ return { orchestration: listPhoneChats(), projects };
9720
+ }
9721
+ function historyStamp(thread) {
9722
+ return thread.archivedAt || thread.updatedAt;
9723
+ }
9724
+ function historyCursor(chat) {
9725
+ return `${chat.updatedAt} ${chat.id}`;
9726
+ }
9727
+ function historyQueryMatch(chat, query) {
9728
+ if (!query) return true;
9729
+ return `${chat.title} ${chat.where} ${chat.agent} ${chat.preview}`.toLowerCase().includes(query);
9730
+ }
9731
+ function listPhoneHistory(opts) {
9732
+ const query = opts?.query?.trim().toLowerCase() ?? "";
9733
+ const after = opts?.after?.trim() ?? "";
9734
+ 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) => ({
9735
+ id: thread.id,
9736
+ title: thread.title?.trim() || "Untitled",
9737
+ preview: previewOf(thread),
9738
+ updatedAt: historyStamp(thread),
9739
+ where: isGlobalThread(thread) ? "Orchestration" : repoName(thread.repoPath),
9740
+ agent: thread.agent
9741
+ })).filter((chat) => historyQueryMatch(chat, query));
9742
+ let start = 0;
9743
+ if (after) {
9744
+ const index = all.findIndex((chat) => historyCursor(chat) === after);
9745
+ start = index >= 0 ? index + 1 : all.length;
9746
+ }
9747
+ const chats = all.slice(start, start + HISTORY_PAGE_SIZE);
9748
+ const last = chats.at(-1);
9749
+ const more = start + chats.length < all.length;
9750
+ return {
9751
+ chats,
9752
+ total: all.length,
9753
+ ...more && last ? { next: historyCursor(last) } : {}
9754
+ };
9755
+ }
9756
+ function matchPhoneThread(chatId) {
9757
+ const id = chatId.trim();
9758
+ if (!id) return null;
9759
+ const exact = readThread(id);
9760
+ if (exact) return exact;
9761
+ const match = findThreadByRef(id);
9762
+ if (!match) return null;
9763
+ if (match.id !== id && !match.id.startsWith(id)) return null;
9764
+ return match;
9765
+ }
9766
+ function visiblePhoneThread(thread) {
9767
+ if (isGlobalThread(thread)) return true;
9768
+ return isProjectPath(thread.repoPath);
9769
+ }
9770
+ function findPhoneChat(chatId) {
9771
+ const thread = matchPhoneThread(chatId);
9772
+ if (!thread || !visiblePhoneThread(thread)) return null;
9773
+ return thread;
9774
+ }
9775
+ function readPhoneThread(chatId) {
9776
+ const thread = findPhoneChat(chatId);
9777
+ if (!thread || thread.status === "archived") return null;
9778
+ return thread;
9779
+ }
9780
+ function phoneComposerOptions(thread) {
9781
+ return {
9782
+ agent: thread.agent,
9783
+ model: thread.model ?? null,
9784
+ effort: thread.effort,
9785
+ fast: Boolean(thread.fast),
9786
+ planMode: Boolean(thread.planMode),
9787
+ autonomy: thread.autonomy ?? "default"
9788
+ };
9789
+ }
9790
+ function phoneTurnAttachments(draft) {
9791
+ const links = (draft.links ?? []).map((link) => ({
9792
+ id: randomUUID7(),
9793
+ name: link.ref,
9794
+ kind: "issue",
9795
+ content: [`Linked issue: ${link.ref} \u2014 ${link.title}`, link.url ? `URL: ${link.url}` : null].filter(Boolean).join("\n")
9796
+ }));
9797
+ const files = (draft.files ?? []).map((file) => ({
9798
+ id: randomUUID7(),
9799
+ name: file.name,
9800
+ kind: "file",
9801
+ previewDataUrl: `data:application/octet-stream;base64,${file.dataBase64}`,
9802
+ content: `File attached: ${file.name}`
9803
+ }));
9804
+ return [...links, ...files];
9805
+ }
9806
+ function openedFrom(thread, place) {
9807
+ const fresh = readThread(thread.id) ?? thread;
9808
+ return {
9809
+ chat: summarize(fresh),
9810
+ messages: listPhoneMessages(fresh),
9811
+ place,
9812
+ options: phoneComposerOptions(fresh)
9813
+ };
9814
+ }
9815
+ function openPhoneChat(chatId) {
9816
+ const thread = readPhoneThread(chatId);
9817
+ if (!thread) return null;
9818
+ const place = isGlobalThread(thread) ? { kind: "orchestration" } : projectPlace(thread);
9819
+ return openedFrom(thread, place);
9820
+ }
9821
+ function sameRepoPath(a, b) {
9822
+ return a.replace(/\/+$/, "") === b.replace(/\/+$/, "");
9823
+ }
9824
+ function projectPlace(thread) {
9825
+ const group = threadsSharingWorktree(thread.worktreePath);
9826
+ return {
9827
+ kind: "project",
9828
+ repoPath: thread.repoPath,
9829
+ worktree: worktreeDisplayLabelForGroup(group.length > 0 ? group : [thread])
9830
+ };
9831
+ }
9832
+ function assertKnownProject(repoPath) {
9833
+ const path2 = repoPath.trim();
9834
+ if (!isProjectPath(path2)) throw new Error("Pick a project on this Mac.");
9835
+ const known = listWorkspaces().some((workspace) => sameRepoPath(workspace.path, path2)) || listThreads().some(
9836
+ (thread) => thread.status !== "archived" && sameRepoPath(thread.repoPath, path2)
9837
+ );
9838
+ if (!known) throw new Error("That project is not on this Mac.");
9839
+ return path2;
9840
+ }
9841
+ function draftAgentOptions(draft, defaults) {
9842
+ return {
9843
+ agent: draft?.agent ?? defaults.agent,
9844
+ model: draft && "model" in draft ? draft.model ?? null : defaults.model,
9845
+ effort: draft?.effort ?? defaults.effort,
9846
+ fast: draft?.fast ?? defaults.fast,
9847
+ planMode: Boolean(draft?.planMode),
9848
+ autonomy: draft?.autonomy
9849
+ };
9850
+ }
9851
+ function createPhoneChat(agent) {
9852
+ const defaults = resolveOrchestratorDefaults();
9853
+ const thread = createGlobalChat({
9854
+ agent: agent ?? defaults.agent,
9855
+ model: defaults.model,
9856
+ effort: defaults.effort,
9857
+ fast: defaults.fast
9858
+ });
9859
+ return openedFrom(thread, { kind: "orchestration" });
9860
+ }
9861
+ async function createPhoneOrchestration(draft) {
9862
+ const goal = (draft?.goal ?? draft?.prompt ?? "").trim();
9863
+ const attachments = phoneTurnAttachments(draft ?? {});
9864
+ if (!goal && attachments.length === 0) return createPhoneChat(draft?.agent);
9865
+ const defaults = resolveOrchestratorDefaults();
9866
+ const options = draftAgentOptions(draft, defaults);
9867
+ const thread = await startOrchestration({
9868
+ goal: goal || "See the attached files.",
9869
+ agent: options.agent,
9870
+ model: options.model,
9871
+ effort: options.effort,
9872
+ fast: options.fast,
9873
+ planMode: options.planMode,
9874
+ autonomy: options.autonomy,
9875
+ attachments
9876
+ });
9877
+ return openedFrom(thread, { kind: "orchestration" });
9878
+ }
9879
+ function createPhoneWorktreeAgent(fromChatId, draft) {
9880
+ const source = readPhoneThread(fromChatId);
9881
+ if (!source || isGlobalThread(source)) throw new Error("That worktree is not on this Mac.");
9882
+ const defaults = resolveNewThreadOptions();
9883
+ const options = draftAgentOptions(draft, defaults);
9884
+ const attachments = persistPendingFileAttachments(
9885
+ source.worktreePath,
9886
+ phoneTurnAttachments(draft ?? {})
9887
+ );
9888
+ const thread = createChatTab({
9889
+ fromThreadId: source.id,
9890
+ agent: options.agent,
9891
+ model: options.model,
9892
+ effort: options.effort,
9893
+ fast: options.fast,
9894
+ autonomy: options.autonomy,
9895
+ attachments
9896
+ });
9897
+ if (draft?.planMode !== void 0) updateThread(thread.id, { planMode: draft.planMode });
9898
+ return openedFrom(thread, projectPlace(readThread(thread.id) ?? thread));
9899
+ }
9900
+ async function createPhoneProjectWorktree(repoPathOrInput) {
9901
+ const input = typeof repoPathOrInput === "string" ? { repoPath: repoPathOrInput } : repoPathOrInput;
9902
+ const path2 = assertKnownProject(input.repoPath);
9903
+ const defaults = resolveNewThreadOptions();
9904
+ const options = draftAgentOptions(input, defaults);
9905
+ const thread = await createThread({
9906
+ sourceType: input.sourceType ?? "branch",
9907
+ sourceRef: input.sourceRef || "default",
9908
+ repoPath: path2,
9909
+ title: input.title,
9910
+ cowboy: input.cowboy,
9911
+ agent: options.agent,
9912
+ model: options.model,
9913
+ effort: options.effort,
9914
+ fast: options.fast,
9915
+ planMode: options.planMode,
9916
+ autonomy: options.autonomy,
9917
+ attachments: phoneTurnAttachments(input),
9918
+ prompt: input.prompt,
9919
+ reuseExisting: false
9920
+ });
9921
+ return openedFrom(thread, projectPlace(thread));
9922
+ }
9923
+ async function listPhoneModels(agent) {
9924
+ const catalogs = await listModelsForAgent(agent);
9925
+ return (catalogs[0]?.models ?? []).slice(0, 40).map((model) => ({
9926
+ id: model.id,
9927
+ label: model.displayName || model.id
9928
+ }));
9929
+ }
9930
+ function archivePhoneChat(chatId) {
9931
+ const thread = readPhoneThread(chatId);
9932
+ if (!thread) return false;
9933
+ updateThread(thread.id, { status: "archived" });
9934
+ return true;
9935
+ }
9936
+
9937
+ // src/remote/phone-sources.ts
9938
+ function sourceWarning(label, err) {
9939
+ const message = err instanceof Error ? err.message : String(err);
9940
+ return message ? `${label}: ${message}` : `${label} failed.`;
9941
+ }
9942
+ async function listPhoneSources(repoPath, query) {
9943
+ const path2 = assertKnownProject(repoPath);
9944
+ const q = query?.trim() ?? "";
9945
+ const warnings = [];
9946
+ const [prs, branches, issues] = await Promise.all([
9947
+ listPrs(path2, { state: "open", limit: 40, ...q ? { query: q } : {} }).catch((err) => {
9948
+ warnings.push(sourceWarning("Pull requests", err));
9949
+ return [];
9950
+ }),
9951
+ listBranches(path2).catch((err) => {
9952
+ warnings.push(sourceWarning("Branches", err));
9953
+ return [];
9954
+ }),
9955
+ listIssues(path2, { limit: 40, ...q ? { query: q } : {} }).catch((err) => {
9956
+ warnings.push(sourceWarning("Issues", err));
9957
+ return { issues: [] };
9958
+ })
9959
+ ]);
9960
+ const needle = q.toLowerCase();
9961
+ const branchRows = branches.filter((branch) => !needle || branch.name.toLowerCase().includes(needle)).slice(0, 50).map((branch) => ({
9962
+ ref: branch.name,
9963
+ title: branch.current ? `${branch.name} (current)` : branch.name
9964
+ }));
9965
+ return {
9966
+ prs: prs.slice(0, 40).map((pr) => ({
9967
+ ref: String(pr.number),
9968
+ title: clip(pr.title || `PR #${pr.number}`, 140),
9969
+ ...pr.url ? { url: pr.url } : {}
9970
+ })),
9971
+ branches: branchRows,
9972
+ issues: (issues.issues ?? []).slice(0, 40).map((issue) => ({
9973
+ ref: issue.identifier,
9974
+ title: clip(issue.title || issue.identifier, 140),
9975
+ ...issue.url ? { url: issue.url } : {}
9976
+ })),
9977
+ ...warnings.length ? { warnings } : {}
9978
+ };
9979
+ }
9980
+
9981
+ // src/remote/phone-stream.ts
9982
+ var PHONE_STREAM_FLUSH_MS = 80;
9983
+ function phoneStreamFrame(parts) {
9984
+ return {
9985
+ text: visibleAssistantText(void 0, parts),
9986
+ activity: liveActivitySummary(parts)
9987
+ };
9988
+ }
9989
+ var SKIPPED = /* @__PURE__ */ new Set(["session_id", "usage", "stderr", "exit"]);
9990
+ function phoneStreamParts(parts, event) {
9991
+ if (SKIPPED.has(event.type)) return null;
9992
+ return applyAgentEvent(parts, event);
9993
+ }
9994
+ function phoneStreamFlushNow(event) {
9995
+ return event.type === "tool_use" || event.type === "tool_result" && !event.partial;
9996
+ }
9997
+
9998
+ // src/remote/phone-live.ts
9358
9999
  var REMOTE_STOPPED_REPLY = "Sideboard stopped the in-progress turn.";
10000
+ var chatChains = /* @__PURE__ */ new Map();
10001
+ var chatGeneration = /* @__PURE__ */ new Map();
10002
+ var phoneTurnGen = /* @__PURE__ */ new Map();
10003
+ function bumpChat(chatId) {
10004
+ const next = (chatGeneration.get(chatId) ?? 0) + 1;
10005
+ chatGeneration.set(chatId, next);
10006
+ return next;
10007
+ }
10008
+ function finishPhoneTurn(chatId, generation) {
10009
+ if (phoneTurnGen.get(chatId) === generation) phoneTurnGen.delete(chatId);
10010
+ }
10011
+ function enqueueChat(chatId, fn) {
10012
+ const prev = chatChains.get(chatId) ?? Promise.resolve();
10013
+ const run2 = prev.then(fn, fn);
10014
+ chatChains.set(
10015
+ chatId,
10016
+ run2.then(
10017
+ () => void 0,
10018
+ () => void 0
10019
+ )
10020
+ );
10021
+ }
10022
+ function emitControl(opts, reply) {
10023
+ opts.onOutbound({ type: "assistant", text: encodePhoneControl(reply) });
10024
+ }
10025
+ function watchPhoneStream(opts, chatId, threadId, generation) {
10026
+ let parts = [];
10027
+ let timer = null;
10028
+ let lastKey = "";
10029
+ let closed = false;
10030
+ const alive = () => !closed && chatGeneration.get(chatId) === generation;
10031
+ const flush = () => {
10032
+ timer = null;
10033
+ if (!alive()) return;
10034
+ const frame = phoneStreamFrame(parts);
10035
+ const key = `${frame.activity}
10036
+ ${frame.text}`;
10037
+ if (key === lastKey) return;
10038
+ if (!frame.text && !frame.activity) return;
10039
+ lastKey = key;
10040
+ emitControl(opts, { op: "stream", chatId, text: frame.text, activity: frame.activity });
10041
+ };
10042
+ const schedule = (event) => {
10043
+ if (phoneStreamFlushNow(event) || lastKey === "") {
10044
+ if (timer) {
10045
+ clearTimeout(timer);
10046
+ timer = null;
10047
+ }
10048
+ flush();
10049
+ return;
10050
+ }
10051
+ if (timer) return;
10052
+ timer = setTimeout(flush, PHONE_STREAM_FLUSH_MS);
10053
+ };
10054
+ const off = getOrchestrator().on((event) => {
10055
+ if (!alive()) return;
10056
+ if (event.type !== "turn_output" || event.threadId !== threadId) return;
10057
+ const next = phoneStreamParts(parts, event.event);
10058
+ if (!next) return;
10059
+ parts = next;
10060
+ schedule(event.event);
10061
+ });
10062
+ return () => {
10063
+ closed = true;
10064
+ if (timer) clearTimeout(timer);
10065
+ off();
10066
+ };
10067
+ }
10068
+ function interruptChat(chatId, log) {
10069
+ try {
10070
+ const thread = readPhoneThread(chatId);
10071
+ if (!thread) return false;
10072
+ if (thread.status !== "running" && thread.status !== "queued") return false;
10073
+ getOrchestrator().stop(thread.id, { clearQueue: true });
10074
+ log(`interrupt phone \u2192 chat ${thread.id.slice(0, 8)} (${thread.status})`);
10075
+ return true;
10076
+ } catch (err) {
10077
+ log(`interrupt chat: ${err instanceof Error ? err.message : String(err)}`);
10078
+ return false;
10079
+ }
10080
+ }
10081
+ function emitChatTurn(opts, chatId, threadId) {
10082
+ const thread = readThread(threadId);
10083
+ if (!thread) {
10084
+ emitControl(opts, { op: "error", chatId, message: "That agent is not on this Mac." });
10085
+ return;
10086
+ }
10087
+ if (thread.status === "stopped") {
10088
+ emitControl(opts, { op: "stopped", chatId });
10089
+ return;
10090
+ }
10091
+ const result = getOrchestrator().getTurnResult(thread.id);
10092
+ const pending = extractPendingPlanQuestions(
10093
+ [...thread.messages].reverse().find((m) => m.role === "agent")?.parts
10094
+ );
10095
+ if (result.taskState === "input-required" && pending) {
10096
+ emitControl(opts, {
10097
+ op: "ask",
10098
+ chatId,
10099
+ text: result.text.trim(),
10100
+ questions: pending.questions.map((q) => ({
10101
+ question: q.question,
10102
+ options: q.options.map(
10103
+ (o) => o.description ? { label: o.label, description: o.description } : { label: o.label }
10104
+ )
10105
+ }))
10106
+ });
10107
+ return;
10108
+ }
10109
+ const reply = outboundReplyFromTurn(result, {
10110
+ inputRequired: "Sideboard is waiting for an answer.",
10111
+ canceled: REMOTE_STOPPED_REPLY,
10112
+ completedEmpty: () => "",
10113
+ failed: (detail) => detail ? `Sideboard failed: ${detail}` : "Sideboard failed before producing a result."
10114
+ });
10115
+ emitControl(opts, { op: "assistant", chatId, text: reply.trim() });
10116
+ }
10117
+ function phoneError(err) {
10118
+ return err instanceof Error ? err.message : String(err);
10119
+ }
10120
+ function deliverPhoneTurn(opts, chatId) {
10121
+ const thread = readPhoneThread(chatId);
10122
+ if (!thread || thread.status !== "running" && thread.status !== "queued") return;
10123
+ const id = thread.id;
10124
+ const claimed = phoneTurnGen.get(id);
10125
+ if (claimed != null && claimed === chatGeneration.get(id)) return;
10126
+ const generation = bumpChat(id);
10127
+ phoneTurnGen.set(id, generation);
10128
+ enqueueChat(id, async () => {
10129
+ if (chatGeneration.get(id) !== generation) return;
10130
+ const stopStream = watchPhoneStream(opts, id, id, generation);
10131
+ try {
10132
+ await getOrchestrator().waitForTurn(id, 14 * 60 * 1e3);
10133
+ } catch (err) {
10134
+ stopStream();
10135
+ finishPhoneTurn(id, generation);
10136
+ if (chatGeneration.get(id) !== generation) return;
10137
+ emitControl(opts, { op: "error", chatId: id, message: `Sideboard failed: ${phoneError(err)}` });
10138
+ return;
10139
+ }
10140
+ stopStream();
10141
+ finishPhoneTurn(id, generation);
10142
+ if (chatGeneration.get(id) !== generation) return;
10143
+ emitChatTurn(opts, id, id);
10144
+ });
10145
+ }
10146
+
10147
+ // src/remote/listen.ts
9359
10148
  function formatRemotePrompt(text6) {
9360
10149
  return `Phone
9361
10150
 
@@ -9387,10 +10176,312 @@ function interruptRemoteCoordinator(deviceId, log = () => void 0) {
9387
10176
  return false;
9388
10177
  }
9389
10178
  }
10179
+ function publishOpenedChat(opts, chatId) {
10180
+ const opened = openPhoneChat(chatId);
10181
+ if (!opened) {
10182
+ emitControl(opts, { op: "error", message: "That agent is not on this Mac." });
10183
+ return;
10184
+ }
10185
+ emitControl(opts, { op: "opened", ...opened });
10186
+ if (opened.chat.status === "running" || opened.chat.status === "queued") {
10187
+ deliverPhoneTurn(opts, opened.chat.id);
10188
+ }
10189
+ }
10190
+ function applyPhoneOptions(chatId, draft) {
10191
+ const patch = {};
10192
+ if (draft.agent) patch.agent = draft.agent;
10193
+ if ("model" in draft) patch.model = draft.model ?? null;
10194
+ if (draft.effort) patch.effort = draft.effort;
10195
+ if (draft.fast !== void 0) patch.fast = draft.fast;
10196
+ if (draft.planMode !== void 0) patch.planMode = draft.planMode;
10197
+ if (draft.autonomy) patch.autonomy = draft.autonomy;
10198
+ if (Object.keys(patch).length === 0) return;
10199
+ getOrchestrator().setThreadOptions(chatId, patch);
10200
+ }
10201
+ function stagePhoneTurnFiles(worktreePath, draft) {
10202
+ const links = phoneTurnAttachments({ links: draft.links });
10203
+ const files = stageBuffersAsAttachments(
10204
+ worktreePath,
10205
+ (draft.files ?? []).map((file) => ({ name: file.name, dataBase64: file.dataBase64 }))
10206
+ );
10207
+ return [...links, ...files];
10208
+ }
10209
+ function handlePhoneControl(cmd, opts) {
10210
+ const log = opts.onLog ?? (() => void 0);
10211
+ if (cmd.op === "list") {
10212
+ emitControl(opts, { op: "sidebar", ...listPhoneSidebar() });
10213
+ return;
10214
+ }
10215
+ if (cmd.op === "history") {
10216
+ const page = listPhoneHistory({ query: cmd.query, after: cmd.after });
10217
+ emitControl(opts, {
10218
+ op: "history",
10219
+ chats: page.chats,
10220
+ total: page.total,
10221
+ ...page.next ? { next: page.next } : {},
10222
+ ...cmd.query ? { query: cmd.query } : {},
10223
+ ...cmd.after ? { after: cmd.after } : {}
10224
+ });
10225
+ return;
10226
+ }
10227
+ if (cmd.op === "sources") {
10228
+ const repoPath = cmd.repoPath;
10229
+ const query = cmd.query;
10230
+ void listPhoneSources(repoPath, query).then(
10231
+ (page) => emitControl(opts, {
10232
+ op: "sources",
10233
+ repoPath,
10234
+ ...query ? { query } : {},
10235
+ ...page
10236
+ })
10237
+ ).catch((err) => {
10238
+ emitControl(opts, { op: "error", message: phoneError(err) });
10239
+ });
10240
+ return;
10241
+ }
10242
+ if (cmd.op === "models") {
10243
+ const agent = cmd.agent;
10244
+ void listPhoneModels(agent).then((models) => emitControl(opts, { op: "models", agent, models })).catch((err) => {
10245
+ emitControl(opts, { op: "error", message: phoneError(err) });
10246
+ });
10247
+ return;
10248
+ }
10249
+ if (cmd.op === "dictate") {
10250
+ const id = cmd.id;
10251
+ const transcribe = opts.transcribeWav;
10252
+ if (!transcribe) {
10253
+ emitControl(opts, { op: "error", message: "Speech is not available on this Mac." });
10254
+ return;
10255
+ }
10256
+ void transcribe(cmd.audioBase64).then((text6) => emitControl(opts, { op: "dictated", id, text: text6.trim() })).catch((err) => {
10257
+ emitControl(opts, { op: "error", message: phoneError(err) });
10258
+ });
10259
+ return;
10260
+ }
10261
+ if (cmd.op === "options") {
10262
+ const thread = readPhoneThread(cmd.chatId);
10263
+ if (!thread) {
10264
+ emitControl(opts, { op: "error", chatId: cmd.chatId, message: "That agent is not on this Mac." });
10265
+ return;
10266
+ }
10267
+ try {
10268
+ applyPhoneOptions(thread.id, cmd);
10269
+ const fresh = readPhoneThread(thread.id) ?? thread;
10270
+ emitControl(opts, { op: "options", chatId: fresh.id, ...phoneComposerOptions(fresh) });
10271
+ } catch (err) {
10272
+ emitControl(opts, { op: "error", chatId: cmd.chatId, message: phoneError(err) });
10273
+ }
10274
+ return;
10275
+ }
10276
+ if (cmd.op === "create") {
10277
+ if (cmd.where === "worktree") {
10278
+ void (async () => {
10279
+ try {
10280
+ const opened = createPhoneWorktreeAgent(cmd.chatId, cmd);
10281
+ const prompt = cmd.prompt?.trim() || (cmd.files?.length || cmd.links?.length ? "See the attached files." : "");
10282
+ if (prompt) {
10283
+ try {
10284
+ await getOrchestrator().send(opened.chat.id, formatRemotePrompt(prompt));
10285
+ } catch (err) {
10286
+ emitControl(opts, { op: "error", chatId: opened.chat.id, message: phoneError(err) });
10287
+ }
10288
+ }
10289
+ const fresh = openPhoneChat(opened.chat.id) ?? opened;
10290
+ emitControl(opts, { op: "opened", ...fresh });
10291
+ if (prompt) deliverPhoneTurn(opts, fresh.chat.id);
10292
+ } catch (err) {
10293
+ emitControl(opts, { op: "error", message: phoneError(err) });
10294
+ }
10295
+ })();
10296
+ return;
10297
+ }
10298
+ if (cmd.where === "project") {
10299
+ void createPhoneProjectWorktree(cmd).then(async (opened) => {
10300
+ const prompt = resolveCreateFirstPrompt({
10301
+ sourceType: cmd.sourceType ?? "branch",
10302
+ prompt: cmd.prompt
10303
+ }) || (cmd.files?.length || cmd.links?.length ? "See the attached files." : "");
10304
+ if (prompt) {
10305
+ try {
10306
+ await getOrchestrator().send(opened.chat.id, formatRemotePrompt(prompt));
10307
+ } catch (err) {
10308
+ emitControl(opts, { op: "error", chatId: opened.chat.id, message: phoneError(err) });
10309
+ }
10310
+ }
10311
+ const fresh = openPhoneChat(opened.chat.id) ?? opened;
10312
+ emitControl(opts, { op: "opened", ...fresh });
10313
+ if (prompt) deliverPhoneTurn(opts, fresh.chat.id);
10314
+ }).catch((err) => {
10315
+ emitControl(opts, { op: "error", message: phoneError(err) });
10316
+ });
10317
+ return;
10318
+ }
10319
+ const goal = cmd.goal?.trim() || cmd.prompt?.trim() || "";
10320
+ if (goal || cmd.files?.length || cmd.links?.length) {
10321
+ void createPhoneOrchestration({ ...cmd, goal }).then((opened) => {
10322
+ emitControl(opts, { op: "opened", ...opened });
10323
+ deliverPhoneTurn(opts, opened.chat.id);
10324
+ }).catch((err) => {
10325
+ emitControl(opts, { op: "error", message: phoneError(err) });
10326
+ });
10327
+ return;
10328
+ }
10329
+ try {
10330
+ const agent = coerceOrchestratorAgent(
10331
+ cmd.agent ?? opts.agent ?? resolveOrchestratorDefaults().agent
10332
+ );
10333
+ emitControl(opts, { op: "opened", ...createPhoneChat(agent) });
10334
+ } catch (err) {
10335
+ emitControl(opts, { op: "error", message: phoneError(err) });
10336
+ }
10337
+ return;
10338
+ }
10339
+ if (cmd.op === "open") {
10340
+ const thread = findPhoneChat(cmd.chatId);
10341
+ if (!thread) {
10342
+ emitControl(opts, { op: "error", message: "That agent is not on this Mac." });
10343
+ return;
10344
+ }
10345
+ if (thread.status === "archived") {
10346
+ void getOrchestrator().restore(thread.id).then(() => {
10347
+ emitControl(opts, { op: "sidebar", ...listPhoneSidebar() });
10348
+ publishOpenedChat(opts, thread.id);
10349
+ }).catch((err) => {
10350
+ emitControl(opts, { op: "error", message: phoneError(err) });
10351
+ });
10352
+ return;
10353
+ }
10354
+ publishOpenedChat(opts, thread.id);
10355
+ return;
10356
+ }
10357
+ if (cmd.op === "archive") {
10358
+ const thread = readPhoneThread(cmd.chatId);
10359
+ if (!thread) {
10360
+ emitControl(opts, { op: "error", message: "That agent is not on this Mac." });
10361
+ return;
10362
+ }
10363
+ bumpChat(thread.id);
10364
+ interruptChat(thread.id, log);
10365
+ archivePhoneChat(thread.id);
10366
+ emitControl(opts, { op: "sidebar", ...listPhoneSidebar() });
10367
+ return;
10368
+ }
10369
+ if (cmd.op === "restore") {
10370
+ const chatId2 = cmd.chatId;
10371
+ void getOrchestrator().restore(chatId2).then(() => {
10372
+ emitControl(opts, { op: "sidebar", ...listPhoneSidebar() });
10373
+ emitControl(opts, { op: "restored", chatId: chatId2 });
10374
+ }).catch((err) => {
10375
+ emitControl(opts, {
10376
+ op: "error",
10377
+ chatId: chatId2,
10378
+ message: err instanceof Error ? err.message : String(err)
10379
+ });
10380
+ });
10381
+ return;
10382
+ }
10383
+ if (cmd.op === "media") {
10384
+ const thread = readPhoneThread(cmd.chatId);
10385
+ const dataUrl = thread ? phoneImageDataUrl(thread.worktreePath, cmd.path) : null;
10386
+ emitControl(opts, {
10387
+ op: "media",
10388
+ chatId: cmd.chatId,
10389
+ path: cmd.path,
10390
+ ...dataUrl ? { dataUrl } : {}
10391
+ });
10392
+ return;
10393
+ }
10394
+ if (cmd.op === "open-file") {
10395
+ const thread = readPhoneThread(cmd.chatId);
10396
+ const relative = thread ? phoneWorktreeRelative(thread.worktreePath, cmd.path) : null;
10397
+ if (!thread || !relative) {
10398
+ emitControl(opts, { op: "error", chatId: cmd.chatId, message: "That file is not in this worktree." });
10399
+ return;
10400
+ }
10401
+ void getOrchestrator().statPath(thread.id, relative).then((kind) => {
10402
+ if (kind === "missing") {
10403
+ emitControl(opts, { op: "error", chatId: cmd.chatId, message: "That file is not in this worktree." });
10404
+ return;
10405
+ }
10406
+ opts.onOpenWorktreeFile?.({
10407
+ threadId: thread.id,
10408
+ path: relative,
10409
+ ...kind === "dir" ? { directory: true } : {},
10410
+ ...cmd.startLine != null ? { startLine: cmd.startLine } : {},
10411
+ ...cmd.endLine != null ? { endLine: cmd.endLine } : {}
10412
+ });
10413
+ }).catch(() => {
10414
+ emitControl(opts, { op: "error", chatId: cmd.chatId, message: "That file is not in this worktree." });
10415
+ });
10416
+ return;
10417
+ }
10418
+ if (cmd.op === "stop") {
10419
+ if (!readPhoneThread(cmd.chatId)) {
10420
+ emitControl(opts, { op: "error", chatId: cmd.chatId, message: "That agent is not on this Mac." });
10421
+ return;
10422
+ }
10423
+ bumpChat(cmd.chatId);
10424
+ interruptChat(cmd.chatId, log);
10425
+ emitControl(opts, { op: "stopped", chatId: cmd.chatId });
10426
+ return;
10427
+ }
10428
+ const chatId = cmd.chatId;
10429
+ const generation = bumpChat(chatId);
10430
+ phoneTurnGen.set(chatId, generation);
10431
+ interruptChat(chatId, log);
10432
+ enqueueChat(chatId, async () => {
10433
+ if (chatGeneration.get(chatId) !== generation) return;
10434
+ const thread = readPhoneThread(chatId);
10435
+ if (!thread) {
10436
+ emitControl(opts, { op: "error", chatId, message: "That agent is not on this Mac." });
10437
+ return;
10438
+ }
10439
+ if (thread.status === "running" || thread.status === "queued") {
10440
+ try {
10441
+ getOrchestrator().stop(thread.id, { clearQueue: true });
10442
+ } catch {
10443
+ }
10444
+ }
10445
+ if (chatGeneration.get(chatId) !== generation) return;
10446
+ const stopStream = watchPhoneStream(opts, chatId, thread.id, generation);
10447
+ try {
10448
+ try {
10449
+ applyPhoneOptions(thread.id, cmd);
10450
+ } catch (err) {
10451
+ emitControl(opts, { op: "error", chatId, message: phoneError(err) });
10452
+ }
10453
+ const live = readPhoneThread(thread.id) ?? thread;
10454
+ const staged = stagePhoneTurnFiles(live.worktreePath, cmd);
10455
+ const text6 = cmd.text.trim() || (staged.length ? "See the attached files." : "");
10456
+ const formatted = formatRemotePrompt(text6);
10457
+ await getOrchestrator().send(thread.id, formatted, { attachments: staged });
10458
+ await getOrchestrator().waitForTurn(thread.id, 14 * 60 * 1e3);
10459
+ } catch (err) {
10460
+ if (chatGeneration.get(chatId) !== generation) return;
10461
+ const message = err instanceof Error ? err.message : String(err);
10462
+ emitControl(opts, { op: "error", chatId, message: `Sideboard failed: ${message}` });
10463
+ return;
10464
+ } finally {
10465
+ stopStream();
10466
+ finishPhoneTurn(chatId, generation);
10467
+ }
10468
+ if (chatGeneration.get(chatId) !== generation) return;
10469
+ emitChatTurn(opts, chatId, thread.id);
10470
+ });
10471
+ }
9390
10472
  function handleRemoteInbound(text6, opts) {
9391
- const generation = ++inboundGeneration;
9392
10473
  const log = opts.onLog ?? (() => void 0);
9393
10474
  const body = text6.trim();
10475
+ const control = takePhoneControl(body);
10476
+ if (control) {
10477
+ if (control === "invalid") {
10478
+ emitControl(opts, { op: "error", message: "invalid phone command" });
10479
+ return;
10480
+ }
10481
+ handlePhoneControl(control, opts);
10482
+ return;
10483
+ }
10484
+ const generation = ++inboundGeneration;
9394
10485
  if (body) interruptRemoteCoordinator(opts.deviceId, log);
9395
10486
  enqueue(async () => {
9396
10487
  if (generation !== inboundGeneration) return;
@@ -9505,7 +10596,8 @@ function runRemoteHost(opts) {
9505
10596
  type: "host_register",
9506
10597
  deviceId: opts.deviceId,
9507
10598
  deviceLabel,
9508
- hostSecret: opts.hostSecret
10599
+ hostSecret: opts.hostSecret,
10600
+ ...opts.accountToken?.trim() ? { accountToken: opts.accountToken.trim() } : {}
9509
10601
  });
9510
10602
  };
9511
10603
  const loop = async () => {
@@ -9613,7 +10705,8 @@ function runRemoteHost(opts) {
9613
10705
  handleRemoteInbound("stop", {
9614
10706
  deviceId: opts.deviceId,
9615
10707
  onLog: log,
9616
- onOutbound: emit
10708
+ onOutbound: emit,
10709
+ onOpenWorktreeFile: opts.onOpenWorktreeFile
9617
10710
  });
9618
10711
  return;
9619
10712
  }
@@ -9621,7 +10714,9 @@ function runRemoteHost(opts) {
9621
10714
  handleRemoteInbound(msg.text, {
9622
10715
  deviceId: opts.deviceId,
9623
10716
  onLog: log,
9624
- onOutbound: emit
10717
+ onOutbound: emit,
10718
+ transcribeWav: opts.transcribeWav,
10719
+ onOpenWorktreeFile: opts.onOpenWorktreeFile
9625
10720
  });
9626
10721
  }
9627
10722
  };
@@ -9648,6 +10743,671 @@ function runRemoteHost(opts) {
9648
10743
  };
9649
10744
  }
9650
10745
 
10746
+ // src/relay/account-login.ts
10747
+ import { randomBytes as randomBytes7 } from "crypto";
10748
+
10749
+ // src/relay/accounts.ts
10750
+ import { createHash as createHash2, randomBytes as randomBytes6, randomUUID as randomUUID8, timingSafeEqual as timingSafeEqual2 } from "crypto";
10751
+ import { mkdirSync as mkdirSync4, readFileSync as readFileSync5, realpathSync } from "fs";
10752
+ import { createRequire } from "module";
10753
+ import { dirname, join as join6 } from "path";
10754
+ var GITHUB_ACCOUNT_ISSUER = "https://github.com";
10755
+ var RELAY_DEVICE_CAP = 3;
10756
+ function thisModuleFile() {
10757
+ const cjsFile = typeof __filename !== "undefined" ? __filename : "";
10758
+ return cjsFile || process.argv[1] || join6(process.cwd(), "package.json");
10759
+ }
10760
+ function openDatabase(filePath) {
10761
+ const req = createRequire(thisModuleFile());
10762
+ const specifier = "node:sqlite";
10763
+ const { DatabaseSync: SqliteDatabase } = req(specifier);
10764
+ const db = new SqliteDatabase(filePath);
10765
+ db.exec("PRAGMA journal_mode = WAL");
10766
+ return db;
10767
+ }
10768
+ function sha256(value) {
10769
+ return createHash2("sha256").update(value).digest("hex");
10770
+ }
10771
+ function sameHash(a, b) {
10772
+ const left = Buffer.from(a);
10773
+ const right = Buffer.from(b);
10774
+ return left.length === right.length && timingSafeEqual2(left, right);
10775
+ }
10776
+ function normalizeEmail(email) {
10777
+ const trimmed = email?.trim().toLowerCase() ?? "";
10778
+ return trimmed || null;
10779
+ }
10780
+ function longestMountPoint(dir, mounts) {
10781
+ let best = "/";
10782
+ for (const line of mounts.split("\n")) {
10783
+ const point = line.split(" ")[1]?.replace(/\\040/g, " ");
10784
+ if (!point) continue;
10785
+ if (dir === point || dir.startsWith(point.endsWith("/") ? point : `${point}/`)) {
10786
+ if (point.length > best.length) best = point;
10787
+ }
10788
+ }
10789
+ return best;
10790
+ }
10791
+ function assertRelayAccountsDurable(filePath) {
10792
+ if (process.env.SIDEBOARD_RELAY_ACCOUNTS_EPHEMERAL === "1") return;
10793
+ if (process.env.NODE_ENV !== "production") return;
10794
+ if (process.platform !== "linux") return;
10795
+ const dir = realpathSync(dirname(filePath));
10796
+ const mounts = readFileSync5("/proc/mounts", "utf8");
10797
+ const point = longestMountPoint(dir, mounts);
10798
+ if (point === "/") {
10799
+ throw new Error(
10800
+ `Relay accounts at ${filePath} are on the machine root filesystem. Mount a Fly volume so sign-ins survive deploys.`
10801
+ );
10802
+ }
10803
+ }
10804
+ var RelayAccountStore = class {
10805
+ db;
10806
+ constructor(filePath) {
10807
+ mkdirSync4(dirname(filePath), { recursive: true });
10808
+ this.db = openDatabase(filePath);
10809
+ this.db.exec(`
10810
+ CREATE TABLE IF NOT EXISTS accounts (
10811
+ id TEXT PRIMARY KEY,
10812
+ created_at INTEGER NOT NULL,
10813
+ device_cap INTEGER NOT NULL
10814
+ );
10815
+ CREATE TABLE IF NOT EXISTS identities (
10816
+ issuer TEXT NOT NULL,
10817
+ subject TEXT NOT NULL,
10818
+ account_id TEXT NOT NULL,
10819
+ email TEXT,
10820
+ email_verified INTEGER NOT NULL,
10821
+ PRIMARY KEY (issuer, subject)
10822
+ );
10823
+ CREATE INDEX IF NOT EXISTS identities_email ON identities(email);
10824
+ CREATE TABLE IF NOT EXISTS credentials (
10825
+ token_hash TEXT PRIMARY KEY,
10826
+ account_id TEXT NOT NULL,
10827
+ created_at INTEGER NOT NULL
10828
+ );
10829
+ CREATE TABLE IF NOT EXISTS devices (
10830
+ device_id TEXT PRIMARY KEY,
10831
+ account_id TEXT NOT NULL,
10832
+ host_secret_hash TEXT NOT NULL,
10833
+ device_label TEXT NOT NULL,
10834
+ created_at INTEGER NOT NULL
10835
+ );
10836
+ `);
10837
+ }
10838
+ close() {
10839
+ this.db.close();
10840
+ }
10841
+ /**
10842
+ * First sign-in creates an account. The same issuer+subject returns that
10843
+ * account. A verified email match attaches a second provider. `linkAccountId`
10844
+ * attaches while that account’s credential is already signed in.
10845
+ * Each call mints a new Mac credential. Older credentials stay valid.
10846
+ */
10847
+ signIn(identity, linkAccountId) {
10848
+ const issuer = identity.issuer.trim();
10849
+ const subject = identity.subject.trim();
10850
+ if (!issuer || !subject) throw new Error("Sign-in is missing an identity.");
10851
+ const email = normalizeEmail(identity.email);
10852
+ const verified = Boolean(identity.emailVerified && email);
10853
+ const link = linkAccountId?.trim() || null;
10854
+ this.db.exec("BEGIN");
10855
+ try {
10856
+ const existing = this.db.prepare("SELECT account_id, email FROM identities WHERE issuer = ? AND subject = ?").get(issuer, subject);
10857
+ let accountId;
10858
+ if (existing) {
10859
+ if (link && link !== existing.account_id) {
10860
+ throw new Error("That sign-in already belongs to another Sideboard account.");
10861
+ }
10862
+ accountId = existing.account_id;
10863
+ this.requireAccount(accountId);
10864
+ if (email) {
10865
+ this.db.prepare("UPDATE identities SET email = ?, email_verified = ? WHERE issuer = ? AND subject = ?").run(email, verified ? 1 : 0, issuer, subject);
10866
+ }
10867
+ } else if (link) {
10868
+ this.requireAccount(link);
10869
+ accountId = link;
10870
+ this.insertIdentity(issuer, subject, accountId, email, verified);
10871
+ } else {
10872
+ const byEmail = verified ? this.accountIdForVerifiedEmail(email) : void 0;
10873
+ accountId = byEmail ?? randomUUID8();
10874
+ if (!byEmail) {
10875
+ this.db.prepare("INSERT INTO accounts (id, created_at, device_cap) VALUES (?, ?, ?)").run(accountId, Date.now(), RELAY_DEVICE_CAP);
10876
+ }
10877
+ this.insertIdentity(issuer, subject, accountId, email, verified);
10878
+ }
10879
+ const credential = this.mintCredential(accountId);
10880
+ 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);
10881
+ this.db.exec("COMMIT");
10882
+ return { accountId, credential, email: stored?.email ?? email };
10883
+ } catch (err) {
10884
+ this.db.exec("ROLLBACK");
10885
+ throw err;
10886
+ }
10887
+ }
10888
+ verifyCredential(token) {
10889
+ const trimmed = token.trim();
10890
+ if (!trimmed) return null;
10891
+ const row = this.db.prepare("SELECT account_id FROM credentials WHERE token_hash = ?").get(sha256(trimmed));
10892
+ return row?.account_id ?? null;
10893
+ }
10894
+ /** Drop one Mac credential. The account and its other Macs stay. */
10895
+ logout(token) {
10896
+ const trimmed = token.trim();
10897
+ if (!trimmed) return;
10898
+ this.db.prepare("DELETE FROM credentials WHERE token_hash = ?").run(sha256(trimmed));
10899
+ }
10900
+ /** Delete the account and every identity, credential, and Mac bound to it. */
10901
+ revokeAccount(accountId) {
10902
+ const id = accountId.trim();
10903
+ if (!id) return;
10904
+ this.db.exec("BEGIN");
10905
+ try {
10906
+ this.db.prepare("DELETE FROM devices WHERE account_id = ?").run(id);
10907
+ this.db.prepare("DELETE FROM credentials WHERE account_id = ?").run(id);
10908
+ this.db.prepare("DELETE FROM identities WHERE account_id = ?").run(id);
10909
+ this.db.prepare("DELETE FROM accounts WHERE id = ?").run(id);
10910
+ this.db.exec("COMMIT");
10911
+ } catch (err) {
10912
+ this.db.exec("ROLLBACK");
10913
+ throw err;
10914
+ }
10915
+ }
10916
+ /**
10917
+ * Accept `host_register` for a signed-in account. The device row survives
10918
+ * process restart, so a later register with a different host secret is rejected
10919
+ * and a new device id counts against the cap.
10920
+ */
10921
+ authorizeHost(input) {
10922
+ const deviceId = input.deviceId.trim();
10923
+ const hostSecret = input.hostSecret.trim();
10924
+ const deviceLabel = input.deviceLabel.trim() || "This Mac";
10925
+ if (!deviceId || !hostSecret) return { ok: false, message: "sign in required" };
10926
+ const accountId = this.verifyCredential(input.accountToken);
10927
+ if (!accountId) return { ok: false, message: "sign in required" };
10928
+ this.db.exec("BEGIN");
10929
+ try {
10930
+ const account = this.db.prepare("SELECT id, device_cap FROM accounts WHERE id = ?").get(accountId);
10931
+ if (!account) {
10932
+ this.db.exec("ROLLBACK");
10933
+ return { ok: false, message: "sign in required" };
10934
+ }
10935
+ const device = this.db.prepare("SELECT device_id, account_id, host_secret_hash FROM devices WHERE device_id = ?").get(deviceId);
10936
+ const secretHash = sha256(hostSecret);
10937
+ if (!device) {
10938
+ const count = this.db.prepare("SELECT COUNT(*) AS n FROM devices WHERE account_id = ?").get(accountId);
10939
+ if (count.n >= account.device_cap) {
10940
+ this.db.exec("ROLLBACK");
10941
+ return { ok: false, message: "This account already has the maximum number of Macs." };
10942
+ }
10943
+ this.db.prepare(
10944
+ "INSERT INTO devices (device_id, account_id, host_secret_hash, device_label, created_at) VALUES (?, ?, ?, ?, ?)"
10945
+ ).run(deviceId, accountId, secretHash, deviceLabel, Date.now());
10946
+ this.db.exec("COMMIT");
10947
+ return { ok: true, accountId };
10948
+ }
10949
+ if (device.account_id !== accountId) {
10950
+ this.db.exec("ROLLBACK");
10951
+ return { ok: false, message: "This Mac is registered to another account." };
10952
+ }
10953
+ if (!sameHash(device.host_secret_hash, secretHash)) {
10954
+ this.db.exec("ROLLBACK");
10955
+ return { ok: false, message: "host secret rejected" };
10956
+ }
10957
+ this.db.prepare("UPDATE devices SET device_label = ? WHERE device_id = ?").run(deviceLabel, deviceId);
10958
+ this.db.exec("COMMIT");
10959
+ return { ok: true, accountId };
10960
+ } catch (err) {
10961
+ this.db.exec("ROLLBACK");
10962
+ throw err;
10963
+ }
10964
+ }
10965
+ requireAccount(accountId) {
10966
+ const row = this.db.prepare("SELECT id FROM accounts WHERE id = ?").get(accountId);
10967
+ if (!row) throw new Error("That Sideboard account is no longer on the relay.");
10968
+ }
10969
+ accountIdForVerifiedEmail(email) {
10970
+ const row = this.db.prepare(
10971
+ "SELECT account_id FROM identities WHERE email_verified = 1 AND email = ? LIMIT 1"
10972
+ ).get(email);
10973
+ return row?.account_id;
10974
+ }
10975
+ insertIdentity(issuer, subject, accountId, email, verified) {
10976
+ this.db.prepare(
10977
+ "INSERT INTO identities (issuer, subject, account_id, email, email_verified) VALUES (?, ?, ?, ?, ?)"
10978
+ ).run(issuer, subject, accountId, email, verified ? 1 : 0);
10979
+ }
10980
+ mintCredential(accountId) {
10981
+ const credential = randomBytes6(32).toString("hex");
10982
+ this.db.prepare("INSERT INTO credentials (token_hash, account_id, created_at) VALUES (?, ?, ?)").run(sha256(credential), accountId, Date.now());
10983
+ return credential;
10984
+ }
10985
+ };
10986
+
10987
+ // src/relay/account-oauth.ts
10988
+ var RELAY_GIT_HOSTS = ["github"];
10989
+ var REMOTE_LOGIN_PATH = "/remote/login";
10990
+ var REMOTE_OAUTH_CALLBACK_PATH = "/remote/oauth/callback";
10991
+ var REMOTE_OAUTH_RESULT_PATH = "/remote/oauth/result";
10992
+ var REMOTE_LOGOUT_PATH = "/remote/logout";
10993
+ var STATE_RE = /^[A-Za-z0-9_-]{16,128}$/;
10994
+ var LOGIN_TTL_MS = 10 * 60 * 1e3;
10995
+ var RelayLoginSessions = class {
10996
+ waiting = /* @__PURE__ */ new Map();
10997
+ results = /* @__PURE__ */ new Map();
10998
+ begin(state, provider, accountId) {
10999
+ this.sweep();
11000
+ this.waiting.set(state, { provider, accountId, expiresAt: Date.now() + LOGIN_TTL_MS });
11001
+ }
11002
+ takeLogin(state) {
11003
+ this.sweep();
11004
+ const row = this.waiting.get(state);
11005
+ if (!row) return null;
11006
+ this.waiting.delete(state);
11007
+ if (row.expiresAt < Date.now()) return null;
11008
+ return row;
11009
+ }
11010
+ putResult(state, value) {
11011
+ this.results.set(state, { value, expiresAt: Date.now() + LOGIN_TTL_MS });
11012
+ }
11013
+ /** `undefined` means the browser flow has not finished. */
11014
+ takeResult(state) {
11015
+ this.sweep();
11016
+ const row = this.results.get(state);
11017
+ if (!row) return this.waiting.has(state) ? void 0 : null;
11018
+ this.results.delete(state);
11019
+ if (row.expiresAt < Date.now()) return null;
11020
+ return row.value;
11021
+ }
11022
+ sweep() {
11023
+ const now = Date.now();
11024
+ for (const [state, row] of this.waiting) {
11025
+ if (row.expiresAt < now) this.waiting.delete(state);
11026
+ }
11027
+ for (const [state, row] of this.results) {
11028
+ if (row.expiresAt < now) this.results.delete(state);
11029
+ }
11030
+ }
11031
+ };
11032
+ var GIT_HOST_AUTH = {
11033
+ github: {
11034
+ label: "GitHub",
11035
+ authorizeUrl: "https://github.com/login/oauth/authorize",
11036
+ scope: "user:email",
11037
+ fetchIdentity: fetchGithubIdentity
11038
+ }
11039
+ };
11040
+ function providerOf(value) {
11041
+ return RELAY_GIT_HOSTS.includes(value) ? value : null;
11042
+ }
11043
+ function relayAuthorizeUrl(provider, client, redirectUri, state) {
11044
+ const host = GIT_HOST_AUTH[provider];
11045
+ const url = new URL(host.authorizeUrl);
11046
+ url.searchParams.set("client_id", client.clientId);
11047
+ url.searchParams.set("redirect_uri", redirectUri);
11048
+ url.searchParams.set("scope", host.scope);
11049
+ url.searchParams.set("state", state);
11050
+ return url.toString();
11051
+ }
11052
+ async function readJson(res) {
11053
+ const text6 = await res.text();
11054
+ if (!text6) return {};
11055
+ try {
11056
+ return JSON.parse(text6);
11057
+ } catch {
11058
+ throw new Error("Sign-in provider returned a non-JSON response.");
11059
+ }
11060
+ }
11061
+ function accessToken(body) {
11062
+ const token = body.access_token;
11063
+ if (typeof token !== "string" || !token.trim()) {
11064
+ const message = body.error_description;
11065
+ throw new Error(typeof message === "string" && message.trim() ? message : "Sign-in provider rejected the code.");
11066
+ }
11067
+ return token.trim();
11068
+ }
11069
+ async function fetchGithubIdentity(client, code, redirectUri, fetchImpl) {
11070
+ const tokenRes = await fetchImpl("https://github.com/login/oauth/access_token", {
11071
+ method: "POST",
11072
+ headers: { Accept: "application/json", "Content-Type": "application/json" },
11073
+ body: JSON.stringify({
11074
+ client_id: client.clientId,
11075
+ client_secret: client.clientSecret,
11076
+ code,
11077
+ redirect_uri: redirectUri
11078
+ })
11079
+ });
11080
+ const token = accessToken(await readJson(tokenRes));
11081
+ const userRes = await fetchImpl("https://api.github.com/user", {
11082
+ headers: { Authorization: `Bearer ${token}`, Accept: "application/vnd.github+json", "User-Agent": "sideboard-relay" }
11083
+ });
11084
+ const user = await readJson(userRes);
11085
+ const subject = typeof user.id === "number" ? String(user.id) : "";
11086
+ if (!subject) throw new Error("GitHub did not return an account id.");
11087
+ const emailRes = await fetchImpl("https://api.github.com/user/emails", {
11088
+ headers: { Authorization: `Bearer ${token}`, Accept: "application/vnd.github+json", "User-Agent": "sideboard-relay" }
11089
+ });
11090
+ const emails = await readJson(emailRes);
11091
+ const list = Array.isArray(emails) ? emails : [];
11092
+ const primary = list.find((row) => row.primary === true && row.verified === true);
11093
+ const anyVerified = list.find((row) => row.verified === true);
11094
+ const chosen = primary ?? anyVerified;
11095
+ const email = chosen && typeof chosen.email === "string" ? chosen.email : null;
11096
+ return { issuer: GITHUB_ACCOUNT_ISSUER, subject, email, emailVerified: Boolean(email) };
11097
+ }
11098
+ async function fetchRelayIdentity(provider, client, code, redirectUri, fetchImpl) {
11099
+ return GIT_HOST_AUTH[provider].fetchIdentity(client, code, redirectUri, fetchImpl);
11100
+ }
11101
+ async function readBody(req) {
11102
+ const chunks = [];
11103
+ let size = 0;
11104
+ for await (const chunk of req) {
11105
+ const buf = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
11106
+ size += buf.length;
11107
+ if (size > 8192) throw new Error("body too large");
11108
+ chunks.push(buf);
11109
+ }
11110
+ const text6 = Buffer.concat(chunks).toString("utf8").trim();
11111
+ if (!text6) return {};
11112
+ return JSON.parse(text6);
11113
+ }
11114
+ function sendJson2(res, status, body) {
11115
+ res.writeHead(status, { "Content-Type": "application/json", "Cache-Control": "no-store" });
11116
+ res.end(JSON.stringify(body));
11117
+ }
11118
+ function relayOAuthReturnPage(title, body) {
11119
+ return `<!DOCTYPE html>
11120
+ <html lang="en">
11121
+ <head>
11122
+ <meta charset="utf-8">
11123
+ <meta name="viewport" content="width=device-width, initial-scale=1">
11124
+ <meta name="robots" content="noindex">
11125
+ <title>${escapeHtml(title)}</title>
11126
+ <style>
11127
+ :root {
11128
+ --bg: #09090b;
11129
+ --card: #16171b;
11130
+ --line: rgba(255, 255, 255, 0.08);
11131
+ --text: #f4f5f7;
11132
+ --muted: #9aa3ad;
11133
+ --blue: #3b9eff;
11134
+ --blue-fill: #0f7ed4;
11135
+ }
11136
+ * { box-sizing: border-box }
11137
+ body {
11138
+ margin: 0;
11139
+ min-height: 100vh;
11140
+ display: grid;
11141
+ place-items: center;
11142
+ padding: 48px 24px;
11143
+ background:
11144
+ radial-gradient(closest-side, rgba(15, 126, 212, 0.22), transparent 72%) center -120px / 720px 480px no-repeat,
11145
+ var(--bg);
11146
+ color: var(--text);
11147
+ font-family: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", "Helvetica Neue", sans-serif;
11148
+ -webkit-font-smoothing: antialiased;
11149
+ }
11150
+ main {
11151
+ width: min(28rem, 100%);
11152
+ background: var(--card);
11153
+ border: 1px solid var(--line);
11154
+ border-radius: 16px;
11155
+ padding: 28px 28px 32px;
11156
+ }
11157
+ .brand {
11158
+ display: flex;
11159
+ align-items: center;
11160
+ gap: 10px;
11161
+ margin: 0 0 22px;
11162
+ font-weight: 650;
11163
+ font-size: 17px;
11164
+ letter-spacing: -0.02em;
11165
+ }
11166
+ .mark { position: relative; display: inline-block; width: 26px; height: 26px; flex: none }
11167
+ .mark i { position: absolute; width: 18px; height: 18px; border-radius: 3px; transform: rotate(14deg) }
11168
+ .mark i:first-child { left: 6px; top: 6px; background: var(--blue-fill) }
11169
+ .mark i:last-child { left: 1px; top: 1px; border: 2px solid #d7dbdf }
11170
+ h1 { margin: 0; font-size: 1.5rem; line-height: 1.2; letter-spacing: -0.03em; font-weight: 650 }
11171
+ p { margin: 10px 0 0; line-height: 1.55; color: var(--muted) }
11172
+ a { color: var(--blue) }
11173
+ </style>
11174
+ </head>
11175
+ <body>
11176
+ <main>
11177
+ <div class="brand"><span class="mark" aria-hidden="true"><i></i><i></i></span>Sideboard</div>
11178
+ ${body}
11179
+ </main>
11180
+ </body>
11181
+ </html>`;
11182
+ }
11183
+ function sendHtml(res, status, title, body) {
11184
+ res.writeHead(status, { "Content-Type": "text/html; charset=utf-8", "Cache-Control": "no-store" });
11185
+ res.end(relayOAuthReturnPage(title, body));
11186
+ }
11187
+ async function handleRelayAccountHttp(opts) {
11188
+ const reqUrl = opts.req.url || "/";
11189
+ const path2 = reqUrl.split("?")[0];
11190
+ const method = opts.req.method || "GET";
11191
+ const origin = opts.publicOrigin.replace(/\/+$/, "");
11192
+ const redirectUri = `${origin}${REMOTE_OAUTH_CALLBACK_PATH}`;
11193
+ const fetchImpl = opts.fetchImpl ?? fetch;
11194
+ if (method === "POST" && path2 === REMOTE_LOGIN_PATH) {
11195
+ let body;
11196
+ try {
11197
+ body = await readBody(opts.req);
11198
+ } catch {
11199
+ sendJson2(opts.res, 400, { ok: false, error: "invalid login request" });
11200
+ return true;
11201
+ }
11202
+ const provider = providerOf(typeof body.provider === "string" ? body.provider : "");
11203
+ const state = typeof body.state === "string" ? body.state.trim() : "";
11204
+ if (!provider || !STATE_RE.test(state)) {
11205
+ sendJson2(opts.res, 400, { ok: false, error: "invalid login request" });
11206
+ return true;
11207
+ }
11208
+ const client = opts.oauth[provider];
11209
+ if (!client?.clientId || !client.clientSecret) {
11210
+ sendJson2(opts.res, 503, {
11211
+ ok: false,
11212
+ error: `${GIT_HOST_AUTH[provider].label} sign-in is not configured on this relay.`
11213
+ });
11214
+ return true;
11215
+ }
11216
+ let accountId = null;
11217
+ const linkToken = typeof body.accountToken === "string" ? body.accountToken.trim() : "";
11218
+ if (linkToken) {
11219
+ accountId = opts.store.verifyCredential(linkToken);
11220
+ if (!accountId) {
11221
+ sendJson2(opts.res, 401, { ok: false, error: "sign in required" });
11222
+ return true;
11223
+ }
11224
+ }
11225
+ opts.sessions.begin(state, provider, accountId);
11226
+ sendJson2(opts.res, 200, { ok: true, url: relayAuthorizeUrl(provider, client, redirectUri, state) });
11227
+ return true;
11228
+ }
11229
+ if (method === "GET" && path2 === REMOTE_OAUTH_CALLBACK_PATH) {
11230
+ const url = new URL(reqUrl, origin);
11231
+ const state = url.searchParams.get("state")?.trim() ?? "";
11232
+ const code = url.searchParams.get("code")?.trim() ?? "";
11233
+ const providerError = url.searchParams.get("error")?.trim() ?? "";
11234
+ const pending = STATE_RE.test(state) ? opts.sessions.takeLogin(state) : null;
11235
+ if (!pending) {
11236
+ sendHtml(opts.res, 400, "Sideboard", "<h1>Sign-in expired</h1><p>Return to Sideboard and try again.</p>");
11237
+ return true;
11238
+ }
11239
+ if (providerError || !code) {
11240
+ const message = providerError || "Sign-in was cancelled.";
11241
+ opts.sessions.putResult(state, { ok: false, error: message });
11242
+ sendHtml(opts.res, 400, "Sideboard", `<h1>Could not sign in</h1><p>${escapeHtml(message)}</p>`);
11243
+ return true;
11244
+ }
11245
+ const client = opts.oauth[pending.provider];
11246
+ if (!client) {
11247
+ opts.sessions.putResult(state, { ok: false, error: "Sign-in is not configured on this relay." });
11248
+ sendHtml(opts.res, 503, "Sideboard", "<h1>Sign-in is not configured</h1>");
11249
+ return true;
11250
+ }
11251
+ try {
11252
+ const identity = await fetchRelayIdentity(pending.provider, client, code, redirectUri, fetchImpl);
11253
+ const signed = opts.store.signIn(identity, pending.accountId);
11254
+ opts.sessions.putResult(state, {
11255
+ ok: true,
11256
+ credential: signed.credential,
11257
+ email: signed.email,
11258
+ accountId: signed.accountId
11259
+ });
11260
+ sendHtml(
11261
+ opts.res,
11262
+ 200,
11263
+ "Sideboard",
11264
+ "<h1>Signed in</h1><p>Return to Sideboard. This window can close.</p>"
11265
+ );
11266
+ } catch (err) {
11267
+ const message = err instanceof Error ? err.message : String(err);
11268
+ opts.sessions.putResult(state, { ok: false, error: message });
11269
+ sendHtml(opts.res, 400, "Sideboard", `<h1>Could not sign in</h1><p>${escapeHtml(message)}</p>`);
11270
+ }
11271
+ return true;
11272
+ }
11273
+ if (method === "GET" && path2 === REMOTE_OAUTH_RESULT_PATH) {
11274
+ const url = new URL(reqUrl, origin);
11275
+ const state = url.searchParams.get("state")?.trim() ?? "";
11276
+ if (!STATE_RE.test(state)) {
11277
+ sendJson2(opts.res, 400, { ok: false, error: "invalid login request" });
11278
+ return true;
11279
+ }
11280
+ const result = opts.sessions.takeResult(state);
11281
+ if (result === void 0 || result === null) {
11282
+ sendJson2(opts.res, 404, { ok: false, error: "pending" });
11283
+ return true;
11284
+ }
11285
+ if (!result.ok) {
11286
+ sendJson2(opts.res, 400, { ok: false, error: result.error });
11287
+ return true;
11288
+ }
11289
+ sendJson2(opts.res, 200, {
11290
+ ok: true,
11291
+ credential: result.credential,
11292
+ email: result.email,
11293
+ accountId: result.accountId
11294
+ });
11295
+ return true;
11296
+ }
11297
+ if (method === "POST" && path2 === REMOTE_LOGOUT_PATH) {
11298
+ let body;
11299
+ try {
11300
+ body = await readBody(opts.req);
11301
+ } catch {
11302
+ sendJson2(opts.res, 400, { ok: false, error: "invalid logout request" });
11303
+ return true;
11304
+ }
11305
+ const token = typeof body.accountToken === "string" ? body.accountToken : "";
11306
+ opts.store.logout(token);
11307
+ sendJson2(opts.res, 200, { ok: true });
11308
+ return true;
11309
+ }
11310
+ return false;
11311
+ }
11312
+
11313
+ // src/relay/account-login.ts
11314
+ var RemoteAccountLoginCancelled = class extends Error {
11315
+ constructor() {
11316
+ super("Sign-in cancelled");
11317
+ this.name = "RemoteAccountLoginCancelled";
11318
+ }
11319
+ };
11320
+ function isRemoteAccountLoginCancelled(err) {
11321
+ return err instanceof RemoteAccountLoginCancelled;
11322
+ }
11323
+ function remoteAccountHttpOrigin(wsUrl = remoteRelayUrl()) {
11324
+ const url = new URL(wsUrl);
11325
+ url.protocol = url.protocol === "wss:" ? "https:" : "http:";
11326
+ url.pathname = "";
11327
+ url.search = "";
11328
+ url.hash = "";
11329
+ return url.origin;
11330
+ }
11331
+ function sleep2(ms, signal) {
11332
+ return new Promise((resolve, reject) => {
11333
+ if (signal?.aborted) {
11334
+ reject(new RemoteAccountLoginCancelled());
11335
+ return;
11336
+ }
11337
+ const timer = setTimeout(() => {
11338
+ signal?.removeEventListener("abort", onAbort);
11339
+ resolve();
11340
+ }, ms);
11341
+ const onAbort = () => {
11342
+ clearTimeout(timer);
11343
+ reject(new RemoteAccountLoginCancelled());
11344
+ };
11345
+ signal?.addEventListener("abort", onAbort, { once: true });
11346
+ });
11347
+ }
11348
+ async function startRemoteAccountLogin(opts) {
11349
+ if (opts.signal?.aborted) throw new RemoteAccountLoginCancelled();
11350
+ const origin = (opts.origin ?? remoteAccountHttpOrigin()).replace(/\/+$/, "");
11351
+ const fetchImpl = opts.fetchImpl ?? fetch;
11352
+ const state = randomBytes7(16).toString("hex");
11353
+ const timeoutMs = opts.timeoutMs ?? 5 * 6e4;
11354
+ const pollIntervalMs = opts.pollIntervalMs ?? 400;
11355
+ const linkToken = opts.linkToken?.trim() ?? "";
11356
+ const started = await fetchImpl(`${origin}${REMOTE_LOGIN_PATH}`, {
11357
+ method: "POST",
11358
+ headers: { "Content-Type": "application/json" },
11359
+ body: JSON.stringify({
11360
+ provider: opts.provider,
11361
+ state,
11362
+ ...linkToken ? { accountToken: linkToken } : {}
11363
+ })
11364
+ });
11365
+ const startBody = await started.json();
11366
+ if (!started.ok || !startBody.url) {
11367
+ throw new Error(startBody.error || "Could not start sign-in.");
11368
+ }
11369
+ await Promise.resolve(opts.openUrl?.(startBody.url));
11370
+ const deadline = Date.now() + timeoutMs;
11371
+ const resultUrl = `${origin}${REMOTE_OAUTH_RESULT_PATH}?state=${encodeURIComponent(state)}`;
11372
+ while (Date.now() < deadline) {
11373
+ if (opts.signal?.aborted) throw new RemoteAccountLoginCancelled();
11374
+ let res;
11375
+ try {
11376
+ res = await fetchImpl(resultUrl);
11377
+ } catch {
11378
+ if (opts.signal?.aborted) throw new RemoteAccountLoginCancelled();
11379
+ await sleep2(pollIntervalMs, opts.signal);
11380
+ continue;
11381
+ }
11382
+ let data;
11383
+ try {
11384
+ data = await res.json();
11385
+ } catch {
11386
+ await sleep2(pollIntervalMs, opts.signal);
11387
+ continue;
11388
+ }
11389
+ if (data.ok && data.credential && data.accountId) {
11390
+ return {
11391
+ credential: data.credential,
11392
+ email: data.email ?? null,
11393
+ accountId: data.accountId
11394
+ };
11395
+ }
11396
+ if (data.error && data.error !== "pending") throw new Error(data.error);
11397
+ await sleep2(pollIntervalMs, opts.signal);
11398
+ }
11399
+ throw new Error("Sign-in timed out.");
11400
+ }
11401
+ async function disconnectRemoteAccount(opts) {
11402
+ const origin = (opts.origin ?? remoteAccountHttpOrigin()).replace(/\/+$/, "");
11403
+ const fetchImpl = opts.fetchImpl ?? fetch;
11404
+ await fetchImpl(`${origin}${REMOTE_LOGOUT_PATH}`, {
11405
+ method: "POST",
11406
+ headers: { "Content-Type": "application/json" },
11407
+ body: JSON.stringify({ accountToken: opts.accountToken })
11408
+ });
11409
+ }
11410
+
9651
11411
  // src/slack/app-home.ts
9652
11412
  var SLACK_APP_HOME_DOWNLOAD = "https://sideboard-downloads.t3.tigrisfiles.io/Sideboard-latest-arm64.dmg";
9653
11413
  var SLACK_APP_HOME_LANDING = "https://www.sideboard.cloud/slack/";
@@ -10104,14 +11864,19 @@ async function tryServeStatic(req, res, root) {
10104
11864
  }
10105
11865
 
10106
11866
  // src/relay/server.ts
10107
- function sendHtml(res, status, title, body) {
11867
+ function sendHtml2(res, status, title, body) {
10108
11868
  res.writeHead(status, { "Content-Type": "text/html; charset=utf-8" });
10109
11869
  res.end(slackOAuthHtmlPage(title, body));
10110
11870
  }
10111
- function sendJson2(res, status, body) {
11871
+ function sendJson3(res, status, body) {
10112
11872
  res.writeHead(status, { "Content-Type": "application/json" });
10113
11873
  res.end(JSON.stringify(body));
10114
11874
  }
11875
+ function relayClientIp(req) {
11876
+ const fly = req.headers["fly-client-ip"];
11877
+ if (typeof fly === "string" && fly.trim()) return fly.trim();
11878
+ return req.socket.remoteAddress ?? "unknown";
11879
+ }
10115
11880
  function startRelaySocketPing(ws, intervalMs) {
10116
11881
  const ping = setInterval(() => {
10117
11882
  if (ws.readyState !== ws.OPEN) {
@@ -10139,11 +11904,26 @@ async function startRelayServer(opts) {
10139
11904
  const clientSecret = (opts.clientSecret ?? process.env.SIDEBOARD_SLACK_CLIENT_SECRET ?? "").trim();
10140
11905
  const redirectUri = (opts.oauthRedirectUri ?? slackOAuthRelayRedirectUri()).trim();
10141
11906
  const pending = new SlackOAuthPendingStore();
11907
+ let boundPort = 0;
10142
11908
  const hub = opts.hub ?? new SlackRelayHub({
10143
11909
  fetchImpl: opts.fetchImpl ?? fetch,
10144
11910
  onLog: log
10145
11911
  });
10146
- const remoteHub = new RemoteHub();
11912
+ const accountsPath = opts.accountsPath?.trim() ?? "";
11913
+ if (accountsPath) assertRelayAccountsDurable(accountsPath);
11914
+ const accountStore = accountsPath ? new RelayAccountStore(accountsPath) : null;
11915
+ const accountSessions = new RelayLoginSessions();
11916
+ const relayOAuth = opts.relayOAuth ?? {};
11917
+ const authMarks = /* @__PURE__ */ new Map();
11918
+ const remoteByIp = /* @__PURE__ */ new Map();
11919
+ let remoteSockets = 0;
11920
+ const remoteHub = new RemoteHub({
11921
+ authorizeHost: accountStore ? (input) => {
11922
+ const decision = accountStore.authorizeHost(input);
11923
+ return decision.ok ? { ok: true } : { ok: false, message: decision.message };
11924
+ } : void 0,
11925
+ onAuthorized: (socket) => authMarks.get(socket)?.()
11926
+ });
10147
11927
  const handleCallback = async (reqUrl, res) => {
10148
11928
  const url2 = parseSlackOAuthCallbackUrl(reqUrl);
10149
11929
  if (!url2) return false;
@@ -10152,7 +11932,7 @@ async function startRelayServer(opts) {
10152
11932
  const code = url2.searchParams.get("code")?.trim() ?? "";
10153
11933
  if (error) {
10154
11934
  if (state) pending.put(state, { ok: false, error: `Slack OAuth: ${error}` });
10155
- sendHtml(
11935
+ sendHtml2(
10156
11936
  res,
10157
11937
  400,
10158
11938
  "Slack",
@@ -10161,7 +11941,7 @@ async function startRelayServer(opts) {
10161
11941
  return true;
10162
11942
  }
10163
11943
  if (!state || !code) {
10164
- sendHtml(res, 400, "Slack", "<h1>Invalid callback</h1><p>State or code missing.</p>");
11944
+ sendHtml2(res, 400, "Slack", "<h1>Invalid callback</h1><p>State or code missing.</p>");
10165
11945
  return true;
10166
11946
  }
10167
11947
  if (!clientSecret) {
@@ -10169,7 +11949,7 @@ async function startRelayServer(opts) {
10169
11949
  ok: false,
10170
11950
  error: "Relay is missing SIDEBOARD_SLACK_CLIENT_SECRET"
10171
11951
  });
10172
- sendHtml(
11952
+ sendHtml2(
10173
11953
  res,
10174
11954
  500,
10175
11955
  "Slack",
@@ -10192,7 +11972,7 @@ async function startRelayServer(opts) {
10192
11972
  void hub.handleAppHomeOpened(payload.team_id, payload.user_id);
10193
11973
  }
10194
11974
  }
10195
- sendHtml(
11975
+ sendHtml2(
10196
11976
  res,
10197
11977
  200,
10198
11978
  "Slack connected",
@@ -10201,7 +11981,7 @@ async function startRelayServer(opts) {
10201
11981
  } catch (err) {
10202
11982
  const message = err instanceof Error ? err.message : String(err);
10203
11983
  pending.put(state, { ok: false, error: message });
10204
- sendHtml(res, 400, "Slack", `<h1>Could not connect Slack</h1><p>${escapeHtml(message)}</p>`);
11984
+ sendHtml2(res, 400, "Slack", `<h1>Could not connect Slack</h1><p>${escapeHtml(message)}</p>`);
10205
11985
  }
10206
11986
  return true;
10207
11987
  };
@@ -10211,19 +11991,30 @@ async function startRelayServer(opts) {
10211
11991
  const state = url2.searchParams.get("state")?.trim() ?? "";
10212
11992
  const value = pending.take(state);
10213
11993
  if (!value) {
10214
- sendJson2(res, 404, { ok: false, error: "pending" });
11994
+ sendJson3(res, 404, { ok: false, error: "pending" });
10215
11995
  return true;
10216
11996
  }
10217
11997
  if (!value.ok) {
10218
- sendJson2(res, 400, { ok: false, error: value.error });
11998
+ sendJson3(res, 400, { ok: false, error: value.error });
10219
11999
  return true;
10220
12000
  }
10221
- sendJson2(res, 200, { ok: true, ...value.payload });
12001
+ sendJson3(res, 200, { ok: true, ...value.payload });
10222
12002
  return true;
10223
12003
  };
10224
12004
  const httpServer = createServer3((req, res) => {
10225
12005
  void (async () => {
10226
12006
  const reqUrl = req.url || "/";
12007
+ if (accountStore && await handleRelayAccountHttp({
12008
+ req,
12009
+ res,
12010
+ store: accountStore,
12011
+ oauth: relayOAuth,
12012
+ sessions: accountSessions,
12013
+ publicOrigin: opts.relayPublicOrigin?.trim() || process.env.SIDEBOARD_RELAY_PUBLIC_ORIGIN?.trim() || `http://127.0.0.1:${boundPort}`,
12014
+ fetchImpl: opts.fetchImpl
12015
+ })) {
12016
+ return;
12017
+ }
10227
12018
  if (await handleCallback(reqUrl, res)) return;
10228
12019
  if (handleResult(reqUrl, res)) return;
10229
12020
  const ableTimeBounce = ableTimeOAuthLocalBounceUrl(reqUrl);
@@ -10237,7 +12028,7 @@ async function startRelayServer(opts) {
10237
12028
  return;
10238
12029
  }
10239
12030
  if (req.url === "/health") {
10240
- sendJson2(res, 200, {
12031
+ sendJson3(res, 200, {
10241
12032
  ok: true,
10242
12033
  service: "sideboard-relay",
10243
12034
  sessions: hub.listSessions().length,
@@ -10256,7 +12047,7 @@ async function startRelayServer(opts) {
10256
12047
  return;
10257
12048
  }
10258
12049
  if (req.url === "/") {
10259
- sendJson2(res, 200, {
12050
+ sendJson3(res, 200, {
10260
12051
  ok: true,
10261
12052
  service: "sideboard-relay",
10262
12053
  sessions: hub.listSessions().length,
@@ -10283,6 +12074,14 @@ async function startRelayServer(opts) {
10283
12074
  socket.destroy();
10284
12075
  return;
10285
12076
  }
12077
+ if (target === remoteWss) {
12078
+ const ip = relayClientIp(req);
12079
+ const count = remoteByIp.get(ip) ?? 0;
12080
+ if (count >= 20 || remoteSockets >= 4e3) {
12081
+ socket.destroy();
12082
+ return;
12083
+ }
12084
+ }
10286
12085
  target.handleUpgrade(req, socket, head, (ws) => {
10287
12086
  target.emit("connection", ws, req);
10288
12087
  });
@@ -10317,7 +12116,10 @@ async function startRelayServer(opts) {
10317
12116
  hub.detachSocket(socket);
10318
12117
  });
10319
12118
  });
10320
- remoteWss.on("connection", (ws) => {
12119
+ remoteWss.on("connection", (ws, req) => {
12120
+ const ip = relayClientIp(req);
12121
+ remoteByIp.set(ip, (remoteByIp.get(ip) ?? 0) + 1);
12122
+ remoteSockets += 1;
10321
12123
  const socket = {
10322
12124
  send: (data) => {
10323
12125
  if (ws.readyState === ws.OPEN) ws.send(data);
@@ -10329,14 +12131,35 @@ async function startRelayServer(opts) {
10329
12131
  }
10330
12132
  }
10331
12133
  };
12134
+ let released = false;
12135
+ const authTimer = accountStore ? setTimeout(() => {
12136
+ try {
12137
+ ws.close(4001, "sign in required");
12138
+ } catch {
12139
+ }
12140
+ }, 12e4) : null;
12141
+ authMarks.set(socket, () => {
12142
+ if (authTimer) clearTimeout(authTimer);
12143
+ });
12144
+ const release = () => {
12145
+ if (released) return;
12146
+ released = true;
12147
+ if (authTimer) clearTimeout(authTimer);
12148
+ authMarks.delete(socket);
12149
+ remoteSockets -= 1;
12150
+ const next = (remoteByIp.get(ip) ?? 1) - 1;
12151
+ if (next <= 0) remoteByIp.delete(ip);
12152
+ else remoteByIp.set(ip, next);
12153
+ remoteHub.detachSocket(socket);
12154
+ };
10332
12155
  startRelaySocketPing(ws, REMOTE_PING_INTERVAL_MS);
10333
12156
  log("remote client connected");
10334
12157
  ws.on("message", (data) => {
10335
12158
  const raw = typeof data === "string" ? data : data.toString("utf8");
10336
12159
  remoteHub.handleClientMessage(socket, raw);
10337
12160
  });
10338
- ws.on("close", () => remoteHub.detachSocket(socket));
10339
- ws.on("error", () => remoteHub.detachSocket(socket));
12161
+ ws.on("close", release);
12162
+ ws.on("error", release);
10340
12163
  });
10341
12164
  const port = opts.port ?? 0;
10342
12165
  const host = opts.host ?? "0.0.0.0";
@@ -10345,7 +12168,7 @@ async function startRelayServer(opts) {
10345
12168
  httpServer.listen(port, host, () => resolve());
10346
12169
  });
10347
12170
  const address = httpServer.address();
10348
- const boundPort = typeof address === "object" && address ? address.port : typeof port === "number" ? port : 0;
12171
+ boundPort = typeof address === "object" && address ? address.port : typeof port === "number" ? port : 0;
10349
12172
  const url = `ws://${host === "0.0.0.0" ? "127.0.0.1" : host}:${boundPort}${SLACK_RELAY_DESKTOP_PATH}`;
10350
12173
  log(`relay listening on ${url}`);
10351
12174
  const ac = new AbortController();
@@ -10388,13 +12211,14 @@ async function startRelayServer(opts) {
10388
12211
  httpServer.close((err) => err ? reject(err) : resolve());
10389
12212
  });
10390
12213
  await socketModeDone.catch(() => void 0);
12214
+ accountStore?.close();
10391
12215
  }
10392
12216
  };
10393
12217
  }
10394
12218
 
10395
12219
  // src/slack/outbound-watch.ts
10396
- import { existsSync as existsSync5, readFileSync as readFileSync5 } from "fs";
10397
- import { join as join6 } from "path";
12220
+ import { existsSync as existsSync5, readFileSync as readFileSync6 } from "fs";
12221
+ import { join as join7 } from "path";
10398
12222
  var MAX_WATCHES = 40;
10399
12223
  var MAX_REPLIES_PER_WATCH = 30;
10400
12224
  var WATCH_TTL_MS = 7 * 24 * 60 * 60 * 1e3;
@@ -10402,7 +12226,7 @@ var POLL_INTERVAL_MS = 12e3;
10402
12226
  var lastPollMs = 0;
10403
12227
  var nameCache = /* @__PURE__ */ new Map();
10404
12228
  function storePath3() {
10405
- return join6(appDataDir(), "slack-outbound-watch.json");
12229
+ return join7(appDataDir(), "slack-outbound-watch.json");
10406
12230
  }
10407
12231
  function watchId(teamId, channelId, ts) {
10408
12232
  return `${teamId}:${channelId}:${ts}`;
@@ -10417,7 +12241,7 @@ function readStore3() {
10417
12241
  const path2 = storePath3();
10418
12242
  if (!existsSync5(path2)) return [];
10419
12243
  try {
10420
- const parsed = isSecureFileEncrypted(path2) ? readSecureJson(path2) : JSON.parse(readFileSync5(path2, "utf8"));
12244
+ const parsed = isSecureFileEncrypted(path2) ? readSecureJson(path2) : JSON.parse(readFileSync6(path2, "utf8"));
10421
12245
  return Array.isArray(parsed?.watches) ? parsed.watches : [];
10422
12246
  } catch {
10423
12247
  return [];
@@ -10469,7 +12293,7 @@ async function continueSourceThread(threadId, prompt) {
10469
12293
  await continueOnReply(threadId, prompt);
10470
12294
  return;
10471
12295
  }
10472
- const { getOrchestrator: getOrchestrator2 } = await import("./orchestrator-QRSEOHXI.js");
12296
+ const { getOrchestrator: getOrchestrator2 } = await import("./orchestrator-QMKVXVUR.js");
10473
12297
  await getOrchestrator2().send(threadId, prompt);
10474
12298
  } catch {
10475
12299
  }
@@ -11061,6 +12885,7 @@ export {
11061
12885
  disconnectLinear,
11062
12886
  disconnectLinearConnection,
11063
12887
  disconnectOptionalService,
12888
+ disconnectRemoteAccount,
11064
12889
  disconnectSlackWorkspace,
11065
12890
  discoverSkills,
11066
12891
  downloadAbleTimeAttachment,
@@ -11317,6 +13142,7 @@ export {
11317
13142
  isPrNotMergeableError,
11318
13143
  isPresentPlanToolName,
11319
13144
  isPrimaryCheckoutThread,
13145
+ isRemoteAccountLoginCancelled,
11320
13146
  isRemoteStopCommand,
11321
13147
  isSelfWrittenRecord,
11322
13148
  isSessionQuotaLimit,
@@ -11678,6 +13504,7 @@ export {
11678
13504
  startMcpServer,
11679
13505
  startOrchestration,
11680
13506
  startRelayServer,
13507
+ startRemoteAccountLogin,
11681
13508
  startSlackOAuth,
11682
13509
  statWorktreePath,
11683
13510
  stopAgentRpcServer,