@sideboard-ai/core 0.1.274 → 0.1.275

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (48) hide show
  1. package/dist/{abletime-4ZSDI4YP.js → abletime-VCVPSLSU.js} +4 -4
  2. package/dist/{abletime-JNQAHLHH.js → abletime-VRATUMDO.js} +4 -4
  3. package/dist/{abletime-oauth-4RXZH2KU.js → abletime-oauth-4C4G2J3R.js} +3 -3
  4. package/dist/{abletime-oauth-DCEIINYT.js → abletime-oauth-4K7MICAD.js} +3 -3
  5. package/dist/{abletime-rest-KVBJFVFD.js → abletime-rest-6N63ZVQ3.js} +3 -3
  6. package/dist/{abletime-rest-VN6PZ6P2.js → abletime-rest-PIBCNHF3.js} +3 -3
  7. package/dist/{agents-LAFFXBBZ.js → agents-2FOWVJCN.js} +4 -4
  8. package/dist/{agents-ZCO37SKG.js → agents-JBVJRDH3.js} +4 -4
  9. package/dist/{app-settings-RAC4PSSP.js → app-settings-EQQZQNYD.js} +1 -1
  10. package/dist/{app-settings-L5HGYXWJ.js → app-settings-NI4X5HW3.js} +1 -1
  11. package/dist/{chunk-UXI52MLJ.js → chunk-2J34G3WJ.js} +3 -3
  12. package/dist/{chunk-PTNGMA5C.js → chunk-A2G3ZBJS.js} +2 -2
  13. package/dist/{chunk-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 +1695 -181
  37. package/dist/index.d.cts +99 -20
  38. package/dist/index.d.ts +99 -20
  39. package/dist/index.js +1583 -43
  40. package/dist/mcp/run-stdio.cjs +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,6 +9375,524 @@ var RemoteHub = class {
9354
9375
  // src/remote/client.ts
9355
9376
  import { WebSocket as WsWebSocket3 } from "ws";
9356
9377
 
9378
+ // src/remote/phone-chats.ts
9379
+ import { randomUUID as randomUUID7 } from "crypto";
9380
+ var PHONE_CONTROL_PREFIX = "\0sb.phone\n";
9381
+ var PHONE_USER_PREFIX = "Phone\n\n";
9382
+ var TRANSCRIPT_LIMIT = 40;
9383
+ var MESSAGE_LIMIT = 4e3;
9384
+ var PREVIEW_LIMIT = 90;
9385
+ var HISTORY_PAGE_SIZE = 40;
9386
+ var AGENT_KINDS = /* @__PURE__ */ new Set(["claude", "codex", "opencode", "brightsy", "cursor"]);
9387
+ var MAX_PHONE_FILES = 8;
9388
+ var MAX_PHONE_FILE_CHARS = Math.ceil(8 * 1024 * 1024 * 4 / 3);
9389
+ var MAX_PHONE_FILES_CHARS = Math.ceil(20 * 1024 * 1024 * 4 / 3);
9390
+ var MAX_DICTATION_CHARS = Math.ceil(12 * 1024 * 1024 * 4 / 3);
9391
+ function encodePhoneControl(payload) {
9392
+ return PHONE_CONTROL_PREFIX + JSON.stringify(payload);
9393
+ }
9394
+ function takePhoneControl(text6) {
9395
+ if (!text6.startsWith(PHONE_CONTROL_PREFIX)) return null;
9396
+ let parsed;
9397
+ try {
9398
+ parsed = JSON.parse(text6.slice(PHONE_CONTROL_PREFIX.length));
9399
+ } catch {
9400
+ return "invalid";
9401
+ }
9402
+ return parsePhoneControl(parsed);
9403
+ }
9404
+ function parsePhoneControl(value) {
9405
+ if (!value || typeof value !== "object") return "invalid";
9406
+ const op = value.op;
9407
+ if (op === "list") return { op: "list" };
9408
+ if (op === "history") {
9409
+ const query = textField(value, "query");
9410
+ const after = textField(value, "after");
9411
+ return { op: "history", ...query ? { query } : {}, ...after ? { after } : {} };
9412
+ }
9413
+ if (op === "sources") {
9414
+ const repoPath = textField(value, "repoPath");
9415
+ if (!repoPath || !isProjectPath(repoPath)) return "invalid";
9416
+ const query = textField(value, "query");
9417
+ return { op: "sources", repoPath, ...query ? { query } : {} };
9418
+ }
9419
+ if (op === "models") {
9420
+ const agent = agentField(value);
9421
+ if (!agent) return "invalid";
9422
+ return { op: "models", agent };
9423
+ }
9424
+ if (op === "dictate") {
9425
+ const id = textField(value, "id");
9426
+ const audioBase64 = base64Field(value, "audioBase64");
9427
+ if (!id || !audioBase64 || audioBase64.length > MAX_DICTATION_CHARS) return "invalid";
9428
+ return { op: "dictate", id, audioBase64 };
9429
+ }
9430
+ if (op === "create") return parsePhoneCreate(value);
9431
+ const chatId = typeof value.chatId === "string" ? value.chatId.trim() : "";
9432
+ if (!chatId) return "invalid";
9433
+ if (op === "open" || op === "archive" || op === "restore" || op === "stop") return { op, chatId };
9434
+ const draft = parsePhoneDraft(value);
9435
+ if (draft === "invalid") return "invalid";
9436
+ if (op === "options") return { op: "options", chatId, ...draft };
9437
+ if (op === "prompt") {
9438
+ const text6 = typeof value.text === "string" ? value.text.trim() : "";
9439
+ const hasPayload = Boolean(text6 || draft.files?.length || draft.links?.length);
9440
+ if (!hasPayload) return "invalid";
9441
+ return { op: "prompt", chatId, text: text6, ...draft };
9442
+ }
9443
+ return "invalid";
9444
+ }
9445
+ function textField(value, key) {
9446
+ const raw = value[key];
9447
+ return typeof raw === "string" ? raw.trim() : "";
9448
+ }
9449
+ function agentField(value) {
9450
+ const agent = textField(value, "agent");
9451
+ if (!agent) return void 0;
9452
+ return AGENT_KINDS.has(agent) ? agent : void 0;
9453
+ }
9454
+ function base64Field(value, key) {
9455
+ const raw = value[key];
9456
+ if (typeof raw !== "string") return "";
9457
+ return raw.replace(/\s/g, "");
9458
+ }
9459
+ function parsePhoneDraft(value) {
9460
+ const draft = {};
9461
+ if (textField(value, "agent")) {
9462
+ const agent = agentField(value);
9463
+ if (!agent) return "invalid";
9464
+ draft.agent = agent;
9465
+ }
9466
+ if ("model" in value) {
9467
+ const model = value.model;
9468
+ if (model === null || model === "") draft.model = null;
9469
+ else if (typeof model === "string") draft.model = model.trim().slice(0, 200);
9470
+ else return "invalid";
9471
+ }
9472
+ if ("effort" in value && value.effort != null) {
9473
+ const effort = value.effort;
9474
+ if (!isThinkingEffort(effort)) return "invalid";
9475
+ draft.effort = effort;
9476
+ }
9477
+ if ("fast" in value) {
9478
+ const fast = value.fast;
9479
+ if (typeof fast !== "boolean") return "invalid";
9480
+ draft.fast = fast;
9481
+ }
9482
+ if ("planMode" in value) {
9483
+ const planMode = value.planMode;
9484
+ if (typeof planMode !== "boolean") return "invalid";
9485
+ draft.planMode = planMode;
9486
+ }
9487
+ if ("autonomy" in value && value.autonomy != null) {
9488
+ const autonomy = value.autonomy;
9489
+ if (autonomy !== "default" && autonomy !== "full") return "invalid";
9490
+ draft.autonomy = autonomy;
9491
+ }
9492
+ const files = parsePhoneFiles(value.files);
9493
+ if (files === "invalid") return "invalid";
9494
+ if (files.length) draft.files = files;
9495
+ const links = parsePhoneLinks(value.links);
9496
+ if (links === "invalid") return "invalid";
9497
+ if (links.length) draft.links = links;
9498
+ const prompt = textField(value, "prompt");
9499
+ if (prompt) draft.prompt = prompt.slice(0, 2e4);
9500
+ return draft;
9501
+ }
9502
+ function parsePhoneFiles(value) {
9503
+ if (value == null) return [];
9504
+ if (!Array.isArray(value) || value.length > MAX_PHONE_FILES) return "invalid";
9505
+ const files = [];
9506
+ let total = 0;
9507
+ for (const item of value) {
9508
+ if (!item || typeof item !== "object") return "invalid";
9509
+ const name = (textField(item, "name").replace(/[/\\]/g, "_") || "file").slice(0, 180);
9510
+ const dataBase64 = base64Field(item, "dataBase64");
9511
+ if (!dataBase64 || !/^[A-Za-z0-9+/=]+$/.test(dataBase64)) return "invalid";
9512
+ if (dataBase64.length > MAX_PHONE_FILE_CHARS) return "invalid";
9513
+ total += dataBase64.length;
9514
+ if (total > MAX_PHONE_FILES_CHARS) return "invalid";
9515
+ files.push({ name, dataBase64 });
9516
+ }
9517
+ return files;
9518
+ }
9519
+ function parsePhoneLinks(value) {
9520
+ if (value == null) return [];
9521
+ if (!Array.isArray(value) || value.length > 8) return "invalid";
9522
+ const links = [];
9523
+ for (const item of value) {
9524
+ if (!item || typeof item !== "object") return "invalid";
9525
+ const ref = textField(item, "ref").slice(0, 80);
9526
+ if (!ref) return "invalid";
9527
+ const title = (textField(item, "title") || ref).slice(0, 200);
9528
+ const url = textField(item, "url").slice(0, 500);
9529
+ links.push({ ref, title, ...url ? { url } : {} });
9530
+ }
9531
+ return links;
9532
+ }
9533
+ function parsePhoneCreate(value) {
9534
+ const draft = parsePhoneDraft(value);
9535
+ if (draft === "invalid") return "invalid";
9536
+ const goal = textField(value, "goal") || draft.prompt || "";
9537
+ const where = value.where;
9538
+ if (where == null || where === "orchestration") {
9539
+ return {
9540
+ op: "create",
9541
+ where: "orchestration",
9542
+ ...draft,
9543
+ ...goal ? { goal, prompt: goal } : {}
9544
+ };
9545
+ }
9546
+ if (where === "worktree") {
9547
+ const chatId = textField(value, "chatId");
9548
+ if (!chatId) return "invalid";
9549
+ return { op: "create", where: "worktree", chatId, ...draft };
9550
+ }
9551
+ if (where === "project") {
9552
+ const repoPath = textField(value, "repoPath");
9553
+ if (!repoPath || !isProjectPath(repoPath)) return "invalid";
9554
+ const sourceRaw = textField(value, "sourceType");
9555
+ let sourceType;
9556
+ if (sourceRaw === "branch" || sourceRaw === "pr" || sourceRaw === "ticket") sourceType = sourceRaw;
9557
+ else if (sourceRaw) return "invalid";
9558
+ const sourceRef = textField(value, "sourceRef");
9559
+ if ((sourceType === "pr" || sourceType === "ticket") && !sourceRef) return "invalid";
9560
+ const title = textField(value, "title").slice(0, 200);
9561
+ const cowboy = value.cowboy === true;
9562
+ return {
9563
+ op: "create",
9564
+ where: "project",
9565
+ repoPath,
9566
+ ...draft,
9567
+ ...sourceType ? { sourceType } : {},
9568
+ ...sourceRef ? { sourceRef } : {},
9569
+ ...title ? { title } : {},
9570
+ ...cowboy ? { cowboy: true } : {}
9571
+ };
9572
+ }
9573
+ return "invalid";
9574
+ }
9575
+ function phoneVisibleText(role, text6) {
9576
+ const trimmed = text6.trim();
9577
+ if (role === "user" && trimmed.startsWith(PHONE_USER_PREFIX)) {
9578
+ return trimmed.slice(PHONE_USER_PREFIX.length).trim();
9579
+ }
9580
+ return trimmed;
9581
+ }
9582
+ function clip(text6, limit) {
9583
+ if (text6.length <= limit) return text6;
9584
+ return `${text6.slice(0, limit - 1)}\u2026`;
9585
+ }
9586
+ function summarize(thread) {
9587
+ return {
9588
+ id: thread.id,
9589
+ title: thread.title?.trim() || "Untitled",
9590
+ status: thread.status,
9591
+ preview: previewOf(thread),
9592
+ updatedAt: thread.updatedAt
9593
+ };
9594
+ }
9595
+ function previewOf(thread) {
9596
+ const messages = listPhoneMessages(thread);
9597
+ const last = messages[messages.length - 1];
9598
+ if (!last) return "";
9599
+ return clip(last.text.replace(/\s+/g, " ").trim(), PREVIEW_LIMIT);
9600
+ }
9601
+ function listPhoneMessages(thread) {
9602
+ const out = [];
9603
+ for (const message of thread.messages) {
9604
+ if (message.role !== "user" && message.role !== "agent") continue;
9605
+ const names = (message.attachments ?? []).map((item) => item.name.trim()).filter(Boolean);
9606
+ const text6 = clip(
9607
+ [phoneVisibleText(message.role, message.text), names.length ? names.join(", ") : ""].filter(Boolean).join("\n"),
9608
+ MESSAGE_LIMIT
9609
+ );
9610
+ if (!text6) continue;
9611
+ out.push({ role: message.role, text: text6 });
9612
+ }
9613
+ for (const prompt of thread.queue) {
9614
+ const text6 = clip(phoneVisibleText("user", prompt), MESSAGE_LIMIT);
9615
+ if (text6) out.push({ role: "user", text: text6 });
9616
+ }
9617
+ return out.slice(-TRANSCRIPT_LIMIT);
9618
+ }
9619
+ function repoName(repoPath) {
9620
+ const parts = repoPath.replace(/\/$/, "").split("/");
9621
+ return parts[parts.length - 1] || repoPath;
9622
+ }
9623
+ function isProjectPath(path2) {
9624
+ if (!path2 || path2 === "/" || path2 === ".") return false;
9625
+ return !isGlobalThread({ repoPath: path2 });
9626
+ }
9627
+ function listPhoneChats() {
9628
+ return listGlobalThreads().slice().sort((a, b) => a.updatedAt < b.updatedAt ? 1 : a.updatedAt > b.updatedAt ? -1 : 0).map(summarize);
9629
+ }
9630
+ function listPhoneSidebar() {
9631
+ const threads = listThreads();
9632
+ const byRepo = /* @__PURE__ */ new Map();
9633
+ for (const thread of threads) {
9634
+ if (!isProjectPath(thread.repoPath)) continue;
9635
+ const list = byRepo.get(thread.repoPath) ?? [];
9636
+ list.push(thread);
9637
+ byRepo.set(thread.repoPath, list);
9638
+ }
9639
+ for (const workspace of listWorkspaces()) {
9640
+ if (!isProjectPath(workspace.path) || byRepo.has(workspace.path)) continue;
9641
+ byRepo.set(workspace.path, []);
9642
+ }
9643
+ const projects = [...byRepo.entries()].sort(([a], [b]) => repoName(a).localeCompare(repoName(b))).map(([path2, repoThreads]) => ({
9644
+ name: repoName(path2),
9645
+ path: path2,
9646
+ worktrees: groupHomeBoardWorktrees(repoThreads).map((group) => ({
9647
+ label: worktreeDisplayLabelForGroup(group),
9648
+ chats: group.map(summarize)
9649
+ }))
9650
+ }));
9651
+ return { orchestration: listPhoneChats(), projects };
9652
+ }
9653
+ function historyStamp(thread) {
9654
+ return thread.archivedAt || thread.updatedAt;
9655
+ }
9656
+ function historyCursor(chat) {
9657
+ return `${chat.updatedAt} ${chat.id}`;
9658
+ }
9659
+ function historyQueryMatch(chat, query) {
9660
+ if (!query) return true;
9661
+ return `${chat.title} ${chat.where} ${chat.agent} ${chat.preview}`.toLowerCase().includes(query);
9662
+ }
9663
+ function listPhoneHistory(opts) {
9664
+ const query = opts?.query?.trim().toLowerCase() ?? "";
9665
+ const after = opts?.after?.trim() ?? "";
9666
+ 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) => ({
9667
+ id: thread.id,
9668
+ title: thread.title?.trim() || "Untitled",
9669
+ preview: previewOf(thread),
9670
+ updatedAt: historyStamp(thread),
9671
+ where: isGlobalThread(thread) ? "Orchestration" : repoName(thread.repoPath),
9672
+ agent: thread.agent
9673
+ })).filter((chat) => historyQueryMatch(chat, query));
9674
+ let start = 0;
9675
+ if (after) {
9676
+ const index = all.findIndex((chat) => historyCursor(chat) === after);
9677
+ start = index >= 0 ? index + 1 : all.length;
9678
+ }
9679
+ const chats = all.slice(start, start + HISTORY_PAGE_SIZE);
9680
+ const last = chats.at(-1);
9681
+ const more = start + chats.length < all.length;
9682
+ return {
9683
+ chats,
9684
+ total: all.length,
9685
+ ...more && last ? { next: historyCursor(last) } : {}
9686
+ };
9687
+ }
9688
+ function readPhoneThread(chatId) {
9689
+ const id = chatId.trim();
9690
+ if (!id) return null;
9691
+ const thread = readThread(id);
9692
+ if (!thread || thread.status === "archived") return null;
9693
+ if (isGlobalThread(thread)) return thread;
9694
+ if (!isProjectPath(thread.repoPath)) return null;
9695
+ return thread;
9696
+ }
9697
+ function phoneComposerOptions(thread) {
9698
+ return {
9699
+ agent: thread.agent,
9700
+ model: thread.model ?? null,
9701
+ effort: thread.effort,
9702
+ fast: Boolean(thread.fast),
9703
+ planMode: Boolean(thread.planMode),
9704
+ autonomy: thread.autonomy ?? "default"
9705
+ };
9706
+ }
9707
+ function phoneTurnAttachments(draft) {
9708
+ const links = (draft.links ?? []).map((link) => ({
9709
+ id: randomUUID7(),
9710
+ name: link.ref,
9711
+ kind: "issue",
9712
+ content: [`Linked issue: ${link.ref} \u2014 ${link.title}`, link.url ? `URL: ${link.url}` : null].filter(Boolean).join("\n")
9713
+ }));
9714
+ const files = (draft.files ?? []).map((file) => ({
9715
+ id: randomUUID7(),
9716
+ name: file.name,
9717
+ kind: "file",
9718
+ previewDataUrl: `data:application/octet-stream;base64,${file.dataBase64}`,
9719
+ content: `File attached: ${file.name}`
9720
+ }));
9721
+ return [...links, ...files];
9722
+ }
9723
+ function openedFrom(thread, place) {
9724
+ const fresh = readThread(thread.id) ?? thread;
9725
+ return {
9726
+ chat: summarize(fresh),
9727
+ messages: listPhoneMessages(fresh),
9728
+ place,
9729
+ options: phoneComposerOptions(fresh)
9730
+ };
9731
+ }
9732
+ function openPhoneChat(chatId) {
9733
+ const thread = readPhoneThread(chatId);
9734
+ if (!thread) return null;
9735
+ const place = isGlobalThread(thread) ? { kind: "orchestration" } : projectPlace(thread);
9736
+ return openedFrom(thread, place);
9737
+ }
9738
+ function sameRepoPath(a, b) {
9739
+ return a.replace(/\/+$/, "") === b.replace(/\/+$/, "");
9740
+ }
9741
+ function projectPlace(thread) {
9742
+ const group = threadsSharingWorktree(thread.worktreePath);
9743
+ return {
9744
+ kind: "project",
9745
+ repoPath: thread.repoPath,
9746
+ worktree: worktreeDisplayLabelForGroup(group.length > 0 ? group : [thread])
9747
+ };
9748
+ }
9749
+ function assertKnownProject(repoPath) {
9750
+ const path2 = repoPath.trim();
9751
+ if (!isProjectPath(path2)) throw new Error("Pick a project on this Mac.");
9752
+ const known = listWorkspaces().some((workspace) => sameRepoPath(workspace.path, path2)) || listThreads().some(
9753
+ (thread) => thread.status !== "archived" && sameRepoPath(thread.repoPath, path2)
9754
+ );
9755
+ if (!known) throw new Error("That project is not on this Mac.");
9756
+ return path2;
9757
+ }
9758
+ function draftAgentOptions(draft, defaults) {
9759
+ return {
9760
+ agent: draft?.agent ?? defaults.agent,
9761
+ model: draft && "model" in draft ? draft.model ?? null : defaults.model,
9762
+ effort: draft?.effort ?? defaults.effort,
9763
+ fast: draft?.fast ?? defaults.fast,
9764
+ planMode: Boolean(draft?.planMode),
9765
+ autonomy: draft?.autonomy
9766
+ };
9767
+ }
9768
+ function createPhoneChat(agent) {
9769
+ const defaults = resolveOrchestratorDefaults();
9770
+ const thread = createGlobalChat({
9771
+ agent: agent ?? defaults.agent,
9772
+ model: defaults.model,
9773
+ effort: defaults.effort,
9774
+ fast: defaults.fast
9775
+ });
9776
+ return openedFrom(thread, { kind: "orchestration" });
9777
+ }
9778
+ async function createPhoneOrchestration(draft) {
9779
+ const goal = (draft?.goal ?? draft?.prompt ?? "").trim();
9780
+ const attachments = phoneTurnAttachments(draft ?? {});
9781
+ if (!goal && attachments.length === 0) return createPhoneChat(draft?.agent);
9782
+ const defaults = resolveOrchestratorDefaults();
9783
+ const options = draftAgentOptions(draft, defaults);
9784
+ const thread = await startOrchestration({
9785
+ goal: goal || "See the attached files.",
9786
+ agent: options.agent,
9787
+ model: options.model,
9788
+ effort: options.effort,
9789
+ fast: options.fast,
9790
+ planMode: options.planMode,
9791
+ autonomy: options.autonomy,
9792
+ attachments
9793
+ });
9794
+ return openedFrom(thread, { kind: "orchestration" });
9795
+ }
9796
+ function createPhoneWorktreeAgent(fromChatId, draft) {
9797
+ const source = readPhoneThread(fromChatId);
9798
+ if (!source || isGlobalThread(source)) throw new Error("That worktree is not on this Mac.");
9799
+ const defaults = resolveNewThreadOptions();
9800
+ const options = draftAgentOptions(draft, defaults);
9801
+ const attachments = persistPendingFileAttachments(
9802
+ source.worktreePath,
9803
+ phoneTurnAttachments(draft ?? {})
9804
+ );
9805
+ const thread = createChatTab({
9806
+ fromThreadId: source.id,
9807
+ agent: options.agent,
9808
+ model: options.model,
9809
+ effort: options.effort,
9810
+ fast: options.fast,
9811
+ autonomy: options.autonomy,
9812
+ attachments
9813
+ });
9814
+ if (draft?.planMode !== void 0) updateThread(thread.id, { planMode: draft.planMode });
9815
+ return openedFrom(thread, projectPlace(readThread(thread.id) ?? thread));
9816
+ }
9817
+ async function createPhoneProjectWorktree(repoPathOrInput) {
9818
+ const input = typeof repoPathOrInput === "string" ? { repoPath: repoPathOrInput } : repoPathOrInput;
9819
+ const path2 = assertKnownProject(input.repoPath);
9820
+ const defaults = resolveNewThreadOptions();
9821
+ const options = draftAgentOptions(input, defaults);
9822
+ const thread = await createThread({
9823
+ sourceType: input.sourceType ?? "branch",
9824
+ sourceRef: input.sourceRef || "default",
9825
+ repoPath: path2,
9826
+ title: input.title,
9827
+ cowboy: input.cowboy,
9828
+ agent: options.agent,
9829
+ model: options.model,
9830
+ effort: options.effort,
9831
+ fast: options.fast,
9832
+ planMode: options.planMode,
9833
+ autonomy: options.autonomy,
9834
+ attachments: phoneTurnAttachments(input),
9835
+ prompt: input.prompt,
9836
+ reuseExisting: false
9837
+ });
9838
+ return openedFrom(thread, projectPlace(thread));
9839
+ }
9840
+ function sourceWarning(label, err) {
9841
+ const message = err instanceof Error ? err.message : String(err);
9842
+ return message ? `${label}: ${message}` : `${label} failed.`;
9843
+ }
9844
+ async function listPhoneSources(repoPath, query) {
9845
+ const path2 = assertKnownProject(repoPath);
9846
+ const q = query?.trim() ?? "";
9847
+ const warnings = [];
9848
+ const [prs, branches, issues] = await Promise.all([
9849
+ listPrs(path2, { state: "open", limit: 40, ...q ? { query: q } : {} }).catch((err) => {
9850
+ warnings.push(sourceWarning("Pull requests", err));
9851
+ return [];
9852
+ }),
9853
+ listBranches(path2).catch((err) => {
9854
+ warnings.push(sourceWarning("Branches", err));
9855
+ return [];
9856
+ }),
9857
+ listIssues(path2, { limit: 40, ...q ? { query: q } : {} }).catch((err) => {
9858
+ warnings.push(sourceWarning("Issues", err));
9859
+ return { issues: [] };
9860
+ })
9861
+ ]);
9862
+ const needle = q.toLowerCase();
9863
+ const branchRows = branches.filter((branch) => !needle || branch.name.toLowerCase().includes(needle)).slice(0, 50).map((branch) => ({
9864
+ ref: branch.name,
9865
+ title: branch.current ? `${branch.name} (current)` : branch.name
9866
+ }));
9867
+ return {
9868
+ prs: prs.slice(0, 40).map((pr) => ({
9869
+ ref: String(pr.number),
9870
+ title: clip(pr.title || `PR #${pr.number}`, 140),
9871
+ ...pr.url ? { url: pr.url } : {}
9872
+ })),
9873
+ branches: branchRows,
9874
+ issues: (issues.issues ?? []).slice(0, 40).map((issue) => ({
9875
+ ref: issue.identifier,
9876
+ title: clip(issue.title || issue.identifier, 140),
9877
+ ...issue.url ? { url: issue.url } : {}
9878
+ })),
9879
+ ...warnings.length ? { warnings } : {}
9880
+ };
9881
+ }
9882
+ async function listPhoneModels(agent) {
9883
+ const catalogs = await listModelsForAgent(agent);
9884
+ return (catalogs[0]?.models ?? []).slice(0, 40).map((model) => ({
9885
+ id: model.id,
9886
+ label: model.displayName || model.id
9887
+ }));
9888
+ }
9889
+ function archivePhoneChat(chatId) {
9890
+ const thread = readPhoneThread(chatId);
9891
+ if (!thread) return false;
9892
+ updateThread(thread.id, { status: "archived" });
9893
+ return true;
9894
+ }
9895
+
9357
9896
  // src/remote/listen.ts
