codex-relay 1.0.3 → 1.0.5
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/README.md +3 -2
- package/dist/api-schema.js +2 -2
- package/dist/api-schema2.js +179 -2
- package/dist/cli.js +97 -4
- package/dist/paths.js +425 -3
- package/dist/src.js +635 -350
- package/package.json +2 -2
- package/src/api-schema.ts +175 -0
package/dist/src.js
CHANGED
|
@@ -1,16 +1,16 @@
|
|
|
1
|
-
import { A as PairResponseSchema,
|
|
2
|
-
import { r as legacyCodexRelayDataPath, t as codexRelayDataPath } from "./paths.js";
|
|
1
|
+
import { A as PairResponseSchema, C as ListQueuedThreadInputsResponseSchema, D as ListWorkspaceFilesResponseSchema, Dt as apiPaths, E as ListWorkspaceDirectoriesResponseSchema, G as ResolveApprovalRequestSchema, K as ResolveApprovalResponseSchema, Lt as stripPromptSkillMentions, Mt as promptMarkdownWithSkills, Ot as chatMessageDetailsFromPromptContext, Q as RuntimePreferencesSchema, S as ListModelsResponseSchema, St as WorkspaceGitActionResponseSchema, T as ListThreadsResponseSchema, U as RateLimitsResponseSchema, X as RuntimePreferencesByWorkspacePathSchema, Z as RuntimePreferencesResponseSchema, _ as EncryptedPayloadSchema, a as ArchiveThreadResponseSchema, at as ThreadContextWindowResponseSchema, b as InterruptThreadRunResponseSchema, bt as WorkspaceFileContentResponseSchema, ct as ThreadMessageDetailResponseSchema, d as CheckoutWorkspaceBranchRequestSchema, dt as ThreadSummarySchema, et as StatusResponseSchema, ft as UpdateRuntimePreferencesRequestSchema, h as CreateThreadRequestSchema, jt as normalizePromptContext, k as PairRequestSchema, kt as createOpenApiDocument, l as ChatMessageSchema, m as ContextWindowUsageSchema, mt as VersionResponseSchema, nt as StreamThreadRunRequestSchema, ot as ThreadDetailResponseSchema, p as CommitPushWorkspaceRequestSchema, pt as UpdateWorkspaceFileContentRequestSchema, q as RunThreadRequestSchema, rt as SubmitThreadInputResponseSchema, st as ThreadMessageDetailFieldSchema, tt as StreamThreadRunEventSchema, vt as WorkspaceChangesResponseSchema, w as ListSkillsResponseSchema, y as ImageAttachmentUploadResponseSchema, z as QueuedThreadInputActionResponseSchema } from "./api-schema2.js";
|
|
2
|
+
import { a as getConnectUrlCandidates, i as createPairingQrPayload, o as getConnectUrlGuidance, r as legacyCodexRelayDataPath, s as createTursoPairingSessionStore, t as codexRelayDataPath } from "./paths.js";
|
|
3
3
|
import { createRequire } from "node:module";
|
|
4
4
|
import qrcode from "qrcode-terminal";
|
|
5
|
-
import { execFile,
|
|
5
|
+
import { execFile, spawn } from "node:child_process";
|
|
6
6
|
import fs, { existsSync, mkdirSync, readFileSync, readdirSync, statSync, writeFileSync } from "node:fs";
|
|
7
|
-
import { access, appendFile, copyFile, mkdir, readFile, readdir, stat, writeFile } from "node:fs/promises";
|
|
8
|
-
import path, { basename, dirname, extname, join, relative, resolve } from "node:path";
|
|
7
|
+
import { access, appendFile, copyFile, mkdir, open, readFile, readdir, stat, writeFile } from "node:fs/promises";
|
|
8
|
+
import path, { basename, dirname, extname, isAbsolute, join, relative, resolve } from "node:path";
|
|
9
9
|
import { fileURLToPath } from "node:url";
|
|
10
10
|
import { z } from "zod";
|
|
11
|
-
import os, { homedir, hostname, networkInterfaces } from "node:os";
|
|
12
|
-
import { serve } from "@hono/node-server";
|
|
13
11
|
import { fromByteArray, toByteArray } from "base64-js";
|
|
12
|
+
import os, { homedir, hostname } from "node:os";
|
|
13
|
+
import { serve } from "@hono/node-server";
|
|
14
14
|
import { createHash, randomBytes, randomUUID } from "node:crypto";
|
|
15
15
|
import pc from "picocolors";
|
|
16
16
|
import { openRepository } from "es-git";
|
|
@@ -24,7 +24,6 @@ import { randomBytes as randomBytes$1, utf8ToBytes } from "@noble/ciphers/utils.
|
|
|
24
24
|
import { ed25519, x25519 } from "@noble/curves/ed25519.js";
|
|
25
25
|
import { hkdf } from "@noble/hashes/hkdf.js";
|
|
26
26
|
import { sha256 } from "@noble/hashes/sha2.js";
|
|
27
|
-
import { connect } from "@tursodatabase/database";
|
|
28
27
|
//#region src/app-server.ts
|
|
29
28
|
var CodexAppServerClient = class {
|
|
30
29
|
child;
|
|
@@ -539,14 +538,16 @@ function updatePreferencesFile(current, patch, workspacePath) {
|
|
|
539
538
|
});
|
|
540
539
|
}
|
|
541
540
|
function mergeRuntimePreferences(current, patch) {
|
|
542
|
-
const { model, reasoningEffort, threadId: _threadId, workspacePath: _workspacePath, ...rest } = UpdateRuntimePreferencesRequestSchema.parse(patch);
|
|
541
|
+
const { model, serviceTier, reasoningEffort, threadId: _threadId, workspacePath: _workspacePath, ...rest } = UpdateRuntimePreferencesRequestSchema.parse(patch);
|
|
543
542
|
const next = {
|
|
544
543
|
...current,
|
|
545
544
|
...rest,
|
|
546
545
|
...model === null ? {} : { model },
|
|
546
|
+
...serviceTier === null ? {} : { serviceTier },
|
|
547
547
|
...reasoningEffort === null ? {} : { reasoningEffort }
|
|
548
548
|
};
|
|
549
549
|
if (model === null) delete next.model;
|
|
550
|
+
if (serviceTier === null) delete next.serviceTier;
|
|
550
551
|
if (reasoningEffort === null) delete next.reasoningEffort;
|
|
551
552
|
return RuntimePreferencesSchema.parse({ ...next });
|
|
552
553
|
}
|
|
@@ -798,6 +799,9 @@ const defaultWorkspacePath = process.cwd();
|
|
|
798
799
|
const defaultCodexModel = "gpt-5.5";
|
|
799
800
|
const execFileAsync = promisify(execFile);
|
|
800
801
|
const IMAGE_ATTACHMENT_MAX_BYTES = 8 * 1024 * 1024;
|
|
802
|
+
const WORKSPACE_FILE_PREVIEW_MAX_BYTES = 256 * 1024;
|
|
803
|
+
const LOCAL_MARKDOWN_IMAGE_PATTERN = /!\[([^\]]*)\]\(([^)]*)\)/g;
|
|
804
|
+
const LOCAL_IMAGE_REFERENCE_PATTERN = /\.(gif|heic|heif|jpe?g|png|webp)$/i;
|
|
801
805
|
const imageAttachmentDirectory = codexRelayDataPath("attachments/images");
|
|
802
806
|
const relayPackage = createRequire(import.meta.url)("../package.json");
|
|
803
807
|
const collaborationModeTemplateNames = [
|
|
@@ -834,6 +838,7 @@ function createApp(options = {}) {
|
|
|
834
838
|
const appServerHistoryLoadsByThreadId = /* @__PURE__ */ new Map();
|
|
835
839
|
const steeringThreads = /* @__PURE__ */ new Set();
|
|
836
840
|
const secureSessionsByTokenHash = /* @__PURE__ */ new Map();
|
|
841
|
+
const activeStreamControllers = /* @__PURE__ */ new Set();
|
|
837
842
|
const threadOptions = { workingDirectory: workspacePath };
|
|
838
843
|
const scheduleAppServerHistoryLoad = (threadId, cachedMessages) => {
|
|
839
844
|
if (!appServer || appServerHistoryLoadsByThreadId.has(threadId)) return;
|
|
@@ -865,7 +870,7 @@ function createApp(options = {}) {
|
|
|
865
870
|
};
|
|
866
871
|
app.use("*", cors());
|
|
867
872
|
app.use("*", async (c, next) => {
|
|
868
|
-
if (!options.pairing || c.req.method === "OPTIONS" || c.req.path === apiPaths.version || c.req.path.startsWith(`${apiPaths.imageAttachments}/`) || c.req.path.startsWith(apiPaths.pair)) {
|
|
873
|
+
if (!options.pairing || c.req.method === "OPTIONS" || c.req.path === apiPaths.version || c.req.path.startsWith(`${apiPaths.imageAttachments}/`) || c.req.path === apiPaths.sessionsClear || c.req.path.startsWith(apiPaths.pair)) {
|
|
869
874
|
await next();
|
|
870
875
|
return;
|
|
871
876
|
}
|
|
@@ -973,6 +978,18 @@ function createApp(options = {}) {
|
|
|
973
978
|
});
|
|
974
979
|
return c.json({ ok: true });
|
|
975
980
|
});
|
|
981
|
+
app.post(apiPaths.sessionsClear, async (c) => {
|
|
982
|
+
if (!options.pairing) return c.json(apiError("pairing_disabled", "Pairing is not enabled on this server."), 404);
|
|
983
|
+
if (options.pairing.approvalSecret && c.req.header("x-codex-relay-approve-secret") !== options.pairing.approvalSecret) return c.json(apiError("unauthorized", "Pairing clear must come from this machine."), 401);
|
|
984
|
+
const result = await options.pairing.sessions.clearAll();
|
|
985
|
+
secureSessionsByTokenHash.clear();
|
|
986
|
+
closeActiveStreamControllers(activeStreamControllers);
|
|
987
|
+
options.pairing.onPairingsCleared?.(result);
|
|
988
|
+
return c.json({
|
|
989
|
+
ok: true,
|
|
990
|
+
...result
|
|
991
|
+
});
|
|
992
|
+
});
|
|
976
993
|
app.post(apiPaths.sessionRefresh, async (c) => {
|
|
977
994
|
if (!options.pairing) return c.json(apiError("pairing_disabled", "Pairing is not enabled on this server."), 404);
|
|
978
995
|
const oldToken = parseBearerToken(c.req.header("authorization"));
|
|
@@ -1157,8 +1174,12 @@ function createApp(options = {}) {
|
|
|
1157
1174
|
const selectedWorkspacePath = await validateThreadWorkspacePath(workspacePath, c.req.query("workspacePath"));
|
|
1158
1175
|
if (!selectedWorkspacePath.success) return secureJson(c, options.pairing, secureSessionsByTokenHash, apiError("invalid_workspace_path", selectedWorkspacePath.error), 400);
|
|
1159
1176
|
const query = c.req.query("query")?.trim() ?? "";
|
|
1177
|
+
const directoryPath = normalizeWorkspaceDirectoryPath(c.req.query("directory") ?? "");
|
|
1178
|
+
if (!directoryPath.success) return secureJson(c, options.pairing, secureSessionsByTokenHash, apiError("invalid_workspace_file_path", directoryPath.error), 400);
|
|
1160
1179
|
const response = ListWorkspaceFilesResponseSchema.parse({
|
|
1161
|
-
|
|
1180
|
+
directory: directoryPath.path,
|
|
1181
|
+
files: await listWorkspaceFiles(selectedWorkspacePath.path, query, directoryPath.path),
|
|
1182
|
+
parentDirectory: parentWorkspaceDirectory(directoryPath.path),
|
|
1162
1183
|
query,
|
|
1163
1184
|
workspacePath: selectedWorkspacePath.path
|
|
1164
1185
|
});
|
|
@@ -1167,6 +1188,40 @@ function createApp(options = {}) {
|
|
|
1167
1188
|
return secureJson(c, options.pairing, secureSessionsByTokenHash, apiError("workspace_files_unavailable", errorMessage(error)), 502);
|
|
1168
1189
|
}
|
|
1169
1190
|
});
|
|
1191
|
+
app.get(apiPaths.workspaceFileContent, async (c) => {
|
|
1192
|
+
try {
|
|
1193
|
+
const selectedWorkspacePath = await validateThreadWorkspacePath(workspacePath, c.req.query("workspacePath"));
|
|
1194
|
+
if (!selectedWorkspacePath.success) return secureJson(c, options.pairing, secureSessionsByTokenHash, apiError("invalid_workspace_path", selectedWorkspacePath.error), 400);
|
|
1195
|
+
const requestedPath = c.req.query("path")?.trim();
|
|
1196
|
+
if (!requestedPath) return secureJson(c, options.pairing, secureSessionsByTokenHash, apiError("missing_workspace_file_path", "Workspace file path is required."), 400);
|
|
1197
|
+
const file = await readWorkspaceFileContent(selectedWorkspacePath.path, requestedPath);
|
|
1198
|
+
if (!file.success) return secureJson(c, options.pairing, secureSessionsByTokenHash, apiError(file.code, file.error), file.status);
|
|
1199
|
+
const response = WorkspaceFileContentResponseSchema.parse({
|
|
1200
|
+
workspacePath: selectedWorkspacePath.path,
|
|
1201
|
+
...file.content
|
|
1202
|
+
});
|
|
1203
|
+
return secureJson(c, options.pairing, secureSessionsByTokenHash, response);
|
|
1204
|
+
} catch (error) {
|
|
1205
|
+
return secureJson(c, options.pairing, secureSessionsByTokenHash, apiError("workspace_file_unavailable", errorMessage(error)), 502);
|
|
1206
|
+
}
|
|
1207
|
+
});
|
|
1208
|
+
app.put(apiPaths.workspaceFileContent, async (c) => {
|
|
1209
|
+
const parsed = await parseRequestJson(c, options.pairing, secureSessionsByTokenHash, UpdateWorkspaceFileContentRequestSchema);
|
|
1210
|
+
if (!parsed.success) return secureJson(c, options.pairing, secureSessionsByTokenHash, validationError(parsed.error), 400);
|
|
1211
|
+
try {
|
|
1212
|
+
const selectedWorkspacePath = await validateThreadWorkspacePath(workspacePath, parsed.data.workspacePath);
|
|
1213
|
+
if (!selectedWorkspacePath.success) return secureJson(c, options.pairing, secureSessionsByTokenHash, apiError("invalid_workspace_path", selectedWorkspacePath.error), 400);
|
|
1214
|
+
const file = await updateWorkspaceFileContent(selectedWorkspacePath.path, parsed.data);
|
|
1215
|
+
if (!file.success) return secureJson(c, options.pairing, secureSessionsByTokenHash, apiError(file.code, file.error), file.status);
|
|
1216
|
+
const response = WorkspaceFileContentResponseSchema.parse({
|
|
1217
|
+
workspacePath: selectedWorkspacePath.path,
|
|
1218
|
+
...file.content
|
|
1219
|
+
});
|
|
1220
|
+
return secureJson(c, options.pairing, secureSessionsByTokenHash, response);
|
|
1221
|
+
} catch (error) {
|
|
1222
|
+
return secureJson(c, options.pairing, secureSessionsByTokenHash, apiError("workspace_file_update_unavailable", errorMessage(error)), 502);
|
|
1223
|
+
}
|
|
1224
|
+
});
|
|
1170
1225
|
app.get(apiPaths.rateLimits, async (c) => {
|
|
1171
1226
|
if (!appServer) return secureJson(c, options.pairing, secureSessionsByTokenHash, RateLimitsResponseSchema.parse({ buckets: [] }));
|
|
1172
1227
|
try {
|
|
@@ -1299,7 +1354,7 @@ function createApp(options = {}) {
|
|
|
1299
1354
|
let loadedMessages = false;
|
|
1300
1355
|
let messages = cachedMessages;
|
|
1301
1356
|
let responseThread = preserveKnownRunningThreadState(mappedThread, wasKnownRunning);
|
|
1302
|
-
const rolloutHistory = readRolloutThreadMessages(threadId);
|
|
1357
|
+
const rolloutHistory = readRolloutThreadMessages(threadId, workspacePath);
|
|
1303
1358
|
if (rolloutHistory.messages.length > 0) {
|
|
1304
1359
|
messages = mergeThreadMessagePages(rolloutHistory.messages, cachedMessages);
|
|
1305
1360
|
responseThread = rememberRolloutThreadMessages(threads, responseThread, messages, rolloutHistory.messageCountLowerBound);
|
|
@@ -1338,7 +1393,7 @@ function createApp(options = {}) {
|
|
|
1338
1393
|
});
|
|
1339
1394
|
}
|
|
1340
1395
|
const cachedMessages = messagesByThreadId.get(threadId) ?? [];
|
|
1341
|
-
const rolloutHistory = readRolloutThreadMessages(threadId);
|
|
1396
|
+
const rolloutHistory = readRolloutThreadMessages(threadId, workspacePath);
|
|
1342
1397
|
const baseThread = threads.get(threadId) ?? knownThread ?? (rolloutHistory.rolloutPath ? rolloutThreadMetadata(threadId, workspacePath, rolloutHistory.rolloutPath, [...rolloutHistory.messages, ...cachedMessages]) : void 0);
|
|
1343
1398
|
if (baseThread && (cachedMessages.length > 0 || rolloutHistory.messages.length > 0)) {
|
|
1344
1399
|
const messages = rolloutHistory.messages.length > 0 ? mergeThreadMessagePages(rolloutHistory.messages, cachedMessages) : dedupeThreadMessages(cachedMessages);
|
|
@@ -1705,9 +1760,12 @@ function createApp(options = {}) {
|
|
|
1705
1760
|
});
|
|
1706
1761
|
const encoder = new TextEncoder();
|
|
1707
1762
|
const secureSession = getSecureSessionForRequest(c, options.pairing, secureSessionsByTokenHash);
|
|
1763
|
+
let streamController;
|
|
1708
1764
|
let streamSettled = false;
|
|
1709
1765
|
const stream = new ReadableStream({
|
|
1710
1766
|
start(controller) {
|
|
1767
|
+
streamController = controller;
|
|
1768
|
+
activeStreamControllers.add(controller);
|
|
1711
1769
|
relayDebugLog("thread.stream.started", {
|
|
1712
1770
|
mode: !runOptions.prompt && appServer ? "attach" : "run",
|
|
1713
1771
|
threadId
|
|
@@ -1738,6 +1796,7 @@ function createApp(options = {}) {
|
|
|
1738
1796
|
mode: "attach",
|
|
1739
1797
|
threadId
|
|
1740
1798
|
});
|
|
1799
|
+
activeStreamControllers.delete(controller);
|
|
1741
1800
|
stopPreviewMonitor();
|
|
1742
1801
|
});
|
|
1743
1802
|
return;
|
|
@@ -1755,6 +1814,7 @@ function createApp(options = {}) {
|
|
|
1755
1814
|
mode: "unsupported",
|
|
1756
1815
|
threadId
|
|
1757
1816
|
});
|
|
1817
|
+
activeStreamControllers.delete(controller);
|
|
1758
1818
|
stopPreviewMonitor();
|
|
1759
1819
|
return;
|
|
1760
1820
|
}
|
|
@@ -1787,10 +1847,12 @@ function createApp(options = {}) {
|
|
|
1787
1847
|
mode: "run",
|
|
1788
1848
|
threadId
|
|
1789
1849
|
});
|
|
1850
|
+
activeStreamControllers.delete(controller);
|
|
1790
1851
|
stopPreviewMonitor();
|
|
1791
1852
|
});
|
|
1792
1853
|
},
|
|
1793
1854
|
cancel(reason) {
|
|
1855
|
+
if (streamController) activeStreamControllers.delete(streamController);
|
|
1794
1856
|
relayDebugLog("thread.stream.cancelled_by_client", {
|
|
1795
1857
|
reason: debugReason(reason),
|
|
1796
1858
|
settled: streamSettled,
|
|
@@ -1880,7 +1942,8 @@ async function createAppServerThreadRecord(input) {
|
|
|
1880
1942
|
experimentalRawEvents: false,
|
|
1881
1943
|
model: input.options.model ?? null,
|
|
1882
1944
|
persistExtendedHistory: true,
|
|
1883
|
-
sandbox: runtime.sandbox
|
|
1945
|
+
sandbox: runtime.sandbox,
|
|
1946
|
+
serviceTier: input.options.serviceTier ?? null
|
|
1884
1947
|
});
|
|
1885
1948
|
const metadata = ThreadSummarySchema.parse({
|
|
1886
1949
|
...mapAppServerThread({
|
|
@@ -2133,6 +2196,7 @@ async function startAppServerTurn(appServer, threadId, input) {
|
|
|
2133
2196
|
input: appServerTurnInput(input.prompt, input.attachments, input.skills),
|
|
2134
2197
|
model: input.runOptions.model ?? null,
|
|
2135
2198
|
sandboxPolicy: runtime.sandboxPolicy,
|
|
2199
|
+
serviceTier: input.runOptions.serviceTier ?? null,
|
|
2136
2200
|
threadId
|
|
2137
2201
|
};
|
|
2138
2202
|
await resumeAppServerThreadIfNeeded(appServer, threadId, input, runtime);
|
|
@@ -2164,6 +2228,7 @@ async function resumeAppServerThread(appServer, threadId, input, runtime) {
|
|
|
2164
2228
|
model: input.runOptions.model ?? null,
|
|
2165
2229
|
persistExtendedHistory: true,
|
|
2166
2230
|
sandbox: runtime.sandbox,
|
|
2231
|
+
serviceTier: input.runOptions.serviceTier ?? null,
|
|
2167
2232
|
threadId
|
|
2168
2233
|
});
|
|
2169
2234
|
}
|
|
@@ -2918,7 +2983,8 @@ async function recoverMissingAppServerThread(input) {
|
|
|
2918
2983
|
experimentalRawEvents: false,
|
|
2919
2984
|
model: input.runOptions.model ?? null,
|
|
2920
2985
|
persistExtendedHistory: true,
|
|
2921
|
-
sandbox: runtime.sandbox
|
|
2986
|
+
sandbox: runtime.sandbox,
|
|
2987
|
+
serviceTier: input.runOptions.serviceTier ?? null
|
|
2922
2988
|
});
|
|
2923
2989
|
const recoveredThread = mapAppServerThread({
|
|
2924
2990
|
...thread,
|
|
@@ -3109,6 +3175,9 @@ function appServerUserMessageText(item) {
|
|
|
3109
3175
|
case "text": return content.text;
|
|
3110
3176
|
case "image":
|
|
3111
3177
|
case "localImage":
|
|
3178
|
+
case "document":
|
|
3179
|
+
case "file":
|
|
3180
|
+
case "localFile":
|
|
3112
3181
|
case "mention":
|
|
3113
3182
|
case "skill": return "";
|
|
3114
3183
|
}
|
|
@@ -3127,11 +3196,86 @@ function appServerUserMessageDetails(item) {
|
|
|
3127
3196
|
type: "image"
|
|
3128
3197
|
}];
|
|
3129
3198
|
case "localImage": return localImageAttachmentDetails(content.path);
|
|
3199
|
+
case "document":
|
|
3200
|
+
case "file":
|
|
3201
|
+
case "localFile": return appServerDocumentAttachmentDetails(content);
|
|
3130
3202
|
default: return [];
|
|
3131
3203
|
}
|
|
3132
3204
|
});
|
|
3133
3205
|
return attachments.length > 0 ? { attachments } : void 0;
|
|
3134
3206
|
}
|
|
3207
|
+
function appServerAgentMessageParts(item) {
|
|
3208
|
+
return agentMessageParts(item.text);
|
|
3209
|
+
}
|
|
3210
|
+
function agentMessageParts(text) {
|
|
3211
|
+
const imageReferences = localMarkdownImageReferences(text);
|
|
3212
|
+
if (imageReferences.length === 0) return {
|
|
3213
|
+
content: text,
|
|
3214
|
+
details: void 0
|
|
3215
|
+
};
|
|
3216
|
+
const materializedReferences = /* @__PURE__ */ new Set();
|
|
3217
|
+
const attachments = imageReferences.flatMap((reference) => {
|
|
3218
|
+
const name = reference.alt || basename(localImageFilePath(reference.destination) ?? reference.destination);
|
|
3219
|
+
const details = localImageAttachmentDetails(reference.destination, name);
|
|
3220
|
+
if (details.length > 0) materializedReferences.add(reference.destination);
|
|
3221
|
+
return details;
|
|
3222
|
+
});
|
|
3223
|
+
return {
|
|
3224
|
+
content: attachments.length > 0 ? stripMaterializedMarkdownImages(text, materializedReferences) : text,
|
|
3225
|
+
details: attachments.length > 0 ? { attachments } : void 0
|
|
3226
|
+
};
|
|
3227
|
+
}
|
|
3228
|
+
function localMarkdownImageReferences(markdown) {
|
|
3229
|
+
const references = [];
|
|
3230
|
+
const seen = /* @__PURE__ */ new Set();
|
|
3231
|
+
for (const match of markdown.matchAll(LOCAL_MARKDOWN_IMAGE_PATTERN)) {
|
|
3232
|
+
const destination = markdownImageDestination(match[2] ?? "");
|
|
3233
|
+
if (!destination || seen.has(destination) || !isLocalMarkdownImageReference(destination)) continue;
|
|
3234
|
+
seen.add(destination);
|
|
3235
|
+
references.push({
|
|
3236
|
+
alt: (match[1] ?? "").trim(),
|
|
3237
|
+
destination
|
|
3238
|
+
});
|
|
3239
|
+
}
|
|
3240
|
+
return references;
|
|
3241
|
+
}
|
|
3242
|
+
function stripMaterializedMarkdownImages(markdown, destinations) {
|
|
3243
|
+
return markdown.replace(LOCAL_MARKDOWN_IMAGE_PATTERN, (match, _alt, rawDestination) => {
|
|
3244
|
+
const destination = markdownImageDestination(rawDestination);
|
|
3245
|
+
return destination && destinations.has(destination) ? "" : match;
|
|
3246
|
+
}).replace(/[ \t]+\n/g, "\n").replace(/\n{3,}/g, "\n\n").trim();
|
|
3247
|
+
}
|
|
3248
|
+
function markdownImageDestination(value) {
|
|
3249
|
+
let destination = value.trim();
|
|
3250
|
+
if (destination.startsWith("<")) {
|
|
3251
|
+
const endIndex = destination.indexOf(">");
|
|
3252
|
+
if (endIndex > 0) destination = destination.slice(1, endIndex);
|
|
3253
|
+
} else {
|
|
3254
|
+
const titleIndex = destination.search(/\s+["']/);
|
|
3255
|
+
if (titleIndex > 0) destination = destination.slice(0, titleIndex);
|
|
3256
|
+
}
|
|
3257
|
+
try {
|
|
3258
|
+
return decodeURI(destination);
|
|
3259
|
+
} catch {
|
|
3260
|
+
return destination;
|
|
3261
|
+
}
|
|
3262
|
+
}
|
|
3263
|
+
function isLocalMarkdownImageReference(reference) {
|
|
3264
|
+
if (/^[A-Za-z][A-Za-z0-9+.-]*:/.test(reference) && !reference.startsWith("file://")) return false;
|
|
3265
|
+
const filePath = localImageFilePath(reference);
|
|
3266
|
+
return Boolean(filePath && LOCAL_IMAGE_REFERENCE_PATTERN.test(filePath));
|
|
3267
|
+
}
|
|
3268
|
+
function appServerDocumentAttachmentDetails(input) {
|
|
3269
|
+
const reference = input.path ?? input.url;
|
|
3270
|
+
if (!reference) return [];
|
|
3271
|
+
return [{
|
|
3272
|
+
mimeType: input.mimeType,
|
|
3273
|
+
name: input.name ?? basename(reference),
|
|
3274
|
+
path: input.path,
|
|
3275
|
+
type: "document",
|
|
3276
|
+
url: input.url
|
|
3277
|
+
}];
|
|
3278
|
+
}
|
|
3135
3279
|
async function saveUploadedImageAttachment(file) {
|
|
3136
3280
|
if (!file.type.startsWith("image/")) throw new Error(`Unsupported attachment type: ${file.type || "unknown"}`);
|
|
3137
3281
|
if (file.size > IMAGE_ATTACHMENT_MAX_BYTES) throw new Error(`Image ${file.name || "attachment"} is too large.`);
|
|
@@ -3351,10 +3495,32 @@ function updateThread(threads, messagesByThreadId, threadId, update) {
|
|
|
3351
3495
|
}
|
|
3352
3496
|
function sendSse(controller, encoder, secureSession, event) {
|
|
3353
3497
|
const parsed = StreamThreadRunEventSchema.parse(event);
|
|
3498
|
+
const threadId = threadIdFromStreamEvent(parsed);
|
|
3499
|
+
relayDebugLog("thread.stream.sse", {
|
|
3500
|
+
direction: "server_to_mobile",
|
|
3501
|
+
eventType: parsed.type,
|
|
3502
|
+
threadId,
|
|
3503
|
+
payload: parsed
|
|
3504
|
+
});
|
|
3354
3505
|
const data = secureSession ? EncryptedPayloadSchema.parse(encryptForMobile(secureSession.session, JSON.stringify(parsed))) : parsed;
|
|
3355
3506
|
if (secureSession) secureSession.persist().catch(() => void 0);
|
|
3356
|
-
if (!enqueueSseChunk(controller, encoder.encode(`event: ${parsed.type}\n`)))
|
|
3357
|
-
|
|
3507
|
+
if (!enqueueSseChunk(controller, encoder.encode(`event: ${parsed.type}\n`))) {
|
|
3508
|
+
relayDebugLog("thread.stream.sse.enqueue_failed", {
|
|
3509
|
+
eventType: parsed.type,
|
|
3510
|
+
stage: "event",
|
|
3511
|
+
threadId
|
|
3512
|
+
});
|
|
3513
|
+
return;
|
|
3514
|
+
}
|
|
3515
|
+
if (!enqueueSseChunk(controller, encoder.encode(`data: ${JSON.stringify(data)}\n\n`))) relayDebugLog("thread.stream.sse.enqueue_failed", {
|
|
3516
|
+
eventType: parsed.type,
|
|
3517
|
+
stage: "data",
|
|
3518
|
+
threadId
|
|
3519
|
+
});
|
|
3520
|
+
}
|
|
3521
|
+
function threadIdFromStreamEvent(event) {
|
|
3522
|
+
if ("threadId" in event && typeof event.threadId === "string") return event.threadId;
|
|
3523
|
+
if ("thread" in event && event.thread) return event.thread.id;
|
|
3358
3524
|
}
|
|
3359
3525
|
function enqueueSseChunk(controller, chunk) {
|
|
3360
3526
|
try {
|
|
@@ -3374,6 +3540,10 @@ function closeSseController(controller) {
|
|
|
3374
3540
|
throw error;
|
|
3375
3541
|
}
|
|
3376
3542
|
}
|
|
3543
|
+
function closeActiveStreamControllers(controllers) {
|
|
3544
|
+
for (const controller of controllers) closeSseController(controller);
|
|
3545
|
+
controllers.clear();
|
|
3546
|
+
}
|
|
3377
3547
|
function isClosedStreamControllerError(error) {
|
|
3378
3548
|
return error instanceof TypeError && error.code === "ERR_INVALID_STATE" && error.message.includes("Controller is already closed");
|
|
3379
3549
|
}
|
|
@@ -3556,6 +3726,7 @@ function runtimeMetadataFromOptions(options) {
|
|
|
3556
3726
|
const sandboxMode = options.sandboxMode ?? runtime.sandboxMode;
|
|
3557
3727
|
return {
|
|
3558
3728
|
...options.model ? { model: options.model } : {},
|
|
3729
|
+
...options.serviceTier ? { serviceTier: options.serviceTier } : {},
|
|
3559
3730
|
...options.runtimeMode ? { runtimeMode: options.runtimeMode } : {},
|
|
3560
3731
|
...options.collaborationMode ? { collaborationMode: options.collaborationMode } : {},
|
|
3561
3732
|
...approvalPolicy ? { approvalPolicy } : {},
|
|
@@ -3568,6 +3739,7 @@ function withRuntimePreferences(preferences, options) {
|
|
|
3568
3739
|
...options,
|
|
3569
3740
|
approvalPolicy: options.approvalPolicy,
|
|
3570
3741
|
model: options.model ?? preferences.model,
|
|
3742
|
+
serviceTier: options.serviceTier ?? preferences.serviceTier,
|
|
3571
3743
|
reasoningEffort: options.reasoningEffort ?? preferences.reasoningEffort,
|
|
3572
3744
|
runtimeMode: options.runtimeMode ?? preferences.runtimeMode,
|
|
3573
3745
|
sandboxMode: options.sandboxMode
|
|
@@ -3615,7 +3787,7 @@ function sandboxPolicyForMode(sandboxMode, workspacePath) {
|
|
|
3615
3787
|
};
|
|
3616
3788
|
}
|
|
3617
3789
|
function hasExplicitRunOptions(options) {
|
|
3618
|
-
return Boolean(options.model || options.reasoningEffort || options.approvalPolicy || options.sandboxMode || options.collaborationMode === "plan" || options.runtimeMode);
|
|
3790
|
+
return Boolean(options.model || options.serviceTier || options.reasoningEffort || options.approvalPolicy || options.sandboxMode || options.collaborationMode === "plan" || options.runtimeMode);
|
|
3619
3791
|
}
|
|
3620
3792
|
function mapAppServerThread(thread, fallbackMessageCount) {
|
|
3621
3793
|
const createdAt = fromUnixSeconds(thread.createdAt);
|
|
@@ -3791,7 +3963,7 @@ function readSessionIndexThreadTitle(threadId) {
|
|
|
3791
3963
|
} catch {}
|
|
3792
3964
|
}
|
|
3793
3965
|
}
|
|
3794
|
-
function readRolloutThreadMessages(threadId) {
|
|
3966
|
+
function readRolloutThreadMessages(threadId, workspacePath = defaultWorkspacePath) {
|
|
3795
3967
|
const rolloutPath = findRolloutFileForThread(threadId);
|
|
3796
3968
|
if (!rolloutPath) return {
|
|
3797
3969
|
messageCountLowerBound: 0,
|
|
@@ -3799,6 +3971,8 @@ function readRolloutThreadMessages(threadId) {
|
|
|
3799
3971
|
rolloutPath
|
|
3800
3972
|
};
|
|
3801
3973
|
const collected = [];
|
|
3974
|
+
const applyPatchInputs = /* @__PURE__ */ new Map();
|
|
3975
|
+
const pendingApplyPatchChanges = [];
|
|
3802
3976
|
const lines = readFileSync(rolloutPath, "utf8").split("\n");
|
|
3803
3977
|
for (let index = 0; index < lines.length; index += 1) {
|
|
3804
3978
|
const line = lines[index];
|
|
@@ -3806,7 +3980,15 @@ function readRolloutThreadMessages(threadId) {
|
|
|
3806
3980
|
if (!line.trim()) continue;
|
|
3807
3981
|
if (!isRolloutMessageLine(line)) continue;
|
|
3808
3982
|
try {
|
|
3809
|
-
const
|
|
3983
|
+
const record = JSON.parse(line);
|
|
3984
|
+
rememberRolloutApplyPatchInput(record, applyPatchInputs);
|
|
3985
|
+
collectRolloutApplyPatchOutput(record, workspacePath, applyPatchInputs, pendingApplyPatchChanges);
|
|
3986
|
+
if (isRolloutTaskComplete(record) && pendingApplyPatchChanges.length > 0) {
|
|
3987
|
+
collected.push(rolloutApplyPatchSummaryMessage(threadId, record, `rollout:${lineNumber}:apply_patch`, pendingApplyPatchChanges));
|
|
3988
|
+
pendingApplyPatchChanges.length = 0;
|
|
3989
|
+
continue;
|
|
3990
|
+
}
|
|
3991
|
+
const message = rolloutRecordMessage(threadId, record, `rollout:${lineNumber}`, workspacePath);
|
|
3810
3992
|
if (!message) continue;
|
|
3811
3993
|
collected.push(message);
|
|
3812
3994
|
} catch {}
|
|
@@ -3833,7 +4015,10 @@ function findRolloutFileForThread(threadId) {
|
|
|
3833
4015
|
}
|
|
3834
4016
|
}
|
|
3835
4017
|
}
|
|
3836
|
-
function
|
|
4018
|
+
function isRolloutTaskComplete(record) {
|
|
4019
|
+
return record.type === "event_msg" && record.payload?.type === "task_complete";
|
|
4020
|
+
}
|
|
4021
|
+
function rolloutRecordMessage(threadId, record, messageKey, workspacePath = defaultWorkspacePath) {
|
|
3837
4022
|
const timestamp = typeof record.timestamp === "string" ? record.timestamp : (/* @__PURE__ */ new Date()).toISOString();
|
|
3838
4023
|
const payload = record.payload;
|
|
3839
4024
|
if (!payload) return;
|
|
@@ -3853,15 +4038,37 @@ function rolloutRecordMessage(threadId, record, messageKey) {
|
|
|
3853
4038
|
if (record.type === "event_msg" && payload.type === "agent_message") {
|
|
3854
4039
|
const content = firstString(payload, ["message"]);
|
|
3855
4040
|
if (!content) return;
|
|
4041
|
+
const messageParts = agentMessageParts(content);
|
|
3856
4042
|
return ChatMessageSchema.parse({
|
|
3857
4043
|
id: `${messageKey}:assistant`,
|
|
3858
4044
|
threadId,
|
|
3859
4045
|
role: "assistant",
|
|
3860
|
-
content,
|
|
4046
|
+
content: messageParts.content,
|
|
4047
|
+
details: messageParts.details,
|
|
3861
4048
|
createdAt: timestamp,
|
|
3862
4049
|
state: "completed"
|
|
3863
4050
|
});
|
|
3864
4051
|
}
|
|
4052
|
+
if (record.type === "event_msg" && payload.type === "patch_apply_end") {
|
|
4053
|
+
const changes = rolloutPatchApplyChanges(payload.changes, workspacePath);
|
|
4054
|
+
if (changes.length === 0) return;
|
|
4055
|
+
const patchPreview = largeTextPreview(changes.map((change) => change.patch).filter((patch) => Boolean(patch)).join("\n"));
|
|
4056
|
+
return ChatMessageSchema.parse({
|
|
4057
|
+
id: `${messageKey}:patch:${firstString(payload, ["call_id"]) ?? ""}`,
|
|
4058
|
+
threadId,
|
|
4059
|
+
role: "tool",
|
|
4060
|
+
kind: "fileChange",
|
|
4061
|
+
content: summarizeFileChanges(changes),
|
|
4062
|
+
createdAt: timestamp,
|
|
4063
|
+
state: "completed",
|
|
4064
|
+
details: {
|
|
4065
|
+
changes: changes.map(({ patch: _patch, ...change }) => change),
|
|
4066
|
+
patch: patchPreview?.text,
|
|
4067
|
+
patchOriginalLength: patchPreview?.originalLength,
|
|
4068
|
+
patchTruncated: patchPreview?.truncated
|
|
4069
|
+
}
|
|
4070
|
+
});
|
|
4071
|
+
}
|
|
3865
4072
|
if (record.type === "event_msg" && payload.type === "exec_command_end") {
|
|
3866
4073
|
const command = Array.isArray(payload.command) ? payload.command.map((part) => String(part)).join(" ") : firstString(payload, ["command"]) || "Command";
|
|
3867
4074
|
const outputPreview = largeTextPreview(firstString(payload, ["aggregated_output"]));
|
|
@@ -3916,8 +4123,114 @@ function rolloutImageAttachments(value, source) {
|
|
|
3916
4123
|
return localImageAttachmentDetails(image, name);
|
|
3917
4124
|
});
|
|
3918
4125
|
}
|
|
4126
|
+
function rememberRolloutApplyPatchInput(record, applyPatchInputs) {
|
|
4127
|
+
const payload = record.payload;
|
|
4128
|
+
if (record.type !== "response_item" || payload?.type !== "custom_tool_call" || firstString(payload, ["name"]) !== "apply_patch") return;
|
|
4129
|
+
const callId = firstString(payload, ["call_id"]);
|
|
4130
|
+
const input = firstString(payload, ["input"]);
|
|
4131
|
+
if (callId && input) applyPatchInputs.set(callId, input);
|
|
4132
|
+
}
|
|
4133
|
+
function collectRolloutApplyPatchOutput(record, workspacePath, applyPatchInputs, pendingApplyPatchChanges) {
|
|
4134
|
+
const payload = record.payload;
|
|
4135
|
+
if (record.type !== "response_item" || payload?.type !== "custom_tool_call_output" || !firstString(payload, ["call_id"])) return;
|
|
4136
|
+
const changes = rolloutApplyPatchOutputChanges(rolloutCustomToolOutputText(payload), workspacePath);
|
|
4137
|
+
if (changes.length === 0) return;
|
|
4138
|
+
const callId = firstString(payload, ["call_id"]);
|
|
4139
|
+
const patch = callId ? applyPatchInputs.get(callId) : void 0;
|
|
4140
|
+
for (const change of changes) pendingApplyPatchChanges.push({
|
|
4141
|
+
...change,
|
|
4142
|
+
patch
|
|
4143
|
+
});
|
|
4144
|
+
}
|
|
4145
|
+
function rolloutApplyPatchSummaryMessage(threadId, record, messageKey, pendingApplyPatchChanges) {
|
|
4146
|
+
const timestamp = typeof record.timestamp === "string" ? record.timestamp : (/* @__PURE__ */ new Date()).toISOString();
|
|
4147
|
+
const patchPreview = largeTextPreview([...new Set(pendingApplyPatchChanges.map((change) => change.patch))].filter((patch) => Boolean(patch)).join("\n"));
|
|
4148
|
+
return ChatMessageSchema.parse({
|
|
4149
|
+
id: `${messageKey}:summary`,
|
|
4150
|
+
threadId,
|
|
4151
|
+
role: "tool",
|
|
4152
|
+
kind: "fileChange",
|
|
4153
|
+
content: summarizeFileChanges(pendingApplyPatchChanges),
|
|
4154
|
+
createdAt: timestamp,
|
|
4155
|
+
state: "completed",
|
|
4156
|
+
details: {
|
|
4157
|
+
changes: pendingApplyPatchChanges.map(({ patch: _patch, ...change }) => change),
|
|
4158
|
+
patch: patchPreview?.text,
|
|
4159
|
+
patchOriginalLength: patchPreview?.originalLength,
|
|
4160
|
+
patchTruncated: patchPreview?.truncated
|
|
4161
|
+
}
|
|
4162
|
+
});
|
|
4163
|
+
}
|
|
4164
|
+
function rolloutCustomToolOutputText(payload) {
|
|
4165
|
+
const rawOutput = firstString(payload, ["output"]);
|
|
4166
|
+
if (!rawOutput) return;
|
|
4167
|
+
try {
|
|
4168
|
+
const parsed = JSON.parse(rawOutput);
|
|
4169
|
+
return typeof parsed.output === "string" ? parsed.output : rawOutput;
|
|
4170
|
+
} catch {
|
|
4171
|
+
return rawOutput;
|
|
4172
|
+
}
|
|
4173
|
+
}
|
|
4174
|
+
function rolloutApplyPatchOutputChanges(value, workspacePath) {
|
|
4175
|
+
if (!value || !value.includes("Updated the following files")) return [];
|
|
4176
|
+
return value.split("\n").flatMap((line) => {
|
|
4177
|
+
const match = line.match(/^\s*([ADM])\s+(.+?)\s*$/);
|
|
4178
|
+
if (!match) return [];
|
|
4179
|
+
return [{
|
|
4180
|
+
kind: rolloutPatchChangeKind(match[1] === "A" ? "added" : match[1] === "D" ? "deleted" : "modified"),
|
|
4181
|
+
path: rolloutPatchDisplayPath(match[2] ?? "", workspacePath)
|
|
4182
|
+
}];
|
|
4183
|
+
});
|
|
4184
|
+
}
|
|
4185
|
+
function rolloutPatchApplyChanges(value, workspacePath = defaultWorkspacePath) {
|
|
4186
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return [];
|
|
4187
|
+
return Object.entries(value).flatMap(([rawPath, rawChange]) => {
|
|
4188
|
+
if (!rawChange || typeof rawChange !== "object") return [];
|
|
4189
|
+
const record = rawChange;
|
|
4190
|
+
return [{
|
|
4191
|
+
kind: rolloutPatchChangeKind(firstString(record, ["type"]) ?? "modified"),
|
|
4192
|
+
patch: firstString(record, ["unified_diff", "patch"]),
|
|
4193
|
+
path: rolloutPatchDisplayPath(rawPath, workspacePath)
|
|
4194
|
+
}];
|
|
4195
|
+
});
|
|
4196
|
+
}
|
|
4197
|
+
function rolloutPatchChangeKind(type) {
|
|
4198
|
+
const normalized = type.toLowerCase();
|
|
4199
|
+
if ([
|
|
4200
|
+
"add",
|
|
4201
|
+
"added",
|
|
4202
|
+
"create",
|
|
4203
|
+
"created"
|
|
4204
|
+
].includes(normalized)) return "added";
|
|
4205
|
+
if ([
|
|
4206
|
+
"delete",
|
|
4207
|
+
"deleted",
|
|
4208
|
+
"remove",
|
|
4209
|
+
"removed"
|
|
4210
|
+
].includes(normalized)) return "deleted";
|
|
4211
|
+
if ([
|
|
4212
|
+
"move",
|
|
4213
|
+
"moved",
|
|
4214
|
+
"rename",
|
|
4215
|
+
"renamed"
|
|
4216
|
+
].includes(normalized)) return "renamed";
|
|
4217
|
+
return "modified";
|
|
4218
|
+
}
|
|
4219
|
+
function rolloutPatchDisplayPath(value, workspacePath = defaultWorkspacePath) {
|
|
4220
|
+
const filePath = value.startsWith("file://") ? fileUrlPath(value) : value;
|
|
4221
|
+
if (!isAbsolute(filePath)) return filePath;
|
|
4222
|
+
const relativePath = relative(resolve(workspacePath), filePath);
|
|
4223
|
+
return relativePath && !relativePath.startsWith("..") && !isAbsolute(relativePath) ? relativePath.split("\\").join("/") : filePath;
|
|
4224
|
+
}
|
|
4225
|
+
function fileUrlPath(value) {
|
|
4226
|
+
try {
|
|
4227
|
+
return fileURLToPath(value);
|
|
4228
|
+
} catch {
|
|
4229
|
+
return value;
|
|
4230
|
+
}
|
|
4231
|
+
}
|
|
3919
4232
|
function isRolloutMessageLine(line) {
|
|
3920
|
-
return line.includes("\"type\":\"event_msg\"") && (line.includes("\"type\":\"user_message\"") || line.includes("\"type\":\"agent_message\"") || line.includes("\"type\":\"exec_command_end\"") || line.includes("\"type\":\"mcp_tool_call_end\""));
|
|
4233
|
+
return line.includes("\"type\":\"event_msg\"") && (line.includes("\"type\":\"user_message\"") || line.includes("\"type\":\"agent_message\"") || line.includes("\"type\":\"patch_apply_end\"") || line.includes("\"type\":\"task_complete\"") || line.includes("\"type\":\"exec_command_end\"") || line.includes("\"type\":\"mcp_tool_call_end\"")) || line.includes("\"type\":\"response_item\"") && (line.includes("\"type\":\"custom_tool_call\"") || line.includes("\"type\":\"custom_tool_call_output\""));
|
|
3921
4234
|
}
|
|
3922
4235
|
function threadDetailResponse(input) {
|
|
3923
4236
|
return ThreadDetailResponseSchema.parse({
|
|
@@ -3996,12 +4309,13 @@ function mapAppServerItem(threadId, turn, item) {
|
|
|
3996
4309
|
case "agentMessage": {
|
|
3997
4310
|
const agentItem = item;
|
|
3998
4311
|
const planContent = proposedPlanContent(agentItem.text);
|
|
4312
|
+
const messageParts = appServerAgentMessageParts(agentItem);
|
|
3999
4313
|
return ChatMessageSchema.parse({
|
|
4000
4314
|
...base,
|
|
4001
4315
|
role: "assistant",
|
|
4002
4316
|
kind: planContent ? "plan" : void 0,
|
|
4003
|
-
content: planContent ??
|
|
4004
|
-
details: planContent ? { raw: agentItem.text } :
|
|
4317
|
+
content: planContent ?? messageParts.content,
|
|
4318
|
+
details: planContent ? { raw: agentItem.text } : messageParts.details
|
|
4005
4319
|
});
|
|
4006
4320
|
}
|
|
4007
4321
|
case "reasoning": {
|
|
@@ -4115,6 +4429,11 @@ function appServerTurnShell(turnId) {
|
|
|
4115
4429
|
};
|
|
4116
4430
|
}
|
|
4117
4431
|
function mapAppServerModel(model) {
|
|
4432
|
+
const serviceTiers = model.serviceTiers ?? model.additionalSpeedTiers?.map((tier) => ({
|
|
4433
|
+
id: tier,
|
|
4434
|
+
name: tier === "fast" ? "Fast" : tier,
|
|
4435
|
+
description: void 0
|
|
4436
|
+
})) ?? [];
|
|
4118
4437
|
return {
|
|
4119
4438
|
id: model.id,
|
|
4120
4439
|
model: model.model,
|
|
@@ -4122,7 +4441,8 @@ function mapAppServerModel(model) {
|
|
|
4122
4441
|
description: model.description,
|
|
4123
4442
|
isDefault: Boolean(model.isDefault),
|
|
4124
4443
|
defaultReasoningEffort: model.defaultReasoningEffort,
|
|
4125
|
-
supportedReasoningEfforts: model.supportedReasoningEfforts?.map((effort) => effort.reasoningEffort) ?? []
|
|
4444
|
+
supportedReasoningEfforts: model.supportedReasoningEfforts?.map((effort) => effort.reasoningEffort) ?? [],
|
|
4445
|
+
serviceTiers
|
|
4126
4446
|
};
|
|
4127
4447
|
}
|
|
4128
4448
|
function normalizeRateLimitBuckets(rateLimits) {
|
|
@@ -4183,7 +4503,13 @@ function fallbackModels() {
|
|
|
4183
4503
|
{ reasoningEffort: "medium" },
|
|
4184
4504
|
{ reasoningEffort: "high" },
|
|
4185
4505
|
{ reasoningEffort: "xhigh" }
|
|
4186
|
-
]
|
|
4506
|
+
],
|
|
4507
|
+
additionalSpeedTiers: ["fast"],
|
|
4508
|
+
serviceTiers: [{
|
|
4509
|
+
id: "priority",
|
|
4510
|
+
name: "Fast",
|
|
4511
|
+
description: "1.5x speed, increased usage"
|
|
4512
|
+
}]
|
|
4187
4513
|
}];
|
|
4188
4514
|
}
|
|
4189
4515
|
function mapAppServerThreadState(status, turns) {
|
|
@@ -4887,53 +5213,253 @@ async function git(cwd, args) {
|
|
|
4887
5213
|
});
|
|
4888
5214
|
return stdout.trimEnd();
|
|
4889
5215
|
}
|
|
4890
|
-
async function listWorkspaceFiles(workspacePath, query) {
|
|
5216
|
+
async function listWorkspaceFiles(workspacePath, query, directory) {
|
|
4891
5217
|
const normalizedQuery = query.toLowerCase();
|
|
4892
|
-
const
|
|
5218
|
+
const isIgnored = await workspaceIgnoreMatcher(workspacePath);
|
|
5219
|
+
if (directory && isWorkspacePathIgnored(directory, isIgnored)) return [];
|
|
5220
|
+
const filePaths = await workspaceFilePaths(workspacePath, isIgnored);
|
|
4893
5221
|
const entriesByPath = /* @__PURE__ */ new Map();
|
|
5222
|
+
for (const entry of await workspaceDirectoryEntries(workspacePath, directory, isIgnored)) entriesByPath.set(entry.path, entry);
|
|
4894
5223
|
for (const path of filePaths) {
|
|
4895
5224
|
if (!path || path.startsWith("../") || path.includes("/.git/")) continue;
|
|
4896
|
-
|
|
4897
|
-
|
|
5225
|
+
if (!directory && normalizedQuery) {
|
|
5226
|
+
entriesByPath.set(path, {
|
|
5227
|
+
directory: dirname(path) === "." ? "" : dirname(path),
|
|
5228
|
+
kind: "file",
|
|
5229
|
+
name: path.split("/").pop() ?? path,
|
|
5230
|
+
path
|
|
5231
|
+
});
|
|
5232
|
+
const parts = path.split("/");
|
|
5233
|
+
for (let index = 1; index < parts.length; index += 1) {
|
|
5234
|
+
const directoryPath = parts.slice(0, index).join("/");
|
|
5235
|
+
if (isWorkspacePathIgnored(directoryPath, isIgnored)) continue;
|
|
5236
|
+
entriesByPath.set(directoryPath, {
|
|
5237
|
+
directory: dirname(directoryPath) === "." ? "" : dirname(directoryPath),
|
|
5238
|
+
kind: "directory",
|
|
5239
|
+
name: parts[index - 1],
|
|
5240
|
+
path: directoryPath
|
|
5241
|
+
});
|
|
5242
|
+
}
|
|
5243
|
+
continue;
|
|
5244
|
+
}
|
|
5245
|
+
const prefix = directory ? `${directory}/` : "";
|
|
5246
|
+
if (!path.startsWith(prefix)) continue;
|
|
5247
|
+
const childParts = path.slice(prefix.length).split("/").filter(Boolean);
|
|
5248
|
+
if (childParts.length === 0) continue;
|
|
5249
|
+
if (childParts.length === 1) entriesByPath.set(path, {
|
|
5250
|
+
directory,
|
|
4898
5251
|
kind: "file",
|
|
4899
|
-
name:
|
|
5252
|
+
name: childParts[0],
|
|
4900
5253
|
path
|
|
4901
5254
|
});
|
|
4902
|
-
|
|
4903
|
-
|
|
4904
|
-
|
|
5255
|
+
else {
|
|
5256
|
+
const directoryPath = [...directory ? directory.split("/") : [], childParts[0]].join("/");
|
|
5257
|
+
if (isWorkspacePathIgnored(directoryPath, isIgnored)) continue;
|
|
4905
5258
|
entriesByPath.set(directoryPath, {
|
|
4906
|
-
directory
|
|
5259
|
+
directory,
|
|
4907
5260
|
kind: "directory",
|
|
4908
|
-
name:
|
|
5261
|
+
name: childParts[0],
|
|
4909
5262
|
path: directoryPath
|
|
4910
5263
|
});
|
|
4911
5264
|
}
|
|
4912
5265
|
}
|
|
4913
5266
|
return [...entriesByPath.values()].filter((entry) => {
|
|
4914
5267
|
if (!normalizedQuery) return true;
|
|
4915
|
-
return entry.path.toLowerCase().includes(normalizedQuery);
|
|
5268
|
+
return entry.name.toLowerCase().includes(normalizedQuery) || entry.path.toLowerCase().includes(normalizedQuery);
|
|
4916
5269
|
}).sort((left, right) => {
|
|
4917
5270
|
const leftScore = workspaceFileScore(left.path, normalizedQuery);
|
|
4918
5271
|
const rightScore = workspaceFileScore(right.path, normalizedQuery);
|
|
4919
5272
|
if (leftScore !== rightScore) return leftScore - rightScore;
|
|
4920
5273
|
if (left.kind !== right.kind) return left.kind === "directory" ? -1 : 1;
|
|
4921
5274
|
return left.path.localeCompare(right.path);
|
|
4922
|
-
}).slice(0, 80);
|
|
5275
|
+
}).slice(0, directory || !normalizedQuery ? 160 : 80);
|
|
5276
|
+
}
|
|
5277
|
+
async function readWorkspaceFileContent(workspacePath, requestedPath) {
|
|
5278
|
+
const relativePath = normalizeWorkspaceRelativePath(requestedPath);
|
|
5279
|
+
if (!relativePath.success) return {
|
|
5280
|
+
code: "invalid_workspace_file_path",
|
|
5281
|
+
error: relativePath.error,
|
|
5282
|
+
status: 400,
|
|
5283
|
+
success: false
|
|
5284
|
+
};
|
|
5285
|
+
const rootPath = resolve(workspacePath);
|
|
5286
|
+
const absolutePath = resolve(rootPath, relativePath.path);
|
|
5287
|
+
if (!isPathInside(rootPath, absolutePath)) return {
|
|
5288
|
+
code: "invalid_workspace_file_path",
|
|
5289
|
+
error: "Workspace file path must stay inside the workspace.",
|
|
5290
|
+
status: 400,
|
|
5291
|
+
success: false
|
|
5292
|
+
};
|
|
5293
|
+
const fileStat = await stat(absolutePath).catch(() => null);
|
|
5294
|
+
if (!fileStat?.isFile()) return {
|
|
5295
|
+
code: "workspace_file_not_found",
|
|
5296
|
+
error: "Workspace file was not found.",
|
|
5297
|
+
status: 404,
|
|
5298
|
+
success: false
|
|
5299
|
+
};
|
|
5300
|
+
const bytesToRead = Math.min(fileStat.size, WORKSPACE_FILE_PREVIEW_MAX_BYTES);
|
|
5301
|
+
const buffer = Buffer.alloc(bytesToRead);
|
|
5302
|
+
if (bytesToRead > 0) {
|
|
5303
|
+
const handle = await open(absolutePath, "r");
|
|
5304
|
+
try {
|
|
5305
|
+
await handle.read(buffer, 0, bytesToRead, 0);
|
|
5306
|
+
} finally {
|
|
5307
|
+
await handle.close();
|
|
5308
|
+
}
|
|
5309
|
+
}
|
|
5310
|
+
const binary = buffer.includes(0);
|
|
5311
|
+
return {
|
|
5312
|
+
content: {
|
|
5313
|
+
binary,
|
|
5314
|
+
content: binary ? "" : buffer.toString("utf8"),
|
|
5315
|
+
directory: dirname(relativePath.path) === "." ? "" : dirname(relativePath.path),
|
|
5316
|
+
language: languageFromWorkspaceFile(relativePath.path),
|
|
5317
|
+
name: basename(relativePath.path),
|
|
5318
|
+
path: relativePath.path,
|
|
5319
|
+
size: fileStat.size,
|
|
5320
|
+
truncated: fileStat.size > WORKSPACE_FILE_PREVIEW_MAX_BYTES
|
|
5321
|
+
},
|
|
5322
|
+
success: true
|
|
5323
|
+
};
|
|
5324
|
+
}
|
|
5325
|
+
async function updateWorkspaceFileContent(workspacePath, input) {
|
|
5326
|
+
const relativePath = normalizeWorkspaceRelativePath(input.path);
|
|
5327
|
+
if (!relativePath.success) return {
|
|
5328
|
+
code: "invalid_workspace_file_path",
|
|
5329
|
+
error: relativePath.error,
|
|
5330
|
+
status: 400,
|
|
5331
|
+
success: false
|
|
5332
|
+
};
|
|
5333
|
+
const rootPath = resolve(workspacePath);
|
|
5334
|
+
const absolutePath = resolve(rootPath, relativePath.path);
|
|
5335
|
+
if (!isPathInside(rootPath, absolutePath)) return {
|
|
5336
|
+
code: "invalid_workspace_file_path",
|
|
5337
|
+
error: "Workspace file path must stay inside the workspace.",
|
|
5338
|
+
status: 400,
|
|
5339
|
+
success: false
|
|
5340
|
+
};
|
|
5341
|
+
if (!(await stat(absolutePath).catch(() => null))?.isFile()) return {
|
|
5342
|
+
code: "workspace_file_not_found",
|
|
5343
|
+
error: "Workspace file was not found.",
|
|
5344
|
+
status: 404,
|
|
5345
|
+
success: false
|
|
5346
|
+
};
|
|
5347
|
+
await writeFile(absolutePath, input.content, "utf8");
|
|
5348
|
+
return readWorkspaceFileContent(workspacePath, relativePath.path);
|
|
5349
|
+
}
|
|
5350
|
+
function normalizeWorkspaceRelativePath(requestedPath) {
|
|
5351
|
+
const normalized = requestedPath.trim().replaceAll("\\", "/").replace(/^\.\/+/, "");
|
|
5352
|
+
if (!normalized) return {
|
|
5353
|
+
error: "Workspace file path is required.",
|
|
5354
|
+
success: false
|
|
5355
|
+
};
|
|
5356
|
+
if (isAbsolute(normalized)) return {
|
|
5357
|
+
error: "Workspace file path must be relative.",
|
|
5358
|
+
success: false
|
|
5359
|
+
};
|
|
5360
|
+
const segments = normalized.split("/").filter(Boolean);
|
|
5361
|
+
if (segments.some((segment) => segment === "." || segment === ".." || segment === ".git")) return {
|
|
5362
|
+
error: "Workspace file path contains an unsupported segment.",
|
|
5363
|
+
success: false
|
|
5364
|
+
};
|
|
5365
|
+
return {
|
|
5366
|
+
path: segments.join("/"),
|
|
5367
|
+
success: true
|
|
5368
|
+
};
|
|
5369
|
+
}
|
|
5370
|
+
function normalizeWorkspaceDirectoryPath(requestedPath) {
|
|
5371
|
+
const normalized = requestedPath.trim().replaceAll("\\", "/").replace(/^\.\/+/, "").replace(/\/+$/, "");
|
|
5372
|
+
if (!normalized) return {
|
|
5373
|
+
path: "",
|
|
5374
|
+
success: true
|
|
5375
|
+
};
|
|
5376
|
+
if (isAbsolute(normalized)) return {
|
|
5377
|
+
error: "Workspace directory path must be relative.",
|
|
5378
|
+
success: false
|
|
5379
|
+
};
|
|
5380
|
+
const segments = normalized.split("/").filter(Boolean);
|
|
5381
|
+
if (segments.some((segment) => segment === "." || segment === ".." || segment === ".git")) return {
|
|
5382
|
+
error: "Workspace directory path contains an unsupported segment.",
|
|
5383
|
+
success: false
|
|
5384
|
+
};
|
|
5385
|
+
return {
|
|
5386
|
+
path: segments.join("/"),
|
|
5387
|
+
success: true
|
|
5388
|
+
};
|
|
4923
5389
|
}
|
|
4924
|
-
|
|
5390
|
+
function parentWorkspaceDirectory(directory) {
|
|
5391
|
+
if (!directory) return null;
|
|
5392
|
+
const parent = dirname(directory);
|
|
5393
|
+
return parent === "." ? "" : parent;
|
|
5394
|
+
}
|
|
5395
|
+
function isPathInside(rootPath, targetPath) {
|
|
5396
|
+
const pathFromRoot = relative(rootPath, targetPath);
|
|
5397
|
+
return Boolean(pathFromRoot) && !pathFromRoot.startsWith("..") && !isAbsolute(pathFromRoot);
|
|
5398
|
+
}
|
|
5399
|
+
function languageFromWorkspaceFile(path) {
|
|
5400
|
+
const name = basename(path).toLowerCase();
|
|
5401
|
+
const extension = extname(name).replace(/^\./, "");
|
|
5402
|
+
if (name === "dockerfile") return "dockerfile";
|
|
5403
|
+
if (name === "makefile") return "make";
|
|
5404
|
+
if (name === "package.json") return "json";
|
|
5405
|
+
return {
|
|
5406
|
+
cjs: "javascript",
|
|
5407
|
+
css: "css",
|
|
5408
|
+
diff: "diff",
|
|
5409
|
+
go: "go",
|
|
5410
|
+
htm: "html",
|
|
5411
|
+
html: "html",
|
|
5412
|
+
java: "java",
|
|
5413
|
+
js: "javascript",
|
|
5414
|
+
json: "json",
|
|
5415
|
+
jsx: "jsx",
|
|
5416
|
+
kt: "kotlin",
|
|
5417
|
+
lock: "yaml",
|
|
5418
|
+
log: "text",
|
|
5419
|
+
md: "markdown",
|
|
5420
|
+
mdx: "markdown",
|
|
5421
|
+
mjs: "javascript",
|
|
5422
|
+
plist: "xml",
|
|
5423
|
+
py: "python",
|
|
5424
|
+
rb: "ruby",
|
|
5425
|
+
rs: "rust",
|
|
5426
|
+
sh: "bash",
|
|
5427
|
+
swift: "swift",
|
|
5428
|
+
ts: "typescript",
|
|
5429
|
+
tsx: "tsx",
|
|
5430
|
+
txt: "text",
|
|
5431
|
+
xml: "xml",
|
|
5432
|
+
yaml: "yaml",
|
|
5433
|
+
yml: "yaml"
|
|
5434
|
+
}[extension] ?? extension;
|
|
5435
|
+
}
|
|
5436
|
+
async function workspaceFilePaths(workspacePath, isIgnored) {
|
|
4925
5437
|
try {
|
|
4926
5438
|
return (await git(workspacePath, [
|
|
4927
5439
|
"ls-files",
|
|
4928
5440
|
"--cached",
|
|
4929
5441
|
"--others",
|
|
4930
5442
|
"--exclude-standard"
|
|
4931
|
-
])).split("\n").filter(Boolean);
|
|
5443
|
+
])).split("\n").filter(Boolean).filter((path) => !isWorkspacePathIgnored(path, isIgnored));
|
|
4932
5444
|
} catch {
|
|
4933
|
-
return recursiveWorkspaceFilePaths(workspacePath);
|
|
5445
|
+
return recursiveWorkspaceFilePaths(workspacePath, isIgnored);
|
|
4934
5446
|
}
|
|
4935
5447
|
}
|
|
4936
|
-
async function
|
|
5448
|
+
async function workspaceDirectoryEntries(workspacePath, directory, isIgnored) {
|
|
5449
|
+
const rootPath = resolve(workspacePath);
|
|
5450
|
+
const absoluteDirectory = resolve(rootPath, directory);
|
|
5451
|
+
if (absoluteDirectory !== rootPath && !isPathInside(rootPath, absoluteDirectory)) return [];
|
|
5452
|
+
return (await readdir(absoluteDirectory, { withFileTypes: true })).filter((entry) => entry.isDirectory() && entry.name !== ".git").map((entry) => {
|
|
5453
|
+
const path = [...directory ? directory.split("/") : [], entry.name].join("/");
|
|
5454
|
+
return {
|
|
5455
|
+
directory,
|
|
5456
|
+
kind: "directory",
|
|
5457
|
+
name: entry.name,
|
|
5458
|
+
path
|
|
5459
|
+
};
|
|
5460
|
+
}).filter((entry) => !isWorkspacePathIgnored(entry.path, isIgnored));
|
|
5461
|
+
}
|
|
5462
|
+
async function recursiveWorkspaceFilePaths(rootPath, isIgnored) {
|
|
4937
5463
|
const ignoredDirectories = new Set([
|
|
4938
5464
|
".git",
|
|
4939
5465
|
".expo",
|
|
@@ -4947,17 +5473,56 @@ async function recursiveWorkspaceFilePaths(rootPath) {
|
|
|
4947
5473
|
for (const entry of entries) {
|
|
4948
5474
|
if (entry.name.startsWith(".") && entry.name !== ".github") continue;
|
|
4949
5475
|
const absolutePath = resolve(directory, entry.name);
|
|
5476
|
+
const relativePath = relative(rootPath, absolutePath).split("\\").join("/");
|
|
5477
|
+
if (isWorkspacePathIgnored(relativePath, isIgnored)) continue;
|
|
4950
5478
|
if (entry.isDirectory()) {
|
|
4951
5479
|
if (ignoredDirectories.has(entry.name)) continue;
|
|
4952
5480
|
await visit(absolutePath);
|
|
4953
5481
|
continue;
|
|
4954
5482
|
}
|
|
4955
|
-
if (entry.isFile()) results.push(
|
|
5483
|
+
if (entry.isFile()) results.push(relativePath);
|
|
4956
5484
|
}
|
|
4957
5485
|
}
|
|
4958
5486
|
await visit(rootPath);
|
|
4959
5487
|
return results;
|
|
4960
5488
|
}
|
|
5489
|
+
async function workspaceIgnoreMatcher(workspacePath) {
|
|
5490
|
+
const matchers = (await readFile(join(workspacePath, ".gitignore"), "utf8").catch(() => "")).split(/\r?\n/).map((line) => gitignorePatternMatcher(line)).filter((matcher) => Boolean(matcher));
|
|
5491
|
+
return (path) => matchers.some((matcher) => matcher(path.split("\\").join("/")));
|
|
5492
|
+
}
|
|
5493
|
+
function isWorkspacePathIgnored(path, isIgnored) {
|
|
5494
|
+
const normalizedPath = path.replace(/^\/+/, "").replaceAll("\\", "/").replace(/\/+$/, "");
|
|
5495
|
+
if (!normalizedPath) return false;
|
|
5496
|
+
if (isIgnored(normalizedPath) || isIgnored(`${normalizedPath}/`)) return true;
|
|
5497
|
+
const parts = normalizedPath.split("/");
|
|
5498
|
+
for (let index = 1; index < parts.length; index += 1) {
|
|
5499
|
+
const ancestorPath = parts.slice(0, index).join("/");
|
|
5500
|
+
if (isIgnored(ancestorPath) || isIgnored(`${ancestorPath}/`)) return true;
|
|
5501
|
+
}
|
|
5502
|
+
return false;
|
|
5503
|
+
}
|
|
5504
|
+
function gitignorePatternMatcher(line) {
|
|
5505
|
+
const trimmed = line.trim();
|
|
5506
|
+
if (!trimmed || trimmed.startsWith("#") || trimmed.startsWith("!")) return;
|
|
5507
|
+
const directoryOnly = trimmed.endsWith("/");
|
|
5508
|
+
const anchored = trimmed.startsWith("/");
|
|
5509
|
+
const pattern = trimmed.replace(/^\/+/, "").replace(/\/+$/, "");
|
|
5510
|
+
if (!pattern) return;
|
|
5511
|
+
const matcher = gitignoreGlobMatcher(pattern);
|
|
5512
|
+
const hasSlash = pattern.includes("/");
|
|
5513
|
+
return (path) => {
|
|
5514
|
+
const normalizedPath = path.replace(/^\/+/, "");
|
|
5515
|
+
return (anchored || hasSlash ? [normalizedPath] : normalizedPath.split("/")).some((candidate) => {
|
|
5516
|
+
if (directoryOnly) return matcher(candidate);
|
|
5517
|
+
return matcher(candidate);
|
|
5518
|
+
});
|
|
5519
|
+
};
|
|
5520
|
+
}
|
|
5521
|
+
function gitignoreGlobMatcher(pattern) {
|
|
5522
|
+
const expression = pattern.split("**").map((part) => part.replace(/[.+^${}()|[\]\\]/g, "\\$&").replace(/\*/g, "[^/]*")).join(".*").replace(/\?/g, "[^/]");
|
|
5523
|
+
const regex = new RegExp(`^${expression}(?:/.*)?$`);
|
|
5524
|
+
return (path) => regex.test(path);
|
|
5525
|
+
}
|
|
4961
5526
|
function workspaceFileScore(path, query) {
|
|
4962
5527
|
if (!query) return path.split("/").length;
|
|
4963
5528
|
const lowerPath = path.toLowerCase();
|
|
@@ -4982,238 +5547,6 @@ function errorMessage(error) {
|
|
|
4982
5547
|
return error instanceof Error ? error.message : "Codex run failed.";
|
|
4983
5548
|
}
|
|
4984
5549
|
//#endregion
|
|
4985
|
-
//#region src/pairing-store.ts
|
|
4986
|
-
async function createTursoPairingSessionStore(path) {
|
|
4987
|
-
if (path !== ":memory:") await mkdir(dirname(path), { recursive: true });
|
|
4988
|
-
const db = await connect(path);
|
|
4989
|
-
await db.exec(`
|
|
4990
|
-
CREATE TABLE IF NOT EXISTS pairing_sessions (
|
|
4991
|
-
token_hash TEXT PRIMARY KEY,
|
|
4992
|
-
client_session_id TEXT,
|
|
4993
|
-
client_name TEXT,
|
|
4994
|
-
expires_at INTEGER NOT NULL,
|
|
4995
|
-
key_epoch INTEGER,
|
|
4996
|
-
mobile_to_server_key TEXT,
|
|
4997
|
-
server_to_mobile_key TEXT,
|
|
4998
|
-
last_mobile_counter INTEGER,
|
|
4999
|
-
next_server_counter INTEGER,
|
|
5000
|
-
created_at INTEGER NOT NULL,
|
|
5001
|
-
updated_at INTEGER NOT NULL
|
|
5002
|
-
);
|
|
5003
|
-
|
|
5004
|
-
CREATE TABLE IF NOT EXISTS pending_pairings (
|
|
5005
|
-
approval_code TEXT PRIMARY KEY,
|
|
5006
|
-
client_session_id TEXT,
|
|
5007
|
-
client_name TEXT,
|
|
5008
|
-
client_ephemeral_public_key TEXT NOT NULL,
|
|
5009
|
-
client_nonce TEXT NOT NULL,
|
|
5010
|
-
server_url TEXT NOT NULL,
|
|
5011
|
-
approved INTEGER NOT NULL DEFAULT 0,
|
|
5012
|
-
expires_at INTEGER NOT NULL,
|
|
5013
|
-
created_at INTEGER NOT NULL,
|
|
5014
|
-
updated_at INTEGER NOT NULL
|
|
5015
|
-
);
|
|
5016
|
-
`);
|
|
5017
|
-
await ensurePairingSessionColumns();
|
|
5018
|
-
async function countActive(now) {
|
|
5019
|
-
const row = await db.prepare("SELECT COUNT(DISTINCT COALESCE(client_session_id, token_hash)) AS count FROM pairing_sessions WHERE expires_at > ?").get(now);
|
|
5020
|
-
return Number(row?.count ?? 0);
|
|
5021
|
-
}
|
|
5022
|
-
async function deleteSession(tokenHash) {
|
|
5023
|
-
await db.prepare("DELETE FROM pairing_sessions WHERE token_hash = ?").run(tokenHash);
|
|
5024
|
-
}
|
|
5025
|
-
async function deletePendingPairing(approvalCode) {
|
|
5026
|
-
await db.prepare("DELETE FROM pending_pairings WHERE approval_code = ?").run(approvalCode);
|
|
5027
|
-
}
|
|
5028
|
-
async function getPendingPairing(approvalCode, now) {
|
|
5029
|
-
const row = await db.prepare(`SELECT approval_code AS approvalCode,
|
|
5030
|
-
client_session_id AS clientSessionId,
|
|
5031
|
-
client_name AS clientName,
|
|
5032
|
-
client_ephemeral_public_key AS clientEphemeralPublicKey,
|
|
5033
|
-
client_nonce AS clientNonce,
|
|
5034
|
-
server_url AS serverUrl,
|
|
5035
|
-
approved,
|
|
5036
|
-
expires_at AS expiresAt
|
|
5037
|
-
FROM pending_pairings
|
|
5038
|
-
WHERE approval_code = ?`).get(approvalCode);
|
|
5039
|
-
if (!row) return;
|
|
5040
|
-
const expiresAt = Number(row.expiresAt);
|
|
5041
|
-
if (now > expiresAt) {
|
|
5042
|
-
await deletePendingPairing(approvalCode);
|
|
5043
|
-
return;
|
|
5044
|
-
}
|
|
5045
|
-
return {
|
|
5046
|
-
approvalCode: String(row.approvalCode),
|
|
5047
|
-
approved: Number(row.approved) === 1,
|
|
5048
|
-
clientEphemeralPublicKey: String(row.clientEphemeralPublicKey),
|
|
5049
|
-
clientSessionId: typeof row.clientSessionId === "string" ? row.clientSessionId : void 0,
|
|
5050
|
-
clientName: typeof row.clientName === "string" ? row.clientName : void 0,
|
|
5051
|
-
clientNonce: String(row.clientNonce),
|
|
5052
|
-
expiresAt,
|
|
5053
|
-
serverUrl: String(row.serverUrl)
|
|
5054
|
-
};
|
|
5055
|
-
}
|
|
5056
|
-
return {
|
|
5057
|
-
async approvePendingPairing(approvalCode, now) {
|
|
5058
|
-
const pending = await getPendingPairing(approvalCode, now);
|
|
5059
|
-
if (!pending) return;
|
|
5060
|
-
await db.prepare("UPDATE pending_pairings SET approved = 1, updated_at = ? WHERE approval_code = ?").run(now, approvalCode);
|
|
5061
|
-
return {
|
|
5062
|
-
...pending,
|
|
5063
|
-
approved: true
|
|
5064
|
-
};
|
|
5065
|
-
},
|
|
5066
|
-
countActive,
|
|
5067
|
-
async createPendingPairing(pairing) {
|
|
5068
|
-
const now = Date.now();
|
|
5069
|
-
await db.prepare(`INSERT INTO pending_pairings (
|
|
5070
|
-
approval_code,
|
|
5071
|
-
client_session_id,
|
|
5072
|
-
client_name,
|
|
5073
|
-
client_ephemeral_public_key,
|
|
5074
|
-
client_nonce,
|
|
5075
|
-
server_url,
|
|
5076
|
-
approved,
|
|
5077
|
-
expires_at,
|
|
5078
|
-
created_at,
|
|
5079
|
-
updated_at
|
|
5080
|
-
)
|
|
5081
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`).run(pairing.approvalCode, pairing.clientSessionId ?? null, pairing.clientName ?? null, pairing.clientEphemeralPublicKey, pairing.clientNonce, pairing.serverUrl, pairing.approved ? 1 : 0, pairing.expiresAt, now, now);
|
|
5082
|
-
},
|
|
5083
|
-
async createSession(tokenHash, session) {
|
|
5084
|
-
const now = Date.now();
|
|
5085
|
-
const secure = encodeSecureSession(session.secureSession);
|
|
5086
|
-
if (session.clientSessionId) {
|
|
5087
|
-
await db.prepare("DELETE FROM pairing_sessions WHERE client_session_id = ?").run(session.clientSessionId);
|
|
5088
|
-
if (session.clientName) await db.prepare("DELETE FROM pairing_sessions WHERE client_session_id IS NULL AND client_name = ?").run(session.clientName);
|
|
5089
|
-
}
|
|
5090
|
-
await db.prepare(`INSERT INTO pairing_sessions (
|
|
5091
|
-
token_hash,
|
|
5092
|
-
client_session_id,
|
|
5093
|
-
client_name,
|
|
5094
|
-
expires_at,
|
|
5095
|
-
key_epoch,
|
|
5096
|
-
mobile_to_server_key,
|
|
5097
|
-
server_to_mobile_key,
|
|
5098
|
-
last_mobile_counter,
|
|
5099
|
-
next_server_counter,
|
|
5100
|
-
created_at,
|
|
5101
|
-
updated_at
|
|
5102
|
-
)
|
|
5103
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`).run(tokenHash, session.clientSessionId ?? null, session.clientName ?? null, session.expiresAt, secure?.keyEpoch ?? null, secure?.mobileToServerKey ?? null, secure?.serverToMobileKey ?? null, secure?.lastMobileCounter ?? null, secure?.nextServerCounter ?? null, now, now);
|
|
5104
|
-
return countActive(now);
|
|
5105
|
-
},
|
|
5106
|
-
deleteSession,
|
|
5107
|
-
deletePendingPairing,
|
|
5108
|
-
getPendingPairing,
|
|
5109
|
-
async getValidSession(tokenHash, now) {
|
|
5110
|
-
const row = await db.prepare(`SELECT client_name AS clientName,
|
|
5111
|
-
client_session_id AS clientSessionId,
|
|
5112
|
-
expires_at AS expiresAt,
|
|
5113
|
-
key_epoch AS keyEpoch,
|
|
5114
|
-
mobile_to_server_key AS mobileToServerKey,
|
|
5115
|
-
server_to_mobile_key AS serverToMobileKey,
|
|
5116
|
-
last_mobile_counter AS lastMobileCounter,
|
|
5117
|
-
next_server_counter AS nextServerCounter
|
|
5118
|
-
FROM pairing_sessions
|
|
5119
|
-
WHERE token_hash = ?`).get(tokenHash);
|
|
5120
|
-
if (!row) return;
|
|
5121
|
-
const expiresAt = Number(row.expiresAt);
|
|
5122
|
-
if (now > expiresAt) {
|
|
5123
|
-
await deleteSession(tokenHash);
|
|
5124
|
-
return;
|
|
5125
|
-
}
|
|
5126
|
-
return {
|
|
5127
|
-
clientSessionId: typeof row.clientSessionId === "string" ? row.clientSessionId : void 0,
|
|
5128
|
-
clientName: typeof row.clientName === "string" ? row.clientName : void 0,
|
|
5129
|
-
expiresAt,
|
|
5130
|
-
secureSession: decodeSecureSession(row)
|
|
5131
|
-
};
|
|
5132
|
-
},
|
|
5133
|
-
async pruneExpired(now) {
|
|
5134
|
-
await db.prepare("DELETE FROM pairing_sessions WHERE expires_at <= ?").run(now);
|
|
5135
|
-
await db.prepare("DELETE FROM pending_pairings WHERE expires_at <= ?").run(now);
|
|
5136
|
-
},
|
|
5137
|
-
async rotateSession(oldTokenHash, newTokenHash, session) {
|
|
5138
|
-
const now = Date.now();
|
|
5139
|
-
const secure = encodeSecureSession(session.secureSession);
|
|
5140
|
-
await db.transaction(async () => {
|
|
5141
|
-
await db.prepare("DELETE FROM pairing_sessions WHERE token_hash = ?").run(oldTokenHash);
|
|
5142
|
-
if (session.clientSessionId) {
|
|
5143
|
-
await db.prepare("DELETE FROM pairing_sessions WHERE client_session_id = ?").run(session.clientSessionId);
|
|
5144
|
-
if (session.clientName) await db.prepare("DELETE FROM pairing_sessions WHERE client_session_id IS NULL AND client_name = ?").run(session.clientName);
|
|
5145
|
-
}
|
|
5146
|
-
await db.prepare(`INSERT INTO pairing_sessions (
|
|
5147
|
-
token_hash,
|
|
5148
|
-
client_session_id,
|
|
5149
|
-
client_name,
|
|
5150
|
-
expires_at,
|
|
5151
|
-
key_epoch,
|
|
5152
|
-
mobile_to_server_key,
|
|
5153
|
-
server_to_mobile_key,
|
|
5154
|
-
last_mobile_counter,
|
|
5155
|
-
next_server_counter,
|
|
5156
|
-
created_at,
|
|
5157
|
-
updated_at
|
|
5158
|
-
)
|
|
5159
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`).run(newTokenHash, session.clientSessionId ?? null, session.clientName ?? null, session.expiresAt, secure?.keyEpoch ?? null, secure?.mobileToServerKey ?? null, secure?.serverToMobileKey ?? null, secure?.lastMobileCounter ?? null, secure?.nextServerCounter ?? null, now, now);
|
|
5160
|
-
})();
|
|
5161
|
-
return countActive(now);
|
|
5162
|
-
},
|
|
5163
|
-
async updateSecureSession(tokenHash, secureSession) {
|
|
5164
|
-
const secure = encodeSecureSession(secureSession);
|
|
5165
|
-
const now = Date.now();
|
|
5166
|
-
await db.prepare(`UPDATE pairing_sessions
|
|
5167
|
-
SET key_epoch = ?,
|
|
5168
|
-
mobile_to_server_key = ?,
|
|
5169
|
-
server_to_mobile_key = ?,
|
|
5170
|
-
last_mobile_counter = ?,
|
|
5171
|
-
next_server_counter = ?,
|
|
5172
|
-
updated_at = ?
|
|
5173
|
-
WHERE token_hash = ?`).run(secure.keyEpoch, secure.mobileToServerKey, secure.serverToMobileKey, secure.lastMobileCounter, secure.nextServerCounter, now, tokenHash);
|
|
5174
|
-
}
|
|
5175
|
-
};
|
|
5176
|
-
async function ensurePairingSessionColumns() {
|
|
5177
|
-
const rows = await db.prepare("PRAGMA table_info(pairing_sessions)").all();
|
|
5178
|
-
const columns = new Set(resultRows(rows).map((row) => String(row.name)));
|
|
5179
|
-
for (const [column, sql] of [
|
|
5180
|
-
["client_session_id", "ALTER TABLE pairing_sessions ADD COLUMN client_session_id TEXT"],
|
|
5181
|
-
["key_epoch", "ALTER TABLE pairing_sessions ADD COLUMN key_epoch INTEGER"],
|
|
5182
|
-
["mobile_to_server_key", "ALTER TABLE pairing_sessions ADD COLUMN mobile_to_server_key TEXT"],
|
|
5183
|
-
["server_to_mobile_key", "ALTER TABLE pairing_sessions ADD COLUMN server_to_mobile_key TEXT"],
|
|
5184
|
-
["last_mobile_counter", "ALTER TABLE pairing_sessions ADD COLUMN last_mobile_counter INTEGER"],
|
|
5185
|
-
["next_server_counter", "ALTER TABLE pairing_sessions ADD COLUMN next_server_counter INTEGER"]
|
|
5186
|
-
]) if (!columns.has(column)) await db.exec(sql);
|
|
5187
|
-
const pendingRows = await db.prepare("PRAGMA table_info(pending_pairings)").all();
|
|
5188
|
-
if (!new Set(resultRows(pendingRows).map((row) => String(row.name))).has("client_session_id")) await db.exec("ALTER TABLE pending_pairings ADD COLUMN client_session_id TEXT");
|
|
5189
|
-
}
|
|
5190
|
-
}
|
|
5191
|
-
function encodeSecureSession(session) {
|
|
5192
|
-
if (!session) return;
|
|
5193
|
-
return {
|
|
5194
|
-
keyEpoch: session.keyEpoch,
|
|
5195
|
-
lastMobileCounter: session.lastMobileCounter,
|
|
5196
|
-
mobileToServerKey: fromByteArray(session.mobileToServerKey),
|
|
5197
|
-
nextServerCounter: session.nextServerCounter,
|
|
5198
|
-
serverToMobileKey: fromByteArray(session.serverToMobileKey)
|
|
5199
|
-
};
|
|
5200
|
-
}
|
|
5201
|
-
function decodeSecureSession(row) {
|
|
5202
|
-
if (typeof row.mobileToServerKey !== "string" || typeof row.serverToMobileKey !== "string" || row.keyEpoch === null || row.lastMobileCounter === null || row.nextServerCounter === null) return;
|
|
5203
|
-
return {
|
|
5204
|
-
keyEpoch: Number(row.keyEpoch),
|
|
5205
|
-
lastMobileCounter: Number(row.lastMobileCounter),
|
|
5206
|
-
mobileToServerKey: toByteArray(row.mobileToServerKey),
|
|
5207
|
-
nextServerCounter: Number(row.nextServerCounter),
|
|
5208
|
-
serverToMobileKey: toByteArray(row.serverToMobileKey)
|
|
5209
|
-
};
|
|
5210
|
-
}
|
|
5211
|
-
function resultRows(result) {
|
|
5212
|
-
if (Array.isArray(result)) return result;
|
|
5213
|
-
if (result && typeof result === "object" && Array.isArray(result.rows)) return result.rows;
|
|
5214
|
-
return [];
|
|
5215
|
-
}
|
|
5216
|
-
//#endregion
|
|
5217
5550
|
//#region src/index.ts
|
|
5218
5551
|
const port = Number(process.env.PORT ?? 8787);
|
|
5219
5552
|
const hostname$1 = process.env.HOST ?? "0.0.0.0";
|
|
@@ -5264,6 +5597,9 @@ serve({
|
|
|
5264
5597
|
onPairApproved: ({ clientName }) => {
|
|
5265
5598
|
logRuntimeEvent("Approved", `Pairing request approved${clientName ? ` for ${clientName}` : ""}. Waiting for secure session pickup.`);
|
|
5266
5599
|
},
|
|
5600
|
+
onPairingsCleared: ({ pendingPairingsCleared, sessionsCleared }) => {
|
|
5601
|
+
logRuntimeEvent("Cleared", `Signed out ${sessionsCleared} mobile session${sessionsCleared === 1 ? "" : "s"} and removed ${pendingPairingsCleared} pending pairing request${pendingPairingsCleared === 1 ? "" : "s"}.`);
|
|
5602
|
+
},
|
|
5267
5603
|
onTokenRefreshed: ({ clientName, tokenCount }) => {
|
|
5268
5604
|
logRuntimeEvent("Refreshed", `Mobile session rotated${clientName ? ` for ${clientName}` : ""}; ${formatClientCount(tokenCount)} active.`);
|
|
5269
5605
|
}
|
|
@@ -5274,10 +5610,19 @@ serve({
|
|
|
5274
5610
|
port
|
|
5275
5611
|
}, (info) => {
|
|
5276
5612
|
const listenUrl = `http://${info.address}:${info.port}`;
|
|
5277
|
-
const
|
|
5278
|
-
|
|
5613
|
+
const connectUrlCandidates = getConnectUrlCandidates({
|
|
5614
|
+
listenUrl,
|
|
5615
|
+
port: info.port
|
|
5616
|
+
});
|
|
5617
|
+
const connectUrl = connectUrlCandidates[0]?.url ?? listenUrl;
|
|
5618
|
+
const connectUrls = connectUrlCandidates.map((candidate) => candidate.url);
|
|
5619
|
+
const pairingPayload = createPairingQrPayload({
|
|
5620
|
+
serverPublicKey: serverIdentity.publicKey,
|
|
5621
|
+
serverUrls: connectUrls.length > 0 ? connectUrls : [connectUrl]
|
|
5622
|
+
});
|
|
5279
5623
|
writeServerState({
|
|
5280
5624
|
connectUrl,
|
|
5625
|
+
connectUrlCandidates,
|
|
5281
5626
|
host: hostname$1,
|
|
5282
5627
|
listenUrl,
|
|
5283
5628
|
pairingPayload,
|
|
@@ -5288,6 +5633,7 @@ serve({
|
|
|
5288
5633
|
logRuntimeEvent("Debug", `Writing diagnostics to ${debugLogPath}`);
|
|
5289
5634
|
relayDebugLog("relay.started", {
|
|
5290
5635
|
connectUrl,
|
|
5636
|
+
connectUrlCandidates,
|
|
5291
5637
|
listenUrl,
|
|
5292
5638
|
port: info.port,
|
|
5293
5639
|
workspacePath: process.env.CODEX_RELAY_WORKSPACE_PATH ?? process.cwd()
|
|
@@ -5297,6 +5643,7 @@ serve({
|
|
|
5297
5643
|
qrcode.generate(pairingPayload, { small: true });
|
|
5298
5644
|
console.log(formatStartupInstructions({
|
|
5299
5645
|
connectUrl,
|
|
5646
|
+
connectUrlCandidates,
|
|
5300
5647
|
dangerouslyAutoApprove,
|
|
5301
5648
|
listenUrl,
|
|
5302
5649
|
pairingPayload,
|
|
@@ -5310,6 +5657,8 @@ function formatStartupInstructions(details) {
|
|
|
5310
5657
|
`${color.prompt("›")} Scan the QR code above to pair ${color.brand("Codex Relay mobile")}.`,
|
|
5311
5658
|
"",
|
|
5312
5659
|
`${color.prompt("›")} Mobile: ${color.url(details.connectUrl)}`,
|
|
5660
|
+
...formatConnectUrlGuidance(details.connectUrl),
|
|
5661
|
+
...formatConnectUrlCandidates(details.connectUrlCandidates),
|
|
5313
5662
|
`${color.prompt("›")} Server: ${color.muted(details.listenUrl)}`,
|
|
5314
5663
|
"",
|
|
5315
5664
|
`${color.prompt("›")} Pairing: ${color.url(details.pairingPayload)}`,
|
|
@@ -5326,34 +5675,23 @@ function formatStartupInstructions(details) {
|
|
|
5326
5675
|
""
|
|
5327
5676
|
].join("\n");
|
|
5328
5677
|
}
|
|
5678
|
+
function formatConnectUrlGuidance(connectUrl) {
|
|
5679
|
+
const guidance = getConnectUrlGuidance(connectUrl);
|
|
5680
|
+
return guidance ? [`${color.prompt("›")} Network: ${guidance}`] : [];
|
|
5681
|
+
}
|
|
5682
|
+
function formatConnectUrlCandidates(candidates) {
|
|
5683
|
+
if (candidates.length <= 1) return [];
|
|
5684
|
+
return [`${color.prompt("›")} QR includes ${candidates.length} candidate addresses; the app will use the first reachable one.`, ...candidates.slice(1).map((candidate) => ` ${color.muted(candidate.label)} ${color.url(candidate.url)}`)];
|
|
5685
|
+
}
|
|
5329
5686
|
function logRuntimeEvent(label, message) {
|
|
5330
5687
|
console.log(`${color.prompt("›")} ${color.event(label.padEnd(8))} ${message}`);
|
|
5331
5688
|
}
|
|
5332
5689
|
function formatClientCount(tokenCount) {
|
|
5333
5690
|
return `${tokenCount} client${tokenCount === 1 ? "" : "s"}`;
|
|
5334
5691
|
}
|
|
5335
|
-
function createPairingQrPayload(serverUrl) {
|
|
5336
|
-
const url = new URL("codex-relay://pair");
|
|
5337
|
-
url.searchParams.set("serverUrl", serverUrl);
|
|
5338
|
-
url.searchParams.set("serverPublicKey", serverIdentity.publicKey);
|
|
5339
|
-
return url.toString();
|
|
5340
|
-
}
|
|
5341
5692
|
function hashClientToken(token) {
|
|
5342
5693
|
return createHash("sha256").update(token).digest("base64url");
|
|
5343
5694
|
}
|
|
5344
|
-
function getConfiguredConnectUrl() {
|
|
5345
|
-
const configuredUrl = normalizeUrl(process.env.CODEX_RELAY_PUBLIC_URL);
|
|
5346
|
-
if (configuredUrl) return configuredUrl;
|
|
5347
|
-
}
|
|
5348
|
-
function getTailscaleConnectUrl(port) {
|
|
5349
|
-
const status = getTailscaleStatus();
|
|
5350
|
-
const tailscaleIp = status?.Self?.TailscaleIPs?.find((ip) => ip.startsWith("100.") && ip.includes("."));
|
|
5351
|
-
if (tailscaleIp) return `http://${tailscaleIp}:${port}`;
|
|
5352
|
-
const dnsName = status?.Self?.DNSName?.replace(/\.$/, "");
|
|
5353
|
-
if (dnsName) return getTailscaleServeHttpsUrl(dnsName, port) ?? `http://${dnsName}:${port}`;
|
|
5354
|
-
const tailscaleHost = status?.Self?.TailscaleIPs?.find((ip) => ip.includes("."));
|
|
5355
|
-
return tailscaleHost ? `http://${tailscaleHost}:${port}` : void 0;
|
|
5356
|
-
}
|
|
5357
5695
|
async function getApprovalSecret() {
|
|
5358
5696
|
if (process.env.CODEX_RELAY_APPROVAL_SECRET) return process.env.CODEX_RELAY_APPROVAL_SECRET;
|
|
5359
5697
|
const path = await prepareCodexRelayDataPath("approval-secret");
|
|
@@ -5406,58 +5744,5 @@ async function writeBackgroundPid() {
|
|
|
5406
5744
|
function formatApprovalCommand(approvalCode, activePort) {
|
|
5407
5745
|
return activePort === 8787 ? `${npxCommand} approve ${approvalCode}` : `PORT=${activePort} ${npxCommand} approve ${approvalCode}`;
|
|
5408
5746
|
}
|
|
5409
|
-
function getLocalNetworkConnectUrl(port) {
|
|
5410
|
-
for (const addresses of Object.values(networkInterfaces())) for (const address of addresses ?? []) if (address.family === "IPv4" && !address.internal) return `http://${address.address}:${port}`;
|
|
5411
|
-
}
|
|
5412
|
-
function normalizeUrl(value) {
|
|
5413
|
-
if (!value) return;
|
|
5414
|
-
const trimmed = value.trim().replace(/\/$/, "");
|
|
5415
|
-
if (!trimmed) return;
|
|
5416
|
-
try {
|
|
5417
|
-
const url = new URL(trimmed);
|
|
5418
|
-
return url.protocol === "http:" || url.protocol === "https:" ? url.toString().replace(/\/$/, "") : void 0;
|
|
5419
|
-
} catch {
|
|
5420
|
-
return;
|
|
5421
|
-
}
|
|
5422
|
-
}
|
|
5423
|
-
function getTailscaleStatus() {
|
|
5424
|
-
try {
|
|
5425
|
-
const output = execFileSync("tailscale", ["status", "--json"], {
|
|
5426
|
-
encoding: "utf8",
|
|
5427
|
-
stdio: [
|
|
5428
|
-
"ignore",
|
|
5429
|
-
"pipe",
|
|
5430
|
-
"ignore"
|
|
5431
|
-
],
|
|
5432
|
-
timeout: 1500
|
|
5433
|
-
});
|
|
5434
|
-
return JSON.parse(output);
|
|
5435
|
-
} catch {
|
|
5436
|
-
return;
|
|
5437
|
-
}
|
|
5438
|
-
}
|
|
5439
|
-
function getTailscaleServeHttpsUrl(dnsName, port) {
|
|
5440
|
-
try {
|
|
5441
|
-
const output = execFileSync("tailscale", [
|
|
5442
|
-
"serve",
|
|
5443
|
-
"status",
|
|
5444
|
-
"--json"
|
|
5445
|
-
], {
|
|
5446
|
-
encoding: "utf8",
|
|
5447
|
-
stdio: [
|
|
5448
|
-
"ignore",
|
|
5449
|
-
"pipe",
|
|
5450
|
-
"ignore"
|
|
5451
|
-
],
|
|
5452
|
-
timeout: 1500
|
|
5453
|
-
});
|
|
5454
|
-
const serveStatus = JSON.parse(output);
|
|
5455
|
-
const portKey = String(port);
|
|
5456
|
-
const hostPort = `${dnsName}:${portKey}`;
|
|
5457
|
-
return serveStatus.TCP?.[portKey]?.HTTPS && serveStatus.Web?.[hostPort] ? `https://${hostPort}` : void 0;
|
|
5458
|
-
} catch {
|
|
5459
|
-
return;
|
|
5460
|
-
}
|
|
5461
|
-
}
|
|
5462
5747
|
//#endregion
|
|
5463
5748
|
export {};
|