@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.
- package/dist/{abletime-4ZSDI4YP.js → abletime-VCVPSLSU.js} +4 -4
- package/dist/{abletime-JNQAHLHH.js → abletime-VRATUMDO.js} +4 -4
- package/dist/{abletime-oauth-4RXZH2KU.js → abletime-oauth-4C4G2J3R.js} +3 -3
- package/dist/{abletime-oauth-DCEIINYT.js → abletime-oauth-4K7MICAD.js} +3 -3
- package/dist/{abletime-rest-KVBJFVFD.js → abletime-rest-6N63ZVQ3.js} +3 -3
- package/dist/{abletime-rest-VN6PZ6P2.js → abletime-rest-PIBCNHF3.js} +3 -3
- package/dist/{agents-LAFFXBBZ.js → agents-2FOWVJCN.js} +4 -4
- package/dist/{agents-ZCO37SKG.js → agents-JBVJRDH3.js} +4 -4
- package/dist/{app-settings-RAC4PSSP.js → app-settings-EQQZQNYD.js} +1 -1
- package/dist/{app-settings-L5HGYXWJ.js → app-settings-NI4X5HW3.js} +1 -1
- package/dist/{chunk-UXI52MLJ.js → chunk-2J34G3WJ.js} +3 -3
- package/dist/{chunk-PTNGMA5C.js → chunk-A2G3ZBJS.js} +2 -2
- package/dist/{chunk-BJ5T22L5.js → chunk-FELFTNH2.js} +22 -21
- package/dist/{chunk-GCSG3OGV.js → chunk-HOMZKYHF.js} +29 -27
- package/dist/{chunk-GONPMH36.js → chunk-IJDQKC5A.js} +5 -5
- package/dist/{chunk-USBZ7PH4.js → chunk-IPPY3FG5.js} +1 -1
- package/dist/{chunk-NWE3NOWQ.js → chunk-K7MWJLR7.js} +1 -1
- package/dist/{chunk-DVTEC4BF.js → chunk-MGS5T2A3.js} +29 -27
- package/dist/{chunk-ADWNK2JH.js → chunk-NIB2IZ6J.js} +5 -5
- package/dist/{chunk-AGXZAMOF.js → chunk-NQ7JMVSU.js} +2 -2
- package/dist/{chunk-JLDJGGCL.js → chunk-NT7RIWRR.js} +5 -5
- package/dist/{chunk-L6CYOON4.js → chunk-PZURG3XN.js} +5 -5
- package/dist/{chunk-LAIBYKZS.js → chunk-QINJKNLR.js} +3 -3
- package/dist/{chunk-2QUILREI.js → chunk-QTZO4LPU.js} +2 -2
- package/dist/{chunk-KZ2CDZBZ.js → chunk-R7HLXD4I.js} +3 -3
- package/dist/{chunk-XIVM7K77.js → chunk-SQOAFR2U.js} +23 -22
- package/dist/{chunk-OIENQEGG.js → chunk-WRIMAEF2.js} +67 -122
- package/dist/{chunk-DBFAFI5A.js → chunk-XKZAVGTT.js} +2 -2
- package/dist/{chunk-PAH7KDL2.js → chunk-YNTXWFXT.js} +67 -122
- package/dist/{conductor-B2BXSA7N.js → conductor-GBCE37K4.js} +4 -4
- package/dist/{conductor-DXBLL23V.js → conductor-YYQKZAND.js} +4 -4
- package/dist/{coordinator-prompt-4KVA6XUX.js → coordinator-prompt-B3GLVXPP.js} +3 -3
- package/dist/{coordinator-prompt-MVUHFNSC.js → coordinator-prompt-SFZCZ3HR.js} +3 -3
- package/dist/{global-workspace-FR7QTLPK.js → global-workspace-7XEXTIHL.js} +3 -3
- package/dist/{global-workspace-34Z2IWYQ.js → global-workspace-N2YYJKA5.js} +3 -3
- package/dist/index.cjs +1989 -182
- package/dist/index.d.cts +121 -22
- package/dist/index.d.ts +121 -22
- package/dist/index.js +1872 -45
- package/dist/mcp/run-stdio.cjs +90 -136
- package/dist/mcp/run-stdio.js +11 -11
- package/dist/{orchestrator-6T35CJHB.js → orchestrator-6QJLM6Y4.js} +6 -6
- package/dist/{orchestrator-QRSEOHXI.js → orchestrator-QMKVXVUR.js} +6 -6
- package/dist/{workspaces-5GERYHDJ.js → workspaces-BJCHMGVG.js} +3 -3
- package/dist/{workspaces-2JBDUMM6.js → workspaces-PBB6HY2D.js} +3 -3
- package/dist/{worktree-ACW23KJ5.js → worktree-L57T3KH7.js} +2 -2
- package/dist/{worktree-C7RPVHZ7.js → worktree-Q6UJHG25.js} +2 -2
- 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-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
475
|
-
import "./chunk-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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(
|
|
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(
|
|
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-
|
|
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-
|
|
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-
|
|
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
|
-
|
|
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/
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
11994
|
+
sendJson3(res, 404, { ok: false, error: "pending" });
|
|
10215
11995
|
return true;
|
|
10216
11996
|
}
|
|
10217
11997
|
if (!value.ok) {
|
|
10218
|
-
|
|
11998
|
+
sendJson3(res, 400, { ok: false, error: value.error });
|
|
10219
11999
|
return true;
|
|
10220
12000
|
}
|
|
10221
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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",
|
|
10339
|
-
ws.on("error",
|
|
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
|
-
|
|
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
|
|
10397
|
-
import { join as
|
|
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
|
|
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(
|
|
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-
|
|
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,
|