9358
9897
  var REMOTE_STOPPED_REPLY = "Sideboard stopped the in-progress turn.";
9359
9898
  function formatRemotePrompt(text6) {
@@ -9366,6 +9905,8 @@ function isRemoteStopCommand(text6) {
9366
9905
  }
9367
9906
  var handleChain2 = Promise.resolve();
9368
9907
  var inboundGeneration = 0;
9908
+ var chatChains = /* @__PURE__ */ new Map();
9909
+ var chatGeneration = /* @__PURE__ */ new Map();
9369
9910
  function enqueue(fn) {
9370
9911
  const run2 = handleChain2.then(fn, fn);
9371
9912
  handleChain2 = run2.then(
@@ -9387,10 +9928,340 @@ function interruptRemoteCoordinator(deviceId, log = () => void 0) {
9387
9928
  return false;
9388
9929
  }
9389
9930
  }
9931
+ function bumpChat(chatId) {
9932
+ const next = (chatGeneration.get(chatId) ?? 0) + 1;
9933
+ chatGeneration.set(chatId, next);
9934
+ return next;
9935
+ }
9936
+ function enqueueChat(chatId, fn) {
9937
+ const prev = chatChains.get(chatId) ?? Promise.resolve();
9938
+ const run2 = prev.then(fn, fn);
9939
+ chatChains.set(
9940
+ chatId,
9941
+ run2.then(
9942
+ () => void 0,
9943
+ () => void 0
9944
+ )
9945
+ );
9946
+ }
9947
+ function emitControl(opts, reply) {
9948
+ opts.onOutbound({ type: "assistant", text: encodePhoneControl(reply) });
9949
+ }
9950
+ function interruptChat(chatId, log) {
9951
+ try {
9952
+ const thread = readPhoneThread(chatId);
9953
+ if (!thread) return false;
9954
+ if (thread.status !== "running" && thread.status !== "queued") return false;
9955
+ getOrchestrator().stop(thread.id, { clearQueue: true });
9956
+ log(`interrupt phone \u2192 chat ${thread.id.slice(0, 8)} (${thread.status})`);
9957
+ return true;
9958
+ } catch (err) {
9959
+ log(`interrupt chat: ${err instanceof Error ? err.message : String(err)}`);
9960
+ return false;
9961
+ }
9962
+ }
9963
+ function emitChatTurn(opts, chatId, threadId) {
9964
+ const thread = readThread(threadId);
9965
+ if (!thread) {
9966
+ emitControl(opts, { op: "error", chatId, message: "That agent is not on this Mac." });
9967
+ return;
9968
+ }
9969
+ if (thread.status === "stopped") {
9970
+ emitControl(opts, { op: "stopped", chatId });
9971
+ return;
9972
+ }
9973
+ const result = getOrchestrator().getTurnResult(thread.id);
9974
+ const pending = extractPendingPlanQuestions(
9975
+ [...thread.messages].reverse().find((m) => m.role === "agent")?.parts
9976
+ );
9977
+ if (result.taskState === "input-required" && pending) {
9978
+ emitControl(opts, {
9979
+ op: "ask",
9980
+ chatId,
9981
+ text: result.text.trim(),
9982
+ questions: pending.questions.map((q) => ({
9983
+ question: q.question,
9984
+ options: q.options.map(
9985
+ (o) => o.description ? { label: o.label, description: o.description } : { label: o.label }
9986
+ )
9987
+ }))
9988
+ });
9989
+ return;
9990
+ }
9991
+ const reply = outboundReplyFromTurn(result, {
9992
+ inputRequired: "Sideboard is waiting for an answer.",
9993
+ canceled: REMOTE_STOPPED_REPLY,
9994
+ completedEmpty: () => "",
9995
+ failed: (detail) => detail ? `Sideboard failed: ${detail}` : "Sideboard failed before producing a result."
9996
+ });
9997
+ emitControl(opts, { op: "assistant", chatId, text: reply.trim() });
9998
+ }
9999
+ function phoneError(err) {
10000
+ return err instanceof Error ? err.message : String(err);
10001
+ }
10002
+ function deliverPhoneTurn(opts, chatId) {
10003
+ const thread = readPhoneThread(chatId);
10004
+ if (!thread || thread.status !== "running" && thread.status !== "queued") return;
10005
+ const generation = bumpChat(chatId);
10006
+ enqueueChat(chatId, async () => {
10007
+ if (chatGeneration.get(chatId) !== generation) return;
10008
+ try {
10009
+ await getOrchestrator().waitForTurn(chatId, 14 * 60 * 1e3);
10010
+ } catch (err) {
10011
+ if (chatGeneration.get(chatId) !== generation) return;
10012
+ emitControl(opts, { op: "error", chatId, message: `Sideboard failed: ${phoneError(err)}` });
10013
+ return;
10014
+ }
10015
+ if (chatGeneration.get(chatId) !== generation) return;
10016
+ emitChatTurn(opts, chatId, chatId);
10017
+ });
10018
+ }
10019
+ function applyPhoneOptions(chatId, draft) {
10020
+ const patch = {};
10021
+ if (draft.agent) patch.agent = draft.agent;
10022
+ if ("model" in draft) patch.model = draft.model ?? null;
10023
+ if (draft.effort) patch.effort = draft.effort;
10024
+ if (draft.fast !== void 0) patch.fast = draft.fast;
10025
+ if (draft.planMode !== void 0) patch.planMode = draft.planMode;
10026
+ if (draft.autonomy) patch.autonomy = draft.autonomy;
10027
+ if (Object.keys(patch).length === 0) return;
10028
+ getOrchestrator().setThreadOptions(chatId, patch);
10029
+ }
10030
+ function stagePhoneTurnFiles(worktreePath, draft) {
10031
+ const links = phoneTurnAttachments({ links: draft.links });
10032
+ const files = stageBuffersAsAttachments(
10033
+ worktreePath,
10034
+ (draft.files ?? []).map((file) => ({ name: file.name, dataBase64: file.dataBase64 }))
10035
+ );
10036
+ return [...links, ...files];
10037
+ }
10038
+ function handlePhoneControl(cmd, opts) {
10039
+ const log = opts.onLog ?? (() => void 0);
10040
+ if (cmd.op === "list") {
10041
+ emitControl(opts, { op: "sidebar", ...listPhoneSidebar() });
10042
+ return;
10043
+ }
10044
+ if (cmd.op === "history") {
10045
+ const page = listPhoneHistory({ query: cmd.query, after: cmd.after });
10046
+ emitControl(opts, {
10047
+ op: "history",
10048
+ chats: page.chats,
10049
+ total: page.total,
10050
+ ...page.next ? { next: page.next } : {},
10051
+ ...cmd.query ? { query: cmd.query } : {},
10052
+ ...cmd.after ? { after: cmd.after } : {}
10053
+ });
10054
+ return;
10055
+ }
10056
+ if (cmd.op === "sources") {
10057
+ const repoPath = cmd.repoPath;
10058
+ const query = cmd.query;
10059
+ void listPhoneSources(repoPath, query).then(
10060
+ (page) => emitControl(opts, {
10061
+ op: "sources",
10062
+ repoPath,
10063
+ ...query ? { query } : {},
10064
+ ...page
10065
+ })
10066
+ ).catch((err) => {
10067
+ emitControl(opts, { op: "error", message: phoneError(err) });
10068
+ });
10069
+ return;
10070
+ }
10071
+ if (cmd.op === "models") {
10072
+ const agent = cmd.agent;
10073
+ void listPhoneModels(agent).then((models) => emitControl(opts, { op: "models", agent, models })).catch((err) => {
10074
+ emitControl(opts, { op: "error", message: phoneError(err) });
10075
+ });
10076
+ return;
10077
+ }
10078
+ if (cmd.op === "dictate") {
10079
+ const id = cmd.id;
10080
+ const transcribe = opts.transcribeWav;
10081
+ if (!transcribe) {
10082
+ emitControl(opts, { op: "error", message: "Speech is not available on this Mac." });
10083
+ return;
10084
+ }
10085
+ void transcribe(cmd.audioBase64).then((text6) => emitControl(opts, { op: "dictated", id, text: text6.trim() })).catch((err) => {
10086
+ emitControl(opts, { op: "error", message: phoneError(err) });
10087
+ });
10088
+ return;
10089
+ }
10090
+ if (cmd.op === "options") {
10091
+ const thread = readPhoneThread(cmd.chatId);
10092
+ if (!thread) {
10093
+ emitControl(opts, { op: "error", chatId: cmd.chatId, message: "That agent is not on this Mac." });
10094
+ return;
10095
+ }
10096
+ try {
10097
+ applyPhoneOptions(thread.id, cmd);
10098
+ const fresh = readPhoneThread(thread.id) ?? thread;
10099
+ emitControl(opts, { op: "options", chatId: fresh.id, ...phoneComposerOptions(fresh) });
10100
+ } catch (err) {
10101
+ emitControl(opts, { op: "error", chatId: cmd.chatId, message: phoneError(err) });
10102
+ }
10103
+ return;
10104
+ }
10105
+ if (cmd.op === "create") {
10106
+ if (cmd.where === "worktree") {
10107
+ void (async () => {
10108
+ try {
10109
+ const opened = createPhoneWorktreeAgent(cmd.chatId, cmd);
10110
+ const prompt = cmd.prompt?.trim() || (cmd.files?.length || cmd.links?.length ? "See the attached files." : "");
10111
+ if (prompt) {
10112
+ try {
10113
+ await getOrchestrator().send(opened.chat.id, formatRemotePrompt(prompt));
10114
+ } catch (err) {
10115
+ emitControl(opts, { op: "error", chatId: opened.chat.id, message: phoneError(err) });
10116
+ }
10117
+ }
10118
+ const fresh = openPhoneChat(opened.chat.id) ?? opened;
10119
+ emitControl(opts, { op: "opened", ...fresh });
10120
+ if (prompt) deliverPhoneTurn(opts, fresh.chat.id);
10121
+ } catch (err) {
10122
+ emitControl(opts, { op: "error", message: phoneError(err) });
10123
+ }
10124
+ })();
10125
+ return;
10126
+ }
10127
+ if (cmd.where === "project") {
10128
+ void createPhoneProjectWorktree(cmd).then(async (opened) => {
10129
+ const prompt = resolveCreateFirstPrompt({
10130
+ sourceType: cmd.sourceType ?? "branch",
10131
+ prompt: cmd.prompt
10132
+ }) || (cmd.files?.length || cmd.links?.length ? "See the attached files." : "");
10133
+ if (prompt) {
10134
+ try {
10135
+ await getOrchestrator().send(opened.chat.id, formatRemotePrompt(prompt));
10136
+ } catch (err) {
10137
+ emitControl(opts, { op: "error", chatId: opened.chat.id, message: phoneError(err) });
10138
+ }
10139
+ }
10140
+ const fresh = openPhoneChat(opened.chat.id) ?? opened;
10141
+ emitControl(opts, { op: "opened", ...fresh });
10142
+ if (prompt) deliverPhoneTurn(opts, fresh.chat.id);
10143
+ }).catch((err) => {
10144
+ emitControl(opts, { op: "error", message: phoneError(err) });
10145
+ });
10146
+ return;
10147
+ }
10148
+ const goal = cmd.goal?.trim() || cmd.prompt?.trim() || "";
10149
+ if (goal || cmd.files?.length || cmd.links?.length) {
10150
+ void createPhoneOrchestration({ ...cmd, goal }).then((opened) => {
10151
+ emitControl(opts, { op: "opened", ...opened });
10152
+ deliverPhoneTurn(opts, opened.chat.id);
10153
+ }).catch((err) => {
10154
+ emitControl(opts, { op: "error", message: phoneError(err) });
10155
+ });
10156
+ return;
10157
+ }
10158
+ try {
10159
+ const agent = coerceOrchestratorAgent(
10160
+ cmd.agent ?? opts.agent ?? resolveOrchestratorDefaults().agent
10161
+ );
10162
+ emitControl(opts, { op: "opened", ...createPhoneChat(agent) });
10163
+ } catch (err) {
10164
+ emitControl(opts, { op: "error", message: phoneError(err) });
10165
+ }
10166
+ return;
10167
+ }
10168
+ if (cmd.op === "open") {
10169
+ const opened = openPhoneChat(cmd.chatId);
10170
+ if (!opened) {
10171
+ emitControl(opts, { op: "error", message: "That agent is not on this Mac." });
10172
+ return;
10173
+ }
10174
+ emitControl(opts, { op: "opened", ...opened });
10175
+ return;
10176
+ }
10177
+ if (cmd.op === "archive") {
10178
+ const thread = readPhoneThread(cmd.chatId);
10179
+ if (!thread) {
10180
+ emitControl(opts, { op: "error", message: "That agent is not on this Mac." });
10181
+ return;
10182
+ }
10183
+ bumpChat(thread.id);
10184
+ interruptChat(thread.id, log);
10185
+ archivePhoneChat(thread.id);
10186
+ emitControl(opts, { op: "sidebar", ...listPhoneSidebar() });
10187
+ return;
10188
+ }
10189
+ if (cmd.op === "restore") {
10190
+ const chatId2 = cmd.chatId;
10191
+ void getOrchestrator().restore(chatId2).then(() => {
10192
+ emitControl(opts, { op: "sidebar", ...listPhoneSidebar() });
10193
+ emitControl(opts, { op: "restored", chatId: chatId2 });
10194
+ }).catch((err) => {
10195
+ emitControl(opts, {
10196
+ op: "error",
10197
+ chatId: chatId2,
10198
+ message: err instanceof Error ? err.message : String(err)
10199
+ });
10200
+ });
10201
+ return;
10202
+ }
10203
+ if (cmd.op === "stop") {
10204
+ if (!readPhoneThread(cmd.chatId)) {
10205
+ emitControl(opts, { op: "error", chatId: cmd.chatId, message: "That agent is not on this Mac." });
10206
+ return;
10207
+ }
10208
+ bumpChat(cmd.chatId);
10209
+ interruptChat(cmd.chatId, log);
10210
+ emitControl(opts, { op: "stopped", chatId: cmd.chatId });
10211
+ return;
10212
+ }
10213
+ const chatId = cmd.chatId;
10214
+ const generation = bumpChat(chatId);
10215
+ interruptChat(chatId, log);
10216
+ enqueueChat(chatId, async () => {
10217
+ if (chatGeneration.get(chatId) !== generation) return;
10218
+ const thread = readPhoneThread(chatId);
10219
+ if (!thread) {
10220
+ emitControl(opts, { op: "error", chatId, message: "That agent is not on this Mac." });
10221
+ return;
10222
+ }
10223
+ if (thread.status === "running" || thread.status === "queued") {
10224
+ try {
10225
+ getOrchestrator().stop(thread.id, { clearQueue: true });
10226
+ } catch {
10227
+ }
10228
+ }
10229
+ if (chatGeneration.get(chatId) !== generation) return;
10230
+ try {
10231
+ try {
10232
+ applyPhoneOptions(thread.id, cmd);
10233
+ } catch (err) {
10234
+ emitControl(opts, { op: "error", chatId, message: phoneError(err) });
10235
+ }
10236
+ const live = readPhoneThread(thread.id) ?? thread;
10237
+ const staged = stagePhoneTurnFiles(live.worktreePath, cmd);
10238
+ const text6 = cmd.text.trim() || (staged.length ? "See the attached files." : "");
10239
+ const formatted = formatRemotePrompt(text6);
10240
+ await getOrchestrator().send(thread.id, formatted, { attachments: staged });
10241
+ await getOrchestrator().waitForTurn(thread.id, 14 * 60 * 1e3);
10242
+ } catch (err) {
10243
+ if (chatGeneration.get(chatId) !== generation) return;
10244
+ const message = err instanceof Error ? err.message : String(err);
10245
+ emitControl(opts, { op: "error", chatId, message: `Sideboard failed: ${message}` });
10246
+ return;
10247
+ }
10248
+ if (chatGeneration.get(chatId) !== generation) return;
10249
+ emitChatTurn(opts, chatId, thread.id);
10250
+ });
10251
+ }
9390
10252
  function handleRemoteInbound(text6, opts) {
9391
- const generation = ++inboundGeneration;
9392
10253
  const log = opts.onLog ?? (() => void 0);
9393
10254
  const body = text6.trim();
10255
+ const control = takePhoneControl(body);
10256
+ if (control) {
10257
+ if (control === "invalid") {
10258
+ emitControl(opts, { op: "error", message: "invalid phone command" });
10259
+ return;
10260
+ }
10261
+ handlePhoneControl(control, opts);
10262
+ return;
10263
+ }
10264
+ const generation = ++inboundGeneration;
9394
10265
  if (body) interruptRemoteCoordinator(opts.deviceId, log);
9395
10266
  enqueue(async () => {
9396
10267
  if (generation !== inboundGeneration) return;
@@ -9505,7 +10376,8 @@ function runRemoteHost(opts) {
9505
10376
  type: "host_register",
9506
10377
  deviceId: opts.deviceId,
9507
10378
  deviceLabel,
9508
- hostSecret: opts.hostSecret
10379
+ hostSecret: opts.hostSecret,
10380
+ ...opts.accountToken?.trim() ? { accountToken: opts.accountToken.trim() } : {}
9509
10381
  });
9510
10382
  };
9511
10383
  const loop = async () => {
@@ -9621,7 +10493,8 @@ function runRemoteHost(opts) {
9621
10493
  handleRemoteInbound(msg.text, {
9622
10494
  deviceId: opts.deviceId,
9623
10495
  onLog: log,
9624
- onOutbound: emit
10496
+ onOutbound: emit,
10497
+ transcribeWav: opts.transcribeWav
9625
10498
  });
9626
10499
  }
9627
10500
  };
@@ -9648,6 +10521,606 @@ function runRemoteHost(opts) {
9648
10521
  };
9649
10522
  }
9650
10523
 
10524
+ // src/relay/account-login.ts
10525
+ import { randomBytes as randomBytes7 } from "crypto";
10526
+
10527
+ // src/relay/accounts.ts
10528
+ import { createHash as createHash2, randomBytes as randomBytes6, randomUUID as randomUUID8, timingSafeEqual as timingSafeEqual2 } from "crypto";
10529
+ import { mkdirSync as mkdirSync4, readFileSync as readFileSync5, realpathSync } from "fs";
10530
+ import { createRequire } from "module";
10531
+ import { dirname, join as join6 } from "path";
10532
+ var GITHUB_ACCOUNT_ISSUER = "https://github.com";
10533
+ var RELAY_DEVICE_CAP = 3;
10534
+ function thisModuleFile() {
10535
+ const cjsFile = typeof __filename !== "undefined" ? __filename : "";
10536
+ return cjsFile || process.argv[1] || join6(process.cwd(), "package.json");
10537
+ }
10538
+ function openDatabase(filePath) {
10539
+ const req = createRequire(thisModuleFile());
10540
+ const specifier = "node:sqlite";
10541
+ const { DatabaseSync: SqliteDatabase } = req(specifier);
10542
+ const db = new SqliteDatabase(filePath);
10543
+ db.exec("PRAGMA journal_mode = WAL");
10544
+ return db;
10545
+ }
10546
+ function sha256(value) {
10547
+ return createHash2("sha256").update(value).digest("hex");
10548
+ }
10549
+ function sameHash(a, b) {
10550
+ const left = Buffer.from(a);
10551
+ const right = Buffer.from(b);
10552
+ return left.length === right.length && timingSafeEqual2(left, right);
10553
+ }
10554
+ function normalizeEmail(email) {
10555
+ const trimmed = email?.trim().toLowerCase() ?? "";
10556
+ return trimmed || null;
10557
+ }
10558
+ function longestMountPoint(dir, mounts) {
10559
+ let best = "/";
10560
+ for (const line of mounts.split("\n")) {
10561
+ const point = line.split(" ")[1]?.replace(/\\040/g, " ");
10562
+ if (!point) continue;
10563
+ if (dir === point || dir.startsWith(point.endsWith("/") ? point : `${point}/`)) {
10564
+ if (point.length > best.length) best = point;
10565
+ }
10566
+ }
10567
+ return best;
10568
+ }
10569
+ function assertRelayAccountsDurable(filePath) {
10570
+ if (process.env.SIDEBOARD_RELAY_ACCOUNTS_EPHEMERAL === "1") return;
10571
+ if (process.env.NODE_ENV !== "production") return;
10572
+ if (process.platform !== "linux") return;
10573
+ const dir = realpathSync(dirname(filePath));
10574
+ const mounts = readFileSync5("/proc/mounts", "utf8");
10575
+ const point = longestMountPoint(dir, mounts);
10576
+ if (point === "/") {
10577
+ throw new Error(
10578
+ `Relay accounts at ${filePath} are on the machine root filesystem. Mount a Fly volume so sign-ins survive deploys.`
10579
+ );
10580
+ }
10581
+ }
10582
+ var RelayAccountStore = class {
10583
+ db;
10584
+ constructor(filePath) {
10585
+ mkdirSync4(dirname(filePath), { recursive: true });
10586
+ this.db = openDatabase(filePath);
10587
+ this.db.exec(`
10588
+ CREATE TABLE IF NOT EXISTS accounts (
10589
+ id TEXT PRIMARY KEY,
10590
+ created_at INTEGER NOT NULL,
10591
+ device_cap INTEGER NOT NULL
10592
+ );
10593
+ CREATE TABLE IF NOT EXISTS identities (
10594
+ issuer TEXT NOT NULL,
10595
+ subject TEXT NOT NULL,
10596
+ account_id TEXT NOT NULL,
10597
+ email TEXT,
10598
+ email_verified INTEGER NOT NULL,
10599
+ PRIMARY KEY (issuer, subject)
10600
+ );
10601
+ CREATE INDEX IF NOT EXISTS identities_email ON identities(email);
10602
+ CREATE TABLE IF NOT EXISTS credentials (
10603
+ token_hash TEXT PRIMARY KEY,
10604
+ account_id TEXT NOT NULL,
10605
+ created_at INTEGER NOT NULL
10606
+ );
10607
+ CREATE TABLE IF NOT EXISTS devices (
10608
+ device_id TEXT PRIMARY KEY,
10609
+ account_id TEXT NOT NULL,
10610
+ host_secret_hash TEXT NOT NULL,
10611
+ device_label TEXT NOT NULL,
10612
+ created_at INTEGER NOT NULL
10613
+ );
10614
+ `);
10615
+ }
10616
+ close() {
10617
+ this.db.close();
10618
+ }
10619
+ /**
10620
+ * First sign-in creates an account. The same issuer+subject returns that
10621
+ * account. A verified email match attaches a second provider. `linkAccountId`
10622
+ * attaches while that account’s credential is already signed in.
10623
+ * Each call mints a new Mac credential. Older credentials stay valid.
10624
+ */
10625
+ signIn(identity, linkAccountId) {
10626
+ const issuer = identity.issuer.trim();
10627
+ const subject = identity.subject.trim();
10628
+ if (!issuer || !subject) throw new Error("Sign-in is missing an identity.");
10629
+ const email = normalizeEmail(identity.email);
10630
+ const verified = Boolean(identity.emailVerified && email);
10631
+ const link = linkAccountId?.trim() || null;
10632
+ this.db.exec("BEGIN");
10633
+ try {
10634
+ const existing = this.db.prepare("SELECT account_id, email FROM identities WHERE issuer = ? AND subject = ?").get(issuer, subject);
10635
+ let accountId;
10636
+ if (existing) {
10637
+ if (link && link !== existing.account_id) {
10638
+ throw new Error("That sign-in already belongs to another Sideboard account.");
10639
+ }
10640
+ accountId = existing.account_id;
10641
+ this.requireAccount(accountId);
10642
+ if (email) {
10643
+ this.db.prepare("UPDATE identities SET email = ?, email_verified = ? WHERE issuer = ? AND subject = ?").run(email, verified ? 1 : 0, issuer, subject);
10644
+ }
10645
+ } else if (link) {
10646
+ this.requireAccount(link);
10647
+ accountId = link;
10648
+ this.insertIdentity(issuer, subject, accountId, email, verified);
10649
+ } else {
10650
+ const byEmail = verified ? this.accountIdForVerifiedEmail(email) : void 0;
10651
+ accountId = byEmail ?? randomUUID8();
10652
+ if (!byEmail) {
10653
+ this.db.prepare("INSERT INTO accounts (id, created_at, device_cap) VALUES (?, ?, ?)").run(accountId, Date.now(), RELAY_DEVICE_CAP);
10654
+ }
10655
+ this.insertIdentity(issuer, subject, accountId, email, verified);
10656
+ }
10657
+ const credential = this.mintCredential(accountId);
10658
+ 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);
10659
+ this.db.exec("COMMIT");
10660
+ return { accountId, credential, email: stored?.email ?? email };
10661
+ } catch (err) {
10662
+ this.db.exec("ROLLBACK");
10663
+ throw err;
10664
+ }
10665
+ }
10666
+ verifyCredential(token) {
10667
+ const trimmed = token.trim();
10668
+ if (!trimmed) return null;
10669
+ const row = this.db.prepare("SELECT account_id FROM credentials WHERE token_hash = ?").get(sha256(trimmed));
10670
+ return row?.account_id ?? null;
10671
+ }
10672
+ /** Drop one Mac credential. The account and its other Macs stay. */
10673
+ logout(token) {
10674
+ const trimmed = token.trim();
10675
+ if (!trimmed) return;
10676
+ this.db.prepare("DELETE FROM credentials WHERE token_hash = ?").run(sha256(trimmed));
10677
+ }
10678
+ /** Delete the account and every identity, credential, and Mac bound to it. */
10679
+ revokeAccount(accountId) {
10680
+ const id = accountId.trim();
10681
+ if (!id) return;
10682
+ this.db.exec("BEGIN");
10683
+ try {
10684
+ this.db.prepare("DELETE FROM devices WHERE account_id = ?").run(id);
10685
+ this.db.prepare("DELETE FROM credentials WHERE account_id = ?").run(id);
10686
+ this.db.prepare("DELETE FROM identities WHERE account_id = ?").run(id);
10687
+ this.db.prepare("DELETE FROM accounts WHERE id = ?").run(id);
10688
+ this.db.exec("COMMIT");
10689
+ } catch (err) {
10690
+ this.db.exec("ROLLBACK");
10691
+ throw err;
10692
+ }
10693
+ }
10694
+ /**
10695
+ * Accept `host_register` for a signed-in account. The device row survives
10696
+ * process restart, so a later register with a different host secret is rejected
10697
+ * and a new device id counts against the cap.
10698
+ */
10699
+ authorizeHost(input) {
10700
+ const deviceId = input.deviceId.trim();
10701
+ const hostSecret = input.hostSecret.trim();
10702
+ const deviceLabel = input.deviceLabel.trim() || "This Mac";
10703
+ if (!deviceId || !hostSecret) return { ok: false, message: "sign in required" };
10704
+ const accountId = this.verifyCredential(input.accountToken);
10705
+ if (!accountId) return { ok: false, message: "sign in required" };
10706
+ this.db.exec("BEGIN");
10707
+ try {
10708
+ const account = this.db.prepare("SELECT id, device_cap FROM accounts WHERE id = ?").get(accountId);
10709
+ if (!account) {
10710
+ this.db.exec("ROLLBACK");
10711
+ return { ok: false, message: "sign in required" };
10712
+ }
10713
+ const device = this.db.prepare("SELECT device_id, account_id, host_secret_hash FROM devices WHERE device_id = ?").get(deviceId);
10714
+ const secretHash = sha256(hostSecret);
10715
+ if (!device) {
10716
+ const count = this.db.prepare("SELECT COUNT(*) AS n FROM devices WHERE account_id = ?").get(accountId);
10717
+ if (count.n >= account.device_cap) {
10718
+ this.db.exec("ROLLBACK");
10719
+ return { ok: false, message: "This account already has the maximum number of Macs." };
10720
+ }
10721
+ this.db.prepare(
10722
+ "INSERT INTO devices (device_id, account_id, host_secret_hash, device_label, created_at) VALUES (?, ?, ?, ?, ?)"
10723
+ ).run(deviceId, accountId, secretHash, deviceLabel, Date.now());
10724
+ this.db.exec("COMMIT");
10725
+ return { ok: true, accountId };
10726
+ }
10727
+ if (device.account_id !== accountId) {
10728
+ this.db.exec("ROLLBACK");
10729
+ return { ok: false, message: "This Mac is registered to another account." };
10730
+ }
10731
+ if (!sameHash(device.host_secret_hash, secretHash)) {
10732
+ this.db.exec("ROLLBACK");
10733
+ return { ok: false, message: "host secret rejected" };
10734
+ }
10735
+ this.db.prepare("UPDATE devices SET device_label = ? WHERE device_id = ?").run(deviceLabel, deviceId);
10736
+ this.db.exec("COMMIT");
10737
+ return { ok: true, accountId };
10738
+ } catch (err) {
10739
+ this.db.exec("ROLLBACK");
10740
+ throw err;
10741
+ }
10742
+ }
10743
+ requireAccount(accountId) {
10744
+ const row = this.db.prepare("SELECT id FROM accounts WHERE id = ?").get(accountId);
10745
+ if (!row) throw new Error("That Sideboard account is no longer on the relay.");
10746
+ }
10747
+ accountIdForVerifiedEmail(email) {
10748
+ const row = this.db.prepare(
10749
+ "SELECT account_id FROM identities WHERE email_verified = 1 AND email = ? LIMIT 1"
10750
+ ).get(email);
10751
+ return row?.account_id;
10752
+ }
10753
+ insertIdentity(issuer, subject, accountId, email, verified) {
10754
+ this.db.prepare(
10755
+ "INSERT INTO identities (issuer, subject, account_id, email, email_verified) VALUES (?, ?, ?, ?, ?)"
10756
+ ).run(issuer, subject, accountId, email, verified ? 1 : 0);
10757
+ }
10758
+ mintCredential(accountId) {
10759
+ const credential = randomBytes6(32).toString("hex");
10760
+ this.db.prepare("INSERT INTO credentials (token_hash, account_id, created_at) VALUES (?, ?, ?)").run(sha256(credential), accountId, Date.now());
10761
+ return credential;
10762
+ }
10763
+ };
10764
+
10765
+ // src/relay/account-oauth.ts
10766
+ var RELAY_GIT_HOSTS = ["github"];
10767
+ var REMOTE_LOGIN_PATH = "/remote/login";
10768
+ var REMOTE_OAUTH_CALLBACK_PATH = "/remote/oauth/callback";
10769
+ var REMOTE_OAUTH_RESULT_PATH = "/remote/oauth/result";
10770
+ var REMOTE_LOGOUT_PATH = "/remote/logout";
10771
+ var STATE_RE = /^[A-Za-z0-9_-]{16,128}$/;
10772
+ var LOGIN_TTL_MS = 10 * 60 * 1e3;
10773
+ var RelayLoginSessions = class {
10774
+ waiting = /* @__PURE__ */ new Map();
10775
+ results = /* @__PURE__ */ new Map();
10776
+ begin(state, provider, accountId) {
10777
+ this.sweep();
10778
+ this.waiting.set(state, { provider, accountId, expiresAt: Date.now() + LOGIN_TTL_MS });
10779
+ }
10780
+ takeLogin(state) {
10781
+ this.sweep();
10782
+ const row = this.waiting.get(state);
10783
+ if (!row) return null;
10784
+ this.waiting.delete(state);
10785
+ if (row.expiresAt < Date.now()) return null;
10786
+ return row;
10787
+ }
10788
+ putResult(state, value) {
10789
+ this.results.set(state, { value, expiresAt: Date.now() + LOGIN_TTL_MS });
10790
+ }
10791
+ /** `undefined` means the browser flow has not finished. */
10792
+ takeResult(state) {
10793
+ this.sweep();
10794
+ const row = this.results.get(state);
10795
+ if (!row) return this.waiting.has(state) ? void 0 : null;
10796
+ this.results.delete(state);
10797
+ if (row.expiresAt < Date.now()) return null;
10798
+ return row.value;
10799
+ }
10800
+ sweep() {
10801
+ const now = Date.now();
10802
+ for (const [state, row] of this.waiting) {
10803
+ if (row.expiresAt < now) this.waiting.delete(state);
10804
+ }
10805
+ for (const [state, row] of this.results) {
10806
+ if (row.expiresAt < now) this.results.delete(state);
10807
+ }
10808
+ }
10809
+ };
10810
+ var GIT_HOST_AUTH = {
10811
+ github: {
10812
+ label: "GitHub",
10813
+ authorizeUrl: "https://github.com/login/oauth/authorize",
10814
+ scope: "user:email",
10815
+ fetchIdentity: fetchGithubIdentity
10816
+ }
10817
+ };
10818
+ function providerOf(value) {
10819
+ return RELAY_GIT_HOSTS.includes(value) ? value : null;
10820
+ }
10821
+ function relayAuthorizeUrl(provider, client, redirectUri, state) {
10822
+ const host = GIT_HOST_AUTH[provider];
10823
+ const url = new URL(host.authorizeUrl);
10824
+ url.searchParams.set("client_id", client.clientId);
10825
+ url.searchParams.set("redirect_uri", redirectUri);
10826
+ url.searchParams.set("scope", host.scope);
10827
+ url.searchParams.set("state", state);
10828
+ return url.toString();
10829
+ }
10830
+ async function readJson(res) {
10831
+ const text6 = await res.text();
10832
+ if (!text6) return {};
10833
+ try {
10834
+ return JSON.parse(text6);
10835
+ } catch {
10836
+ throw new Error("Sign-in provider returned a non-JSON response.");
10837
+ }
10838
+ }
10839
+ function accessToken(body) {
10840
+ const token = body.access_token;
10841
+ if (typeof token !== "string" || !token.trim()) {
10842
+ const message = body.error_description;
10843
+ throw new Error(typeof message === "string" && message.trim() ? message : "Sign-in provider rejected the code.");
10844
+ }
10845
+ return token.trim();
10846
+ }
10847
+ async function fetchGithubIdentity(client, code, redirectUri, fetchImpl) {
10848
+ const tokenRes = await fetchImpl("https://github.com/login/oauth/access_token", {
10849
+ method: "POST",
10850
+ headers: { Accept: "application/json", "Content-Type": "application/json" },
10851
+ body: JSON.stringify({
10852
+ client_id: client.clientId,
10853
+ client_secret: client.clientSecret,
10854
+ code,
10855
+ redirect_uri: redirectUri
10856
+ })
10857
+ });
10858
+ const token = accessToken(await readJson(tokenRes));
10859
+ const userRes = await fetchImpl("https://api.github.com/user", {
10860
+ headers: { Authorization: `Bearer ${token}`, Accept: "application/vnd.github+json", "User-Agent": "sideboard-relay" }
10861
+ });
10862
+ const user = await readJson(userRes);
10863
+ const subject = typeof user.id === "number" ? String(user.id) : "";
10864
+ if (!subject) throw new Error("GitHub did not return an account id.");
10865
+ const emailRes = await fetchImpl("https://api.github.com/user/emails", {
10866
+ headers: { Authorization: `Bearer ${token}`, Accept: "application/vnd.github+json", "User-Agent": "sideboard-relay" }
10867
+ });
10868
+ const emails = await readJson(emailRes);
10869
+ const list = Array.isArray(emails) ? emails : [];
10870
+ const primary = list.find((row) => row.primary === true && row.verified === true);
10871
+ const anyVerified = list.find((row) => row.verified === true);
10872
+ const chosen = primary ?? anyVerified;
10873
+ const email = chosen && typeof chosen.email === "string" ? chosen.email : null;
10874
+ return { issuer: GITHUB_ACCOUNT_ISSUER, subject, email, emailVerified: Boolean(email) };
10875
+ }
10876
+ async function fetchRelayIdentity(provider, client, code, redirectUri, fetchImpl) {
10877
+ return GIT_HOST_AUTH[provider].fetchIdentity(client, code, redirectUri, fetchImpl);
10878
+ }
10879
+ async function readBody(req) {
10880
+ const chunks = [];
10881
+ let size = 0;
10882
+ for await (const chunk of req) {
10883
+ const buf = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
10884
+ size += buf.length;
10885
+ if (size > 8192) throw new Error("body too large");
10886
+ chunks.push(buf);
10887
+ }
10888
+ const text6 = Buffer.concat(chunks).toString("utf8").trim();
10889
+ if (!text6) return {};
10890
+ return JSON.parse(text6);
10891
+ }
10892
+ function sendJson2(res, status, body) {
10893
+ res.writeHead(status, { "Content-Type": "application/json", "Cache-Control": "no-store" });
10894
+ res.end(JSON.stringify(body));
10895
+ }
10896
+ function sendHtml(res, status, title, body) {
10897
+ res.writeHead(status, { "Content-Type": "text/html; charset=utf-8", "Cache-Control": "no-store" });
10898
+ res.end(`<!DOCTYPE html><html><head><meta charset="utf-8"><title>${escapeHtml(title)}</title></head><body>${body}</body></html>`);
10899
+ }
10900
+ async function handleRelayAccountHttp(opts) {
10901
+ const reqUrl = opts.req.url || "/";
10902
+ const path2 = reqUrl.split("?")[0];
10903
+ const method = opts.req.method || "GET";
10904
+ const origin = opts.publicOrigin.replace(/\/+$/, "");
10905
+ const redirectUri = `${origin}${REMOTE_OAUTH_CALLBACK_PATH}`;
10906
+ const fetchImpl = opts.fetchImpl ?? fetch;
10907
+ if (method === "POST" && path2 === REMOTE_LOGIN_PATH) {
10908
+ let body;
10909
+ try {
10910
+ body = await readBody(opts.req);
10911
+ } catch {
10912
+ sendJson2(opts.res, 400, { ok: false, error: "invalid login request" });
10913
+ return true;
10914
+ }
10915
+ const provider = providerOf(typeof body.provider === "string" ? body.provider : "");
10916
+ const state = typeof body.state === "string" ? body.state.trim() : "";
10917
+ if (!provider || !STATE_RE.test(state)) {
10918
+ sendJson2(opts.res, 400, { ok: false, error: "invalid login request" });
10919
+ return true;
10920
+ }
10921
+ const client = opts.oauth[provider];
10922
+ if (!client?.clientId || !client.clientSecret) {
10923
+ sendJson2(opts.res, 503, {
10924
+ ok: false,
10925
+ error: `${GIT_HOST_AUTH[provider].label} sign-in is not configured on this relay.`
10926
+ });
10927
+ return true;
10928
+ }
10929
+ let accountId = null;
10930
+ const linkToken = typeof body.accountToken === "string" ? body.accountToken.trim() : "";
10931
+ if (linkToken) {
10932
+ accountId = opts.store.verifyCredential(linkToken);
10933
+ if (!accountId) {
10934
+ sendJson2(opts.res, 401, { ok: false, error: "sign in required" });
10935
+ return true;
10936
+ }
10937
+ }
10938
+ opts.sessions.begin(state, provider, accountId);
10939
+ sendJson2(opts.res, 200, { ok: true, url: relayAuthorizeUrl(provider, client, redirectUri, state) });
10940
+ return true;
10941
+ }
10942
+ if (method === "GET" && path2 === REMOTE_OAUTH_CALLBACK_PATH) {
10943
+ const url = new URL(reqUrl, origin);
10944
+ const state = url.searchParams.get("state")?.trim() ?? "";
10945
+ const code = url.searchParams.get("code")?.trim() ?? "";
10946
+ const providerError = url.searchParams.get("error")?.trim() ?? "";
10947
+ const pending = STATE_RE.test(state) ? opts.sessions.takeLogin(state) : null;
10948
+ if (!pending) {
10949
+ sendHtml(opts.res, 400, "Sideboard", "<h1>Sign-in expired</h1><p>Return to Sideboard and try again.</p>");
10950
+ return true;
10951
+ }
10952
+ if (providerError || !code) {
10953
+ const message = providerError || "Sign-in was cancelled.";
10954
+ opts.sessions.putResult(state, { ok: false, error: message });
10955
+ sendHtml(opts.res, 400, "Sideboard", `<h1>Could not sign in</h1><p>${escapeHtml(message)}</p>`);
10956
+ return true;
10957
+ }
10958
+ const client = opts.oauth[pending.provider];
10959
+ if (!client) {
10960
+ opts.sessions.putResult(state, { ok: false, error: "Sign-in is not configured on this relay." });
10961
+ sendHtml(opts.res, 503, "Sideboard", "<h1>Sign-in is not configured</h1>");
10962
+ return true;
10963
+ }
10964
+ try {
10965
+ const identity = await fetchRelayIdentity(pending.provider, client, code, redirectUri, fetchImpl);
10966
+ const signed = opts.store.signIn(identity, pending.accountId);
10967
+ opts.sessions.putResult(state, {
10968
+ ok: true,
10969
+ credential: signed.credential,
10970
+ email: signed.email,
10971
+ accountId: signed.accountId
10972
+ });
10973
+ sendHtml(
10974
+ opts.res,
10975
+ 200,
10976
+ "Sideboard",
10977
+ "<h1>Signed in</h1><p>Return to Sideboard. This window can close.</p>"
10978
+ );
10979
+ } catch (err) {
10980
+ const message = err instanceof Error ? err.message : String(err);
10981
+ opts.sessions.putResult(state, { ok: false, error: message });
10982
+ sendHtml(opts.res, 400, "Sideboard", `<h1>Could not sign in</h1><p>${escapeHtml(message)}</p>`);
10983
+ }
10984
+ return true;
10985
+ }
10986
+ if (method === "GET" && path2 === REMOTE_OAUTH_RESULT_PATH) {
10987
+ const url = new URL(reqUrl, origin);
10988
+ const state = url.searchParams.get("state")?.trim() ?? "";
10989
+ if (!STATE_RE.test(state)) {
10990
+ sendJson2(opts.res, 400, { ok: false, error: "invalid login request" });
10991
+ return true;
10992
+ }
10993
+ const result = opts.sessions.takeResult(state);
10994
+ if (result === void 0 || result === null) {
10995
+ sendJson2(opts.res, 404, { ok: false, error: "pending" });
10996
+ return true;
10997
+ }
10998
+ if (!result.ok) {
10999
+ sendJson2(opts.res, 400, { ok: false, error: result.error });
11000
+ return true;
11001
+ }
11002
+ sendJson2(opts.res, 200, {
11003
+ ok: true,
11004
+ credential: result.credential,
11005
+ email: result.email,
11006
+ accountId: result.accountId
11007
+ });
11008
+ return true;
11009
+ }
11010
+ if (method === "POST" && path2 === REMOTE_LOGOUT_PATH) {
11011
+ let body;
11012
+ try {
11013
+ body = await readBody(opts.req);
11014
+ } catch {
11015
+ sendJson2(opts.res, 400, { ok: false, error: "invalid logout request" });
11016
+ return true;
11017
+ }
11018
+ const token = typeof body.accountToken === "string" ? body.accountToken : "";
11019
+ opts.store.logout(token);
11020
+ sendJson2(opts.res, 200, { ok: true });
11021
+ return true;
11022
+ }
11023
+ return false;
11024
+ }
11025
+
11026
+ // src/relay/account-login.ts
11027
+ var RemoteAccountLoginCancelled = class extends Error {
11028
+ constructor() {
11029
+ super("Sign-in cancelled");
11030
+ this.name = "RemoteAccountLoginCancelled";
11031
+ }
11032
+ };
11033
+ function isRemoteAccountLoginCancelled(err) {
11034
+ return err instanceof RemoteAccountLoginCancelled;
11035
+ }
11036
+ function remoteAccountHttpOrigin(wsUrl = remoteRelayUrl()) {
11037
+ const url = new URL(wsUrl);
11038
+ url.protocol = url.protocol === "wss:" ? "https:" : "http:";
11039
+ url.pathname = "";
11040
+ url.search = "";
11041
+ url.hash = "";
11042
+ return url.origin;
11043
+ }
11044
+ function sleep2(ms, signal) {
11045
+ return new Promise((resolve, reject) => {
11046
+ if (signal?.aborted) {
11047
+ reject(new RemoteAccountLoginCancelled());
11048
+ return;
11049
+ }
11050
+ const timer = setTimeout(() => {
11051
+ signal?.removeEventListener("abort", onAbort);
11052
+ resolve();
11053
+ }, ms);
11054
+ const onAbort = () => {
11055
+ clearTimeout(timer);
11056
+ reject(new RemoteAccountLoginCancelled());
11057
+ };
11058
+ signal?.addEventListener("abort", onAbort, { once: true });
11059
+ });
11060
+ }
11061
+ async function startRemoteAccountLogin(opts) {
11062
+ if (opts.signal?.aborted) throw new RemoteAccountLoginCancelled();
11063
+ const origin = (opts.origin ?? remoteAccountHttpOrigin()).replace(/\/+$/, "");
11064
+ const fetchImpl = opts.fetchImpl ?? fetch;
11065
+ const state = randomBytes7(16).toString("hex");
11066
+ const timeoutMs = opts.timeoutMs ?? 5 * 6e4;
11067
+ const pollIntervalMs = opts.pollIntervalMs ?? 400;
11068
+ const linkToken = opts.linkToken?.trim() ?? "";
11069
+ const started = await fetchImpl(`${origin}${REMOTE_LOGIN_PATH}`, {
11070
+ method: "POST",
11071
+ headers: { "Content-Type": "application/json" },
11072
+ body: JSON.stringify({
11073
+ provider: opts.provider,
11074
+ state,
11075
+ ...linkToken ? { accountToken: linkToken } : {}
11076
+ })
11077
+ });
11078
+ const startBody = await started.json();
11079
+ if (!started.ok || !startBody.url) {
11080
+ throw new Error(startBody.error || "Could not start sign-in.");
11081
+ }
11082
+ await Promise.resolve(opts.openUrl?.(startBody.url));
11083
+ const deadline = Date.now() + timeoutMs;
11084
+ const resultUrl = `${origin}${REMOTE_OAUTH_RESULT_PATH}?state=${encodeURIComponent(state)}`;
11085
+ while (Date.now() < deadline) {
11086
+ if (opts.signal?.aborted) throw new RemoteAccountLoginCancelled();
11087
+ let res;
11088
+ try {
11089
+ res = await fetchImpl(resultUrl);
11090
+ } catch {
11091
+ if (opts.signal?.aborted) throw new RemoteAccountLoginCancelled();
11092
+ await sleep2(pollIntervalMs, opts.signal);
11093
+ continue;
11094
+ }
11095
+ let data;
11096
+ try {
11097
+ data = await res.json();
11098
+ } catch {
11099
+ await sleep2(pollIntervalMs, opts.signal);
11100
+ continue;
11101
+ }
11102
+ if (data.ok && data.credential && data.accountId) {
11103
+ return {
11104
+ credential: data.credential,
11105
+ email: data.email ?? null,
11106
+ accountId: data.accountId
11107
+ };
11108
+ }
11109
+ if (data.error && data.error !== "pending") throw new Error(data.error);
11110
+ await sleep2(pollIntervalMs, opts.signal);
11111
+ }
11112
+ throw new Error("Sign-in timed out.");
11113
+ }
11114
+ async function disconnectRemoteAccount(opts) {
11115
+ const origin = (opts.origin ?? remoteAccountHttpOrigin()).replace(/\/+$/, "");
11116
+ const fetchImpl = opts.fetchImpl ?? fetch;
11117
+ await fetchImpl(`${origin}${REMOTE_LOGOUT_PATH}`, {
11118
+ method: "POST",
11119
+ headers: { "Content-Type": "application/json" },
11120
+ body: JSON.stringify({ accountToken: opts.accountToken })
11121
+ });
11122
+ }
11123
+
9651
11124
  // src/slack/app-home.ts
9652
11125
  var SLACK_APP_HOME_DOWNLOAD = "https://sideboard-downloads.t3.tigrisfiles.io/Sideboard-latest-arm64.dmg";
9653
11126
  var SLACK_APP_HOME_LANDING = "https://www.sideboard.cloud/slack/";
@@ -10104,14 +11577,19 @@ async function tryServeStatic(req, res, root) {
10104
11577
  }
10105
11578
 
10106
11579
  // src/relay/server.ts
10107
- function sendHtml(res, status, title, body) {
11580
+ function sendHtml2(res, status, title, body) {
10108
11581
  res.writeHead(status, { "Content-Type": "text/html; charset=utf-8" });
10109
11582
  res.end(slackOAuthHtmlPage(title, body));
10110
11583
  }
10111
- function sendJson2(res, status, body) {
11584
+ function sendJson3(res, status, body) {
10112
11585
  res.writeHead(status, { "Content-Type": "application/json" });
10113
11586
  res.end(JSON.stringify(body));
10114
11587
  }
11588
+ function relayClientIp(req) {
11589
+ const fly = req.headers["fly-client-ip"];
11590
+ if (typeof fly === "string" && fly.trim()) return fly.trim();
11591
+ return req.socket.remoteAddress ?? "unknown";
11592
+ }
10115
11593
  function startRelaySocketPing(ws, intervalMs) {
10116
11594
  const ping = setInterval(() => {
10117
11595
  if (ws.readyState !== ws.OPEN) {
@@ -10139,11 +11617,26 @@ async function startRelayServer(opts) {
10139
11617
  const clientSecret = (opts.clientSecret ?? process.env.SIDEBOARD_SLACK_CLIENT_SECRET ?? "").trim();
10140
11618
  const redirectUri = (opts.oauthRedirectUri ?? slackOAuthRelayRedirectUri()).trim();
10141
11619
  const pending = new SlackOAuthPendingStore();
11620
+ let boundPort = 0;
10142
11621
  const hub = opts.hub ?? new SlackRelayHub({
10143
11622
  fetchImpl: opts.fetchImpl ?? fetch,
10144
11623
  onLog: log
10145
11624
  });
10146
- const remoteHub = new RemoteHub();
11625
+ const accountsPath = opts.accountsPath?.trim() ?? "";
11626
+ if (accountsPath) assertRelayAccountsDurable(accountsPath);
11627
+ const accountStore = accountsPath ? new RelayAccountStore(accountsPath) : null;
11628
+ const accountSessions = new RelayLoginSessions();
11629
+ const relayOAuth = opts.relayOAuth ?? {};
11630
+ const authMarks = /* @__PURE__ */ new Map();
11631
+ const remoteByIp = /* @__PURE__ */ new Map();
11632
+ let remoteSockets = 0;
11633
+ const remoteHub = new RemoteHub({
11634
+ authorizeHost: accountStore ? (input) => {
11635
+ const decision = accountStore.authorizeHost(input);
11636
+ return decision.ok ? { ok: true } : { ok: false, message: decision.message };
11637
+ } : void 0,
11638
+ onAuthorized: (socket) => authMarks.get(socket)?.()
11639
+ });
10147
11640
  const handleCallback = async (reqUrl, res) => {
10148
11641
  const url2 = parseSlackOAuthCallbackUrl(reqUrl);
10149
11642
  if (!url2) return false;
@@ -10152,7 +11645,7 @@ async function startRelayServer(opts) {
10152
11645
  const code = url2.searchParams.get("code")?.trim() ?? "";
10153
11646
  if (error) {
10154
11647
  if (state) pending.put(state, { ok: false, error: `Slack OAuth: ${error}` });
10155
- sendHtml(
11648
+ sendHtml2(
10156
11649
  res,
10157
11650
  400,
10158
11651
  "Slack",
@@ -10161,7 +11654,7 @@ async function startRelayServer(opts) {
10161
11654
  return true;
10162
11655
  }
10163
11656
  if (!state || !code) {
10164
- sendHtml(res, 400, "Slack", "<h1>Invalid callback</h1><p>State or code missing.</p>");
11657
+ sendHtml2(res, 400, "Slack", "<h1>Invalid callback</h1><p>State or code missing.</p>");
10165
11658
  return true;
10166
11659
  }
10167
11660
  if (!clientSecret) {
@@ -10169,7 +11662,7 @@ async function startRelayServer(opts) {
10169
11662
  ok: false,
10170
11663
  error: "Relay is missing SIDEBOARD_SLACK_CLIENT_SECRET"
10171
11664
  });
10172
- sendHtml(
11665
+ sendHtml2(
10173
11666
  res,
10174
11667
  500,
10175
11668
  "Slack",
@@ -10192,7 +11685,7 @@ async function startRelayServer(opts) {
10192
11685
  void hub.handleAppHomeOpened(payload.team_id, payload.user_id);
10193
11686
  }
10194
11687
  }
10195
- sendHtml(
11688
+ sendHtml2(
10196
11689
  res,
10197
11690
  200,
10198
11691
  "Slack connected",
@@ -10201,7 +11694,7 @@ async function startRelayServer(opts) {
10201
11694
  } catch (err) {
10202
11695
  const message = err instanceof Error ? err.message : String(err);
10203
11696
  pending.put(state, { ok: false, error: message });
10204
- sendHtml(res, 400, "Slack", `<h1>Could not connect Slack</h1><p>${escapeHtml(message)}</p>`);
11697
+ sendHtml2(res, 400, "Slack", `<h1>Could not connect Slack</h1><p>${escapeHtml(message)}</p>`);
10205
11698
  }
10206
11699
  return true;
10207
11700
  };
@@ -10211,19 +11704,30 @@ async function startRelayServer(opts) {
10211
11704
  const state = url2.searchParams.get("state")?.trim() ?? "";
10212
11705
  const value = pending.take(state);
10213
11706
  if (!value) {
10214
- sendJson2(res, 404, { ok: false, error: "pending" });
11707
+ sendJson3(res, 404, { ok: false, error: "pending" });
10215
11708
  return true;
10216
11709
  }
10217
11710
  if (!value.ok) {
10218
- sendJson2(res, 400, { ok: false, error: value.error });
11711
+ sendJson3(res, 400, { ok: false, error: value.error });
10219
11712
  return true;
10220
11713
  }
10221
- sendJson2(res, 200, { ok: true, ...value.payload });
11714
+ sendJson3(res, 200, { ok: true, ...value.payload });
10222
11715
  return true;
10223
11716
  };
10224
11717
  const httpServer = createServer3((req, res) => {
10225
11718
  void (async () => {
10226
11719
  const reqUrl = req.url || "/";
11720
+ if (accountStore && await handleRelayAccountHttp({
11721
+ req,
11722
+ res,
11723
+ store: accountStore,
11724
+ oauth: relayOAuth,
11725
+ sessions: accountSessions,
11726
+ publicOrigin: opts.relayPublicOrigin?.trim() || process.env.SIDEBOARD_RELAY_PUBLIC_ORIGIN?.trim() || `http://127.0.0.1:${boundPort}`,
11727
+ fetchImpl: opts.fetchImpl
11728
+ })) {
11729
+ return;
11730
+ }
10227
11731
  if (await handleCallback(reqUrl, res)) return;
10228
11732
  if (handleResult(reqUrl, res)) return;
10229
11733
  const ableTimeBounce = ableTimeOAuthLocalBounceUrl(reqUrl);
@@ -10237,7 +11741,7 @@ async function startRelayServer(opts) {
10237
11741
  return;
10238
11742
  }
10239
11743
  if (req.url === "/health") {
10240
- sendJson2(res, 200, {
11744
+ sendJson3(res, 200, {
10241
11745
  ok: true,
10242
11746
  service: "sideboard-relay",
10243
11747
  sessions: hub.listSessions().length,
@@ -10256,7 +11760,7 @@ async function startRelayServer(opts) {
10256
11760
  return;
10257
11761
  }
10258
11762
  if (req.url === "/") {
10259
- sendJson2(res, 200, {
11763
+ sendJson3(res, 200, {
10260
11764
  ok: true,
10261
11765
  service: "sideboard-relay",
10262
11766
  sessions: hub.listSessions().length,
@@ -10283,6 +11787,14 @@ async function startRelayServer(opts) {
10283
11787
  socket.destroy();
10284
11788
  return;
10285
11789
  }
11790
+ if (target === remoteWss) {
11791
+ const ip = relayClientIp(req);
11792
+ const count = remoteByIp.get(ip) ?? 0;
11793
+ if (count >= 20 || remoteSockets >= 4e3) {
11794
+ socket.destroy();
11795
+ return;
11796
+ }
11797
+ }
10286
11798
  target.handleUpgrade(req, socket, head, (ws) => {
10287
11799
  target.emit("connection", ws, req);
10288
11800
  });
@@ -10317,7 +11829,10 @@ async function startRelayServer(opts) {
10317
11829
  hub.detachSocket(socket);
10318
11830
  });
10319
11831
  });
10320
- remoteWss.on("connection", (ws) => {
11832
+ remoteWss.on("connection", (ws, req) => {
11833
+ const ip = relayClientIp(req);
11834
+ remoteByIp.set(ip, (remoteByIp.get(ip) ?? 0) + 1);
11835
+ remoteSockets += 1;
10321
11836
  const socket = {
10322
11837
  send: (data) => {
10323
11838
  if (ws.readyState === ws.OPEN) ws.send(data);
@@ -10329,14 +11844,35 @@ async function startRelayServer(opts) {
10329
11844
  }
10330
11845
  }
10331
11846
  };
11847
+ let released = false;
11848
+ const authTimer = accountStore ? setTimeout(() => {
11849
+ try {
11850
+ ws.close(4001, "sign in required");
11851
+ } catch {
11852
+ }
11853
+ }, 12e4) : null;
11854
+ authMarks.set(socket, () => {
11855
+ if (authTimer) clearTimeout(authTimer);
11856
+ });
11857
+ const release = () => {
11858
+ if (released) return;
11859
+ released = true;
11860
+ if (authTimer) clearTimeout(authTimer);
11861
+ authMarks.delete(socket);
11862
+ remoteSockets -= 1;
11863
+ const next = (remoteByIp.get(ip) ?? 1) - 1;
11864
+ if (next <= 0) remoteByIp.delete(ip);
11865
+ else remoteByIp.set(ip, next);
11866
+ remoteHub.detachSocket(socket);
11867
+ };
10332
11868
  startRelaySocketPing(ws, REMOTE_PING_INTERVAL_MS);
10333
11869
  log("remote client connected");
10334
11870
  ws.on("message", (data) => {
10335
11871
  const raw = typeof data === "string" ? data : data.toString("utf8");
10336
11872
  remoteHub.handleClientMessage(socket, raw);
10337
11873
  });
10338
- ws.on("close", () => remoteHub.detachSocket(socket));
10339
- ws.on("error", () => remoteHub.detachSocket(socket));
11874
+ ws.on("close", release);
11875
+ ws.on("error", release);
10340
11876
  });
10341
11877
  const port = opts.port ?? 0;
10342
11878
  const host = opts.host ?? "0.0.0.0";
@@ -10345,7 +11881,7 @@ async function startRelayServer(opts) {
10345
11881
  httpServer.listen(port, host, () => resolve());
10346
11882
  });
10347
11883
  const address = httpServer.address();
10348
- const boundPort = typeof address === "object" && address ? address.port : typeof port === "number" ? port : 0;
11884
+ boundPort = typeof address === "object" && address ? address.port : typeof port === "number" ? port : 0;
10349
11885
  const url = `ws://${host === "0.0.0.0" ? "127.0.0.1" : host}:${boundPort}${SLACK_RELAY_DESKTOP_PATH}`;
10350
11886
  log(`relay listening on ${url}`);
10351
11887
  const ac = new AbortController();
@@ -10388,13 +11924,14 @@ async function startRelayServer(opts) {
10388
11924
  httpServer.close((err) => err ? reject(err) : resolve());
10389
11925
  });
10390
11926
  await socketModeDone.catch(() => void 0);
11927
+ accountStore?.close();
10391
11928
  }
10392
11929
  };
10393
11930
  }
10394
11931
 
10395
11932
  // src/slack/outbound-watch.ts
10396
- import { existsSync as existsSync5, readFileSync as readFileSync5 } from "fs";
10397
- import { join as join6 } from "path";
11933
+ import { existsSync as existsSync5, readFileSync as readFileSync6 } from "fs";
11934
+ import { join as join7 } from "path";
10398
11935
  var MAX_WATCHES = 40;
10399
11936
  var MAX_REPLIES_PER_WATCH = 30;
10400
11937
  var WATCH_TTL_MS = 7 * 24 * 60 * 60 * 1e3;
@@ -10402,7 +11939,7 @@ var POLL_INTERVAL_MS = 12e3;
10402
11939
  var lastPollMs = 0;
10403
11940
  var nameCache = /* @__PURE__ */ new Map();
10404
11941
  function storePath3() {
10405
- return join6(appDataDir(), "slack-outbound-watch.json");
11942
+ return join7(appDataDir(), "slack-outbound-watch.json");
10406
11943
  }
10407
11944
  function watchId(teamId, channelId, ts) {
10408
11945
  return `${teamId}:${channelId}:${ts}`;
@@ -10417,7 +11954,7 @@ function readStore3() {
10417
11954
  const path2 = storePath3();
10418
11955
  if (!existsSync5(path2)) return [];
10419
11956
  try {
10420
- const parsed = isSecureFileEncrypted(path2) ? readSecureJson(path2) : JSON.parse(readFileSync5(path2, "utf8"));
11957
+ const parsed = isSecureFileEncrypted(path2) ? readSecureJson(path2) : JSON.parse(readFileSync6(path2, "utf8"));
10421
11958
  return Array.isArray(parsed?.watches) ? parsed.watches : [];
10422
11959
  } catch {
10423
11960
  return [];
@@ -10469,7 +12006,7 @@ async function continueSourceThread(threadId, prompt) {
10469
12006
  await continueOnReply(threadId, prompt);
10470
12007
  return;
10471
12008
  }
10472
- const { getOrchestrator: getOrchestrator2 } = await import("./orchestrator-QRSEOHXI.js");
12009
+ const { getOrchestrator: getOrchestrator2 } = await import("./orchestrator-QMKVXVUR.js");
10473
12010
  await getOrchestrator2().send(threadId, prompt);
10474
12011
  } catch {
10475
12012
  }
@@ -11061,6 +12598,7 @@ export {
11061
12598
  disconnectLinear,
11062
12599
  disconnectLinearConnection,
11063
12600
  disconnectOptionalService,
12601
+ disconnectRemoteAccount,
11064
12602
  disconnectSlackWorkspace,
11065
12603
  discoverSkills,
11066
12604
  downloadAbleTimeAttachment,
@@ -11317,6 +12855,7 @@ export {
11317
12855
  isPrNotMergeableError,
11318
12856
  isPresentPlanToolName,
11319
12857
  isPrimaryCheckoutThread,
12858
+ isRemoteAccountLoginCancelled,
11320
12859
  isRemoteStopCommand,
11321
12860
  isSelfWrittenRecord,
11322
12861
  isSessionQuotaLimit,
@@ -11678,6 +13217,7 @@ export {
11678
13217
  startMcpServer,
11679
13218
  startOrchestration,
11680
13219
  startRelayServer,
13220
+ startRemoteAccountLogin,
11681
13221
  startSlackOAuth,
11682
13222
  statWorktreePath,
11683
13223
  stopAgentRpcServer,