codex-relay 1.0.4 → 1.0.6
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/LICENSE +190 -0
- package/README.md +3 -2
- package/dist/api-schema.js +2 -2
- package/dist/api-schema2.js +41 -3
- package/dist/cli.js +97 -4
- package/dist/paths.js +425 -3
- package/dist/src.js +226 -321
- package/package.json +5 -3
- package/src/api-schema.ts +61 -1
package/dist/src.js
CHANGED
|
@@ -1,22 +1,23 @@
|
|
|
1
|
-
import { A as PairResponseSchema, C as ListQueuedThreadInputsResponseSchema, D as ListWorkspaceFilesResponseSchema, Dt as
|
|
2
|
-
import { r as legacyCodexRelayDataPath, t as codexRelayDataPath } from "./paths.js";
|
|
1
|
+
import { A as PairResponseSchema, At as WorkspaceTerminalResizeRequestSchema, C as ListQueuedThreadInputsResponseSchema, D as ListWorkspaceFilesResponseSchema, Dt as WorkspaceTerminalInputRequestSchema, E as ListWorkspaceDirectoriesResponseSchema, Ft as createOpenApiDocument, G as ResolveApprovalRequestSchema, K as ResolveApprovalResponseSchema, Lt as normalizePromptContext, Mt as WorkspaceTerminalStartRequestSchema, Nt as apiPaths, Pt as chatMessageDetailsFromPromptContext, Q as RuntimePreferencesSchema, Rt as promptMarkdownWithSkills, S as ListModelsResponseSchema, St as WorkspaceGitActionResponseSchema, T as ListThreadsResponseSchema, U as RateLimitsResponseSchema, Ut as stripPromptSkillMentions, 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 WorkspaceTerminalSessionResponseSchema, k as PairRequestSchema, kt as WorkspaceTerminalOutputResponseSchema, 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
7
|
import { access, appendFile, copyFile, mkdir, open, readFile, readdir, stat, writeFile } from "node:fs/promises";
|
|
8
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";
|
|
17
17
|
import { Hono } from "hono";
|
|
18
18
|
import { cors } from "hono/cors";
|
|
19
19
|
import { promisify } from "node:util";
|
|
20
|
+
import * as pty from "@lydell/node-pty";
|
|
20
21
|
import { createInterface } from "node:readline";
|
|
21
22
|
import { Codex } from "@openai/codex-sdk";
|
|
22
23
|
import { gcm } from "@noble/ciphers/aes.js";
|
|
@@ -24,7 +25,6 @@ import { randomBytes as randomBytes$1, utf8ToBytes } from "@noble/ciphers/utils.
|
|
|
24
25
|
import { ed25519, x25519 } from "@noble/curves/ed25519.js";
|
|
25
26
|
import { hkdf } from "@noble/hashes/hkdf.js";
|
|
26
27
|
import { sha256 } from "@noble/hashes/sha2.js";
|
|
27
|
-
import { connect } from "@tursodatabase/database";
|
|
28
28
|
//#region src/app-server.ts
|
|
29
29
|
var CodexAppServerClient = class {
|
|
30
30
|
child;
|
|
@@ -823,6 +823,7 @@ const defaultWebPreviewPorts = [
|
|
|
823
823
|
];
|
|
824
824
|
const PairApproveRequestSchema = z.object({ approvalCode: z.string().trim().min(1) });
|
|
825
825
|
const maxResolvedApprovals = 100;
|
|
826
|
+
const maxWorkspaceTerminalOutputChunks = 2e3;
|
|
826
827
|
function createApp(options = {}) {
|
|
827
828
|
const app = new Hono();
|
|
828
829
|
const appServer = options.appServer === void 0 ? process.env.VITEST ? null : new CodexAppServerClient() : options.appServer;
|
|
@@ -835,10 +836,12 @@ function createApp(options = {}) {
|
|
|
835
836
|
const pendingApprovals = /* @__PURE__ */ new Map();
|
|
836
837
|
const resolvedApprovals = /* @__PURE__ */ new Map();
|
|
837
838
|
const queuedInputsByThreadId = /* @__PURE__ */ new Map();
|
|
839
|
+
const workspaceTerminalSessions = /* @__PURE__ */ new Map();
|
|
838
840
|
const activeAppServerTurnIdsByThreadId = /* @__PURE__ */ new Map();
|
|
839
841
|
const appServerHistoryLoadsByThreadId = /* @__PURE__ */ new Map();
|
|
840
842
|
const steeringThreads = /* @__PURE__ */ new Set();
|
|
841
843
|
const secureSessionsByTokenHash = /* @__PURE__ */ new Map();
|
|
844
|
+
const activeStreamControllers = /* @__PURE__ */ new Set();
|
|
842
845
|
const threadOptions = { workingDirectory: workspacePath };
|
|
843
846
|
const scheduleAppServerHistoryLoad = (threadId, cachedMessages) => {
|
|
844
847
|
if (!appServer || appServerHistoryLoadsByThreadId.has(threadId)) return;
|
|
@@ -870,7 +873,7 @@ function createApp(options = {}) {
|
|
|
870
873
|
};
|
|
871
874
|
app.use("*", cors());
|
|
872
875
|
app.use("*", async (c, next) => {
|
|
873
|
-
if (!options.pairing || c.req.method === "OPTIONS" || c.req.path === apiPaths.version || c.req.path.startsWith(`${apiPaths.imageAttachments}/`) || c.req.path.startsWith(apiPaths.pair)) {
|
|
876
|
+
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)) {
|
|
874
877
|
await next();
|
|
875
878
|
return;
|
|
876
879
|
}
|
|
@@ -978,6 +981,18 @@ function createApp(options = {}) {
|
|
|
978
981
|
});
|
|
979
982
|
return c.json({ ok: true });
|
|
980
983
|
});
|
|
984
|
+
app.post(apiPaths.sessionsClear, async (c) => {
|
|
985
|
+
if (!options.pairing) return c.json(apiError("pairing_disabled", "Pairing is not enabled on this server."), 404);
|
|
986
|
+
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);
|
|
987
|
+
const result = await options.pairing.sessions.clearAll();
|
|
988
|
+
secureSessionsByTokenHash.clear();
|
|
989
|
+
closeActiveStreamControllers(activeStreamControllers);
|
|
990
|
+
options.pairing.onPairingsCleared?.(result);
|
|
991
|
+
return c.json({
|
|
992
|
+
ok: true,
|
|
993
|
+
...result
|
|
994
|
+
});
|
|
995
|
+
});
|
|
981
996
|
app.post(apiPaths.sessionRefresh, async (c) => {
|
|
982
997
|
if (!options.pairing) return c.json(apiError("pairing_disabled", "Pairing is not enabled on this server."), 404);
|
|
983
998
|
const oldToken = parseBearerToken(c.req.header("authorization"));
|
|
@@ -1140,6 +1155,70 @@ function createApp(options = {}) {
|
|
|
1140
1155
|
return secureJson(c, options.pairing, secureSessionsByTokenHash, apiError("workspace_commit_push_failed", errorMessage(error)), 400);
|
|
1141
1156
|
}
|
|
1142
1157
|
});
|
|
1158
|
+
app.post(apiPaths.workspaceTerminalSessions, async (c) => {
|
|
1159
|
+
const parsed = await parseRequestJson(c, options.pairing, secureSessionsByTokenHash, WorkspaceTerminalStartRequestSchema);
|
|
1160
|
+
if (!parsed.success) return secureJson(c, options.pairing, secureSessionsByTokenHash, validationError(parsed.error), 400);
|
|
1161
|
+
const selectedWorkspacePath = await validateThreadWorkspacePath(workspacePath, parsed.data.workspacePath);
|
|
1162
|
+
if (!selectedWorkspacePath.success) return secureJson(c, options.pairing, secureSessionsByTokenHash, apiError("invalid_workspace_path", selectedWorkspacePath.error), 400);
|
|
1163
|
+
try {
|
|
1164
|
+
const session = createWorkspaceTerminalSession({
|
|
1165
|
+
cols: parsed.data.cols,
|
|
1166
|
+
cwd: selectedWorkspacePath.path,
|
|
1167
|
+
rows: parsed.data.rows
|
|
1168
|
+
});
|
|
1169
|
+
workspaceTerminalSessions.set(session.sessionId, session);
|
|
1170
|
+
const response = WorkspaceTerminalSessionResponseSchema.parse({
|
|
1171
|
+
cols: session.cols,
|
|
1172
|
+
rows: session.rows,
|
|
1173
|
+
sessionId: session.sessionId,
|
|
1174
|
+
startedAt: session.startedAt,
|
|
1175
|
+
workspacePath: session.workspacePath
|
|
1176
|
+
});
|
|
1177
|
+
return secureJson(c, options.pairing, secureSessionsByTokenHash, response);
|
|
1178
|
+
} catch (error) {
|
|
1179
|
+
return secureJson(c, options.pairing, secureSessionsByTokenHash, apiError("workspace_terminal_start_failed", errorMessage(error)), 400);
|
|
1180
|
+
}
|
|
1181
|
+
});
|
|
1182
|
+
app.get("/v1/workspace/terminal/sessions/:sessionId/output", async (c) => {
|
|
1183
|
+
const session = workspaceTerminalSessions.get(c.req.param("sessionId"));
|
|
1184
|
+
if (!session) return secureJson(c, options.pairing, secureSessionsByTokenHash, apiError("workspace_terminal_not_found", "Terminal session was not found."), 404);
|
|
1185
|
+
const since = Number(c.req.query("since") ?? "0");
|
|
1186
|
+
const chunks = session.output.filter((chunk) => chunk.seq >= since);
|
|
1187
|
+
const response = WorkspaceTerminalOutputResponseSchema.parse({
|
|
1188
|
+
chunks,
|
|
1189
|
+
exitCode: session.exitCode,
|
|
1190
|
+
exitedAt: session.exitedAt,
|
|
1191
|
+
nextSeq: session.seq
|
|
1192
|
+
});
|
|
1193
|
+
return secureJson(c, options.pairing, secureSessionsByTokenHash, response);
|
|
1194
|
+
});
|
|
1195
|
+
app.post("/v1/workspace/terminal/sessions/:sessionId/input", async (c) => {
|
|
1196
|
+
const session = workspaceTerminalSessions.get(c.req.param("sessionId"));
|
|
1197
|
+
if (!session) return secureJson(c, options.pairing, secureSessionsByTokenHash, apiError("workspace_terminal_not_found", "Terminal session was not found."), 404);
|
|
1198
|
+
const parsed = await parseRequestJson(c, options.pairing, secureSessionsByTokenHash, WorkspaceTerminalInputRequestSchema);
|
|
1199
|
+
if (!parsed.success) return secureJson(c, options.pairing, secureSessionsByTokenHash, validationError(parsed.error), 400);
|
|
1200
|
+
if (session.exitedAt) return secureJson(c, options.pairing, secureSessionsByTokenHash, apiError("workspace_terminal_closed", "Terminal session is closed."), 409);
|
|
1201
|
+
session.child.write(parsed.data.data);
|
|
1202
|
+
return secureJson(c, options.pairing, secureSessionsByTokenHash, { ok: true });
|
|
1203
|
+
});
|
|
1204
|
+
app.post("/v1/workspace/terminal/sessions/:sessionId/resize", async (c) => {
|
|
1205
|
+
const session = workspaceTerminalSessions.get(c.req.param("sessionId"));
|
|
1206
|
+
if (!session) return secureJson(c, options.pairing, secureSessionsByTokenHash, apiError("workspace_terminal_not_found", "Terminal session was not found."), 404);
|
|
1207
|
+
const parsed = await parseRequestJson(c, options.pairing, secureSessionsByTokenHash, WorkspaceTerminalResizeRequestSchema);
|
|
1208
|
+
if (!parsed.success) return secureJson(c, options.pairing, secureSessionsByTokenHash, validationError(parsed.error), 400);
|
|
1209
|
+
session.cols = parsed.data.cols;
|
|
1210
|
+
session.rows = parsed.data.rows;
|
|
1211
|
+
if (!session.exitedAt) session.child.resize(parsed.data.cols, parsed.data.rows);
|
|
1212
|
+
return secureJson(c, options.pairing, secureSessionsByTokenHash, { ok: true });
|
|
1213
|
+
});
|
|
1214
|
+
app.delete("/v1/workspace/terminal/sessions/:sessionId", async (c) => {
|
|
1215
|
+
const session = workspaceTerminalSessions.get(c.req.param("sessionId"));
|
|
1216
|
+
if (session) {
|
|
1217
|
+
closeWorkspaceTerminalSession(session);
|
|
1218
|
+
workspaceTerminalSessions.delete(session.sessionId);
|
|
1219
|
+
}
|
|
1220
|
+
return secureJson(c, options.pairing, secureSessionsByTokenHash, { ok: true });
|
|
1221
|
+
});
|
|
1143
1222
|
app.get(apiPaths.models, async (c) => {
|
|
1144
1223
|
try {
|
|
1145
1224
|
const models = appServer ? await appServer.listModels() : fallbackModels();
|
|
@@ -1748,9 +1827,12 @@ function createApp(options = {}) {
|
|
|
1748
1827
|
});
|
|
1749
1828
|
const encoder = new TextEncoder();
|
|
1750
1829
|
const secureSession = getSecureSessionForRequest(c, options.pairing, secureSessionsByTokenHash);
|
|
1830
|
+
let streamController;
|
|
1751
1831
|
let streamSettled = false;
|
|
1752
1832
|
const stream = new ReadableStream({
|
|
1753
1833
|
start(controller) {
|
|
1834
|
+
streamController = controller;
|
|
1835
|
+
activeStreamControllers.add(controller);
|
|
1754
1836
|
relayDebugLog("thread.stream.started", {
|
|
1755
1837
|
mode: !runOptions.prompt && appServer ? "attach" : "run",
|
|
1756
1838
|
threadId
|
|
@@ -1781,6 +1863,7 @@ function createApp(options = {}) {
|
|
|
1781
1863
|
mode: "attach",
|
|
1782
1864
|
threadId
|
|
1783
1865
|
});
|
|
1866
|
+
activeStreamControllers.delete(controller);
|
|
1784
1867
|
stopPreviewMonitor();
|
|
1785
1868
|
});
|
|
1786
1869
|
return;
|
|
@@ -1798,6 +1881,7 @@ function createApp(options = {}) {
|
|
|
1798
1881
|
mode: "unsupported",
|
|
1799
1882
|
threadId
|
|
1800
1883
|
});
|
|
1884
|
+
activeStreamControllers.delete(controller);
|
|
1801
1885
|
stopPreviewMonitor();
|
|
1802
1886
|
return;
|
|
1803
1887
|
}
|
|
@@ -1830,10 +1914,12 @@ function createApp(options = {}) {
|
|
|
1830
1914
|
mode: "run",
|
|
1831
1915
|
threadId
|
|
1832
1916
|
});
|
|
1917
|
+
activeStreamControllers.delete(controller);
|
|
1833
1918
|
stopPreviewMonitor();
|
|
1834
1919
|
});
|
|
1835
1920
|
},
|
|
1836
1921
|
cancel(reason) {
|
|
1922
|
+
if (streamController) activeStreamControllers.delete(streamController);
|
|
1837
1923
|
relayDebugLog("thread.stream.cancelled_by_client", {
|
|
1838
1924
|
reason: debugReason(reason),
|
|
1839
1925
|
settled: streamSettled,
|
|
@@ -3476,10 +3562,32 @@ function updateThread(threads, messagesByThreadId, threadId, update) {
|
|
|
3476
3562
|
}
|
|
3477
3563
|
function sendSse(controller, encoder, secureSession, event) {
|
|
3478
3564
|
const parsed = StreamThreadRunEventSchema.parse(event);
|
|
3565
|
+
const threadId = threadIdFromStreamEvent(parsed);
|
|
3566
|
+
relayDebugLog("thread.stream.sse", {
|
|
3567
|
+
direction: "server_to_mobile",
|
|
3568
|
+
eventType: parsed.type,
|
|
3569
|
+
threadId,
|
|
3570
|
+
payload: parsed
|
|
3571
|
+
});
|
|
3479
3572
|
const data = secureSession ? EncryptedPayloadSchema.parse(encryptForMobile(secureSession.session, JSON.stringify(parsed))) : parsed;
|
|
3480
3573
|
if (secureSession) secureSession.persist().catch(() => void 0);
|
|
3481
|
-
if (!enqueueSseChunk(controller, encoder.encode(`event: ${parsed.type}\n`)))
|
|
3482
|
-
|
|
3574
|
+
if (!enqueueSseChunk(controller, encoder.encode(`event: ${parsed.type}\n`))) {
|
|
3575
|
+
relayDebugLog("thread.stream.sse.enqueue_failed", {
|
|
3576
|
+
eventType: parsed.type,
|
|
3577
|
+
stage: "event",
|
|
3578
|
+
threadId
|
|
3579
|
+
});
|
|
3580
|
+
return;
|
|
3581
|
+
}
|
|
3582
|
+
if (!enqueueSseChunk(controller, encoder.encode(`data: ${JSON.stringify(data)}\n\n`))) relayDebugLog("thread.stream.sse.enqueue_failed", {
|
|
3583
|
+
eventType: parsed.type,
|
|
3584
|
+
stage: "data",
|
|
3585
|
+
threadId
|
|
3586
|
+
});
|
|
3587
|
+
}
|
|
3588
|
+
function threadIdFromStreamEvent(event) {
|
|
3589
|
+
if ("threadId" in event && typeof event.threadId === "string") return event.threadId;
|
|
3590
|
+
if ("thread" in event && event.thread) return event.thread.id;
|
|
3483
3591
|
}
|
|
3484
3592
|
function enqueueSseChunk(controller, chunk) {
|
|
3485
3593
|
try {
|
|
@@ -3499,6 +3607,10 @@ function closeSseController(controller) {
|
|
|
3499
3607
|
throw error;
|
|
3500
3608
|
}
|
|
3501
3609
|
}
|
|
3610
|
+
function closeActiveStreamControllers(controllers) {
|
|
3611
|
+
for (const controller of controllers) closeSseController(controller);
|
|
3612
|
+
controllers.clear();
|
|
3613
|
+
}
|
|
3502
3614
|
function isClosedStreamControllerError(error) {
|
|
3503
3615
|
return error instanceof TypeError && error.code === "ERR_INVALID_STATE" && error.message.includes("Controller is already closed");
|
|
3504
3616
|
}
|
|
@@ -5168,10 +5280,57 @@ async function git(cwd, args) {
|
|
|
5168
5280
|
});
|
|
5169
5281
|
return stdout.trimEnd();
|
|
5170
5282
|
}
|
|
5283
|
+
function createWorkspaceTerminalSession(input) {
|
|
5284
|
+
const sessionId = randomUUID();
|
|
5285
|
+
const startedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
5286
|
+
const shell = process.env.SHELL || "/bin/sh";
|
|
5287
|
+
const child = pty.spawn(shell, ["-l"], {
|
|
5288
|
+
cols: input.cols,
|
|
5289
|
+
cwd: input.cwd,
|
|
5290
|
+
env: {
|
|
5291
|
+
...process.env,
|
|
5292
|
+
COLORTERM: process.env.COLORTERM ?? "truecolor",
|
|
5293
|
+
TERM: process.env.TERM && process.env.TERM !== "dumb" ? process.env.TERM : "xterm-256color"
|
|
5294
|
+
},
|
|
5295
|
+
name: "xterm-256color",
|
|
5296
|
+
rows: input.rows
|
|
5297
|
+
});
|
|
5298
|
+
const session = {
|
|
5299
|
+
child,
|
|
5300
|
+
cols: input.cols,
|
|
5301
|
+
output: [],
|
|
5302
|
+
rows: input.rows,
|
|
5303
|
+
seq: 0,
|
|
5304
|
+
sessionId,
|
|
5305
|
+
startedAt,
|
|
5306
|
+
workspacePath: input.cwd
|
|
5307
|
+
};
|
|
5308
|
+
const appendOutput = (data) => {
|
|
5309
|
+
session.output.push({
|
|
5310
|
+
data,
|
|
5311
|
+
seq: session.seq
|
|
5312
|
+
});
|
|
5313
|
+
session.seq += 1;
|
|
5314
|
+
if (session.output.length > maxWorkspaceTerminalOutputChunks) session.output.splice(0, session.output.length - maxWorkspaceTerminalOutputChunks);
|
|
5315
|
+
};
|
|
5316
|
+
child.onData(appendOutput);
|
|
5317
|
+
child.onExit(({ exitCode }) => {
|
|
5318
|
+
session.exitCode = exitCode;
|
|
5319
|
+
session.exitedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
5320
|
+
});
|
|
5321
|
+
return session;
|
|
5322
|
+
}
|
|
5323
|
+
function closeWorkspaceTerminalSession(session) {
|
|
5324
|
+
if (session.exitedAt) return;
|
|
5325
|
+
session.child.kill();
|
|
5326
|
+
}
|
|
5171
5327
|
async function listWorkspaceFiles(workspacePath, query, directory) {
|
|
5172
5328
|
const normalizedQuery = query.toLowerCase();
|
|
5173
|
-
const
|
|
5329
|
+
const isIgnored = await workspaceIgnoreMatcher(workspacePath);
|
|
5330
|
+
if (directory && isWorkspacePathIgnored(directory, isIgnored)) return [];
|
|
5331
|
+
const filePaths = await workspaceFilePaths(workspacePath, isIgnored);
|
|
5174
5332
|
const entriesByPath = /* @__PURE__ */ new Map();
|
|
5333
|
+
for (const entry of await workspaceDirectoryEntries(workspacePath, directory, isIgnored)) entriesByPath.set(entry.path, entry);
|
|
5175
5334
|
for (const path of filePaths) {
|
|
5176
5335
|
if (!path || path.startsWith("../") || path.includes("/.git/")) continue;
|
|
5177
5336
|
if (!directory && normalizedQuery) {
|
|
@@ -5184,6 +5343,7 @@ async function listWorkspaceFiles(workspacePath, query, directory) {
|
|
|
5184
5343
|
const parts = path.split("/");
|
|
5185
5344
|
for (let index = 1; index < parts.length; index += 1) {
|
|
5186
5345
|
const directoryPath = parts.slice(0, index).join("/");
|
|
5346
|
+
if (isWorkspacePathIgnored(directoryPath, isIgnored)) continue;
|
|
5187
5347
|
entriesByPath.set(directoryPath, {
|
|
5188
5348
|
directory: dirname(directoryPath) === "." ? "" : dirname(directoryPath),
|
|
5189
5349
|
kind: "directory",
|
|
@@ -5205,6 +5365,7 @@ async function listWorkspaceFiles(workspacePath, query, directory) {
|
|
|
5205
5365
|
});
|
|
5206
5366
|
else {
|
|
5207
5367
|
const directoryPath = [...directory ? directory.split("/") : [], childParts[0]].join("/");
|
|
5368
|
+
if (isWorkspacePathIgnored(directoryPath, isIgnored)) continue;
|
|
5208
5369
|
entriesByPath.set(directoryPath, {
|
|
5209
5370
|
directory,
|
|
5210
5371
|
kind: "directory",
|
|
@@ -5383,19 +5544,32 @@ function languageFromWorkspaceFile(path) {
|
|
|
5383
5544
|
yml: "yaml"
|
|
5384
5545
|
}[extension] ?? extension;
|
|
5385
5546
|
}
|
|
5386
|
-
async function workspaceFilePaths(workspacePath) {
|
|
5387
|
-
const isIgnored = await workspaceIgnoreMatcher(workspacePath);
|
|
5547
|
+
async function workspaceFilePaths(workspacePath, isIgnored) {
|
|
5388
5548
|
try {
|
|
5389
5549
|
return (await git(workspacePath, [
|
|
5390
5550
|
"ls-files",
|
|
5391
5551
|
"--cached",
|
|
5392
5552
|
"--others",
|
|
5393
5553
|
"--exclude-standard"
|
|
5394
|
-
])).split("\n").filter(Boolean).filter((path) => !
|
|
5554
|
+
])).split("\n").filter(Boolean).filter((path) => !isWorkspacePathIgnored(path, isIgnored));
|
|
5395
5555
|
} catch {
|
|
5396
5556
|
return recursiveWorkspaceFilePaths(workspacePath, isIgnored);
|
|
5397
5557
|
}
|
|
5398
5558
|
}
|
|
5559
|
+
async function workspaceDirectoryEntries(workspacePath, directory, isIgnored) {
|
|
5560
|
+
const rootPath = resolve(workspacePath);
|
|
5561
|
+
const absoluteDirectory = resolve(rootPath, directory);
|
|
5562
|
+
if (absoluteDirectory !== rootPath && !isPathInside(rootPath, absoluteDirectory)) return [];
|
|
5563
|
+
return (await readdir(absoluteDirectory, { withFileTypes: true })).filter((entry) => entry.isDirectory() && entry.name !== ".git").map((entry) => {
|
|
5564
|
+
const path = [...directory ? directory.split("/") : [], entry.name].join("/");
|
|
5565
|
+
return {
|
|
5566
|
+
directory,
|
|
5567
|
+
kind: "directory",
|
|
5568
|
+
name: entry.name,
|
|
5569
|
+
path
|
|
5570
|
+
};
|
|
5571
|
+
}).filter((entry) => !isWorkspacePathIgnored(entry.path, isIgnored));
|
|
5572
|
+
}
|
|
5399
5573
|
async function recursiveWorkspaceFilePaths(rootPath, isIgnored) {
|
|
5400
5574
|
const ignoredDirectories = new Set([
|
|
5401
5575
|
".git",
|
|
@@ -5411,7 +5585,7 @@ async function recursiveWorkspaceFilePaths(rootPath, isIgnored) {
|
|
|
5411
5585
|
if (entry.name.startsWith(".") && entry.name !== ".github") continue;
|
|
5412
5586
|
const absolutePath = resolve(directory, entry.name);
|
|
5413
5587
|
const relativePath = relative(rootPath, absolutePath).split("\\").join("/");
|
|
5414
|
-
if (
|
|
5588
|
+
if (isWorkspacePathIgnored(relativePath, isIgnored)) continue;
|
|
5415
5589
|
if (entry.isDirectory()) {
|
|
5416
5590
|
if (ignoredDirectories.has(entry.name)) continue;
|
|
5417
5591
|
await visit(absolutePath);
|
|
@@ -5427,6 +5601,17 @@ async function workspaceIgnoreMatcher(workspacePath) {
|
|
|
5427
5601
|
const matchers = (await readFile(join(workspacePath, ".gitignore"), "utf8").catch(() => "")).split(/\r?\n/).map((line) => gitignorePatternMatcher(line)).filter((matcher) => Boolean(matcher));
|
|
5428
5602
|
return (path) => matchers.some((matcher) => matcher(path.split("\\").join("/")));
|
|
5429
5603
|
}
|
|
5604
|
+
function isWorkspacePathIgnored(path, isIgnored) {
|
|
5605
|
+
const normalizedPath = path.replace(/^\/+/, "").replaceAll("\\", "/").replace(/\/+$/, "");
|
|
5606
|
+
if (!normalizedPath) return false;
|
|
5607
|
+
if (isIgnored(normalizedPath) || isIgnored(`${normalizedPath}/`)) return true;
|
|
5608
|
+
const parts = normalizedPath.split("/");
|
|
5609
|
+
for (let index = 1; index < parts.length; index += 1) {
|
|
5610
|
+
const ancestorPath = parts.slice(0, index).join("/");
|
|
5611
|
+
if (isIgnored(ancestorPath) || isIgnored(`${ancestorPath}/`)) return true;
|
|
5612
|
+
}
|
|
5613
|
+
return false;
|
|
5614
|
+
}
|
|
5430
5615
|
function gitignorePatternMatcher(line) {
|
|
5431
5616
|
const trimmed = line.trim();
|
|
5432
5617
|
if (!trimmed || trimmed.startsWith("#") || trimmed.startsWith("!")) return;
|
|
@@ -5439,7 +5624,7 @@ function gitignorePatternMatcher(line) {
|
|
|
5439
5624
|
return (path) => {
|
|
5440
5625
|
const normalizedPath = path.replace(/^\/+/, "");
|
|
5441
5626
|
return (anchored || hasSlash ? [normalizedPath] : normalizedPath.split("/")).some((candidate) => {
|
|
5442
|
-
if (directoryOnly) return matcher(candidate)
|
|
5627
|
+
if (directoryOnly) return matcher(candidate);
|
|
5443
5628
|
return matcher(candidate);
|
|
5444
5629
|
});
|
|
5445
5630
|
};
|
|
@@ -5473,238 +5658,6 @@ function errorMessage(error) {
|
|
|
5473
5658
|
return error instanceof Error ? error.message : "Codex run failed.";
|
|
5474
5659
|
}
|
|
5475
5660
|
//#endregion
|
|
5476
|
-
//#region src/pairing-store.ts
|
|
5477
|
-
async function createTursoPairingSessionStore(path) {
|
|
5478
|
-
if (path !== ":memory:") await mkdir(dirname(path), { recursive: true });
|
|
5479
|
-
const db = await connect(path);
|
|
5480
|
-
await db.exec(`
|
|
5481
|
-
CREATE TABLE IF NOT EXISTS pairing_sessions (
|
|
5482
|
-
token_hash TEXT PRIMARY KEY,
|
|
5483
|
-
client_session_id TEXT,
|
|
5484
|
-
client_name TEXT,
|
|
5485
|
-
expires_at INTEGER NOT NULL,
|
|
5486
|
-
key_epoch INTEGER,
|
|
5487
|
-
mobile_to_server_key TEXT,
|
|
5488
|
-
server_to_mobile_key TEXT,
|
|
5489
|
-
last_mobile_counter INTEGER,
|
|
5490
|
-
next_server_counter INTEGER,
|
|
5491
|
-
created_at INTEGER NOT NULL,
|
|
5492
|
-
updated_at INTEGER NOT NULL
|
|
5493
|
-
);
|
|
5494
|
-
|
|
5495
|
-
CREATE TABLE IF NOT EXISTS pending_pairings (
|
|
5496
|
-
approval_code TEXT PRIMARY KEY,
|
|
5497
|
-
client_session_id TEXT,
|
|
5498
|
-
client_name TEXT,
|
|
5499
|
-
client_ephemeral_public_key TEXT NOT NULL,
|
|
5500
|
-
client_nonce TEXT NOT NULL,
|
|
5501
|
-
server_url TEXT NOT NULL,
|
|
5502
|
-
approved INTEGER NOT NULL DEFAULT 0,
|
|
5503
|
-
expires_at INTEGER NOT NULL,
|
|
5504
|
-
created_at INTEGER NOT NULL,
|
|
5505
|
-
updated_at INTEGER NOT NULL
|
|
5506
|
-
);
|
|
5507
|
-
`);
|
|
5508
|
-
await ensurePairingSessionColumns();
|
|
5509
|
-
async function countActive(now) {
|
|
5510
|
-
const row = await db.prepare("SELECT COUNT(DISTINCT COALESCE(client_session_id, token_hash)) AS count FROM pairing_sessions WHERE expires_at > ?").get(now);
|
|
5511
|
-
return Number(row?.count ?? 0);
|
|
5512
|
-
}
|
|
5513
|
-
async function deleteSession(tokenHash) {
|
|
5514
|
-
await db.prepare("DELETE FROM pairing_sessions WHERE token_hash = ?").run(tokenHash);
|
|
5515
|
-
}
|
|
5516
|
-
async function deletePendingPairing(approvalCode) {
|
|
5517
|
-
await db.prepare("DELETE FROM pending_pairings WHERE approval_code = ?").run(approvalCode);
|
|
5518
|
-
}
|
|
5519
|
-
async function getPendingPairing(approvalCode, now) {
|
|
5520
|
-
const row = await db.prepare(`SELECT approval_code AS approvalCode,
|
|
5521
|
-
client_session_id AS clientSessionId,
|
|
5522
|
-
client_name AS clientName,
|
|
5523
|
-
client_ephemeral_public_key AS clientEphemeralPublicKey,
|
|
5524
|
-
client_nonce AS clientNonce,
|
|
5525
|
-
server_url AS serverUrl,
|
|
5526
|
-
approved,
|
|
5527
|
-
expires_at AS expiresAt
|
|
5528
|
-
FROM pending_pairings
|
|
5529
|
-
WHERE approval_code = ?`).get(approvalCode);
|
|
5530
|
-
if (!row) return;
|
|
5531
|
-
const expiresAt = Number(row.expiresAt);
|
|
5532
|
-
if (now > expiresAt) {
|
|
5533
|
-
await deletePendingPairing(approvalCode);
|
|
5534
|
-
return;
|
|
5535
|
-
}
|
|
5536
|
-
return {
|
|
5537
|
-
approvalCode: String(row.approvalCode),
|
|
5538
|
-
approved: Number(row.approved) === 1,
|
|
5539
|
-
clientEphemeralPublicKey: String(row.clientEphemeralPublicKey),
|
|
5540
|
-
clientSessionId: typeof row.clientSessionId === "string" ? row.clientSessionId : void 0,
|
|
5541
|
-
clientName: typeof row.clientName === "string" ? row.clientName : void 0,
|
|
5542
|
-
clientNonce: String(row.clientNonce),
|
|
5543
|
-
expiresAt,
|
|
5544
|
-
serverUrl: String(row.serverUrl)
|
|
5545
|
-
};
|
|
5546
|
-
}
|
|
5547
|
-
return {
|
|
5548
|
-
async approvePendingPairing(approvalCode, now) {
|
|
5549
|
-
const pending = await getPendingPairing(approvalCode, now);
|
|
5550
|
-
if (!pending) return;
|
|
5551
|
-
await db.prepare("UPDATE pending_pairings SET approved = 1, updated_at = ? WHERE approval_code = ?").run(now, approvalCode);
|
|
5552
|
-
return {
|
|
5553
|
-
...pending,
|
|
5554
|
-
approved: true
|
|
5555
|
-
};
|
|
5556
|
-
},
|
|
5557
|
-
countActive,
|
|
5558
|
-
async createPendingPairing(pairing) {
|
|
5559
|
-
const now = Date.now();
|
|
5560
|
-
await db.prepare(`INSERT INTO pending_pairings (
|
|
5561
|
-
approval_code,
|
|
5562
|
-
client_session_id,
|
|
5563
|
-
client_name,
|
|
5564
|
-
client_ephemeral_public_key,
|
|
5565
|
-
client_nonce,
|
|
5566
|
-
server_url,
|
|
5567
|
-
approved,
|
|
5568
|
-
expires_at,
|
|
5569
|
-
created_at,
|
|
5570
|
-
updated_at
|
|
5571
|
-
)
|
|
5572
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`).run(pairing.approvalCode, pairing.clientSessionId ?? null, pairing.clientName ?? null, pairing.clientEphemeralPublicKey, pairing.clientNonce, pairing.serverUrl, pairing.approved ? 1 : 0, pairing.expiresAt, now, now);
|
|
5573
|
-
},
|
|
5574
|
-
async createSession(tokenHash, session) {
|
|
5575
|
-
const now = Date.now();
|
|
5576
|
-
const secure = encodeSecureSession(session.secureSession);
|
|
5577
|
-
if (session.clientSessionId) {
|
|
5578
|
-
await db.prepare("DELETE FROM pairing_sessions WHERE client_session_id = ?").run(session.clientSessionId);
|
|
5579
|
-
if (session.clientName) await db.prepare("DELETE FROM pairing_sessions WHERE client_session_id IS NULL AND client_name = ?").run(session.clientName);
|
|
5580
|
-
}
|
|
5581
|
-
await db.prepare(`INSERT INTO pairing_sessions (
|
|
5582
|
-
token_hash,
|
|
5583
|
-
client_session_id,
|
|
5584
|
-
client_name,
|
|
5585
|
-
expires_at,
|
|
5586
|
-
key_epoch,
|
|
5587
|
-
mobile_to_server_key,
|
|
5588
|
-
server_to_mobile_key,
|
|
5589
|
-
last_mobile_counter,
|
|
5590
|
-
next_server_counter,
|
|
5591
|
-
created_at,
|
|
5592
|
-
updated_at
|
|
5593
|
-
)
|
|
5594
|
-
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);
|
|
5595
|
-
return countActive(now);
|
|
5596
|
-
},
|
|
5597
|
-
deleteSession,
|
|
5598
|
-
deletePendingPairing,
|
|
5599
|
-
getPendingPairing,
|
|
5600
|
-
async getValidSession(tokenHash, now) {
|
|
5601
|
-
const row = await db.prepare(`SELECT client_name AS clientName,
|
|
5602
|
-
client_session_id AS clientSessionId,
|
|
5603
|
-
expires_at AS expiresAt,
|
|
5604
|
-
key_epoch AS keyEpoch,
|
|
5605
|
-
mobile_to_server_key AS mobileToServerKey,
|
|
5606
|
-
server_to_mobile_key AS serverToMobileKey,
|
|
5607
|
-
last_mobile_counter AS lastMobileCounter,
|
|
5608
|
-
next_server_counter AS nextServerCounter
|
|
5609
|
-
FROM pairing_sessions
|
|
5610
|
-
WHERE token_hash = ?`).get(tokenHash);
|
|
5611
|
-
if (!row) return;
|
|
5612
|
-
const expiresAt = Number(row.expiresAt);
|
|
5613
|
-
if (now > expiresAt) {
|
|
5614
|
-
await deleteSession(tokenHash);
|
|
5615
|
-
return;
|
|
5616
|
-
}
|
|
5617
|
-
return {
|
|
5618
|
-
clientSessionId: typeof row.clientSessionId === "string" ? row.clientSessionId : void 0,
|
|
5619
|
-
clientName: typeof row.clientName === "string" ? row.clientName : void 0,
|
|
5620
|
-
expiresAt,
|
|
5621
|
-
secureSession: decodeSecureSession(row)
|
|
5622
|
-
};
|
|
5623
|
-
},
|
|
5624
|
-
async pruneExpired(now) {
|
|
5625
|
-
await db.prepare("DELETE FROM pairing_sessions WHERE expires_at <= ?").run(now);
|
|
5626
|
-
await db.prepare("DELETE FROM pending_pairings WHERE expires_at <= ?").run(now);
|
|
5627
|
-
},
|
|
5628
|
-
async rotateSession(oldTokenHash, newTokenHash, session) {
|
|
5629
|
-
const now = Date.now();
|
|
5630
|
-
const secure = encodeSecureSession(session.secureSession);
|
|
5631
|
-
await db.transaction(async () => {
|
|
5632
|
-
await db.prepare("DELETE FROM pairing_sessions WHERE token_hash = ?").run(oldTokenHash);
|
|
5633
|
-
if (session.clientSessionId) {
|
|
5634
|
-
await db.prepare("DELETE FROM pairing_sessions WHERE client_session_id = ?").run(session.clientSessionId);
|
|
5635
|
-
if (session.clientName) await db.prepare("DELETE FROM pairing_sessions WHERE client_session_id IS NULL AND client_name = ?").run(session.clientName);
|
|
5636
|
-
}
|
|
5637
|
-
await db.prepare(`INSERT INTO pairing_sessions (
|
|
5638
|
-
token_hash,
|
|
5639
|
-
client_session_id,
|
|
5640
|
-
client_name,
|
|
5641
|
-
expires_at,
|
|
5642
|
-
key_epoch,
|
|
5643
|
-
mobile_to_server_key,
|
|
5644
|
-
server_to_mobile_key,
|
|
5645
|
-
last_mobile_counter,
|
|
5646
|
-
next_server_counter,
|
|
5647
|
-
created_at,
|
|
5648
|
-
updated_at
|
|
5649
|
-
)
|
|
5650
|
-
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);
|
|
5651
|
-
})();
|
|
5652
|
-
return countActive(now);
|
|
5653
|
-
},
|
|
5654
|
-
async updateSecureSession(tokenHash, secureSession) {
|
|
5655
|
-
const secure = encodeSecureSession(secureSession);
|
|
5656
|
-
const now = Date.now();
|
|
5657
|
-
await db.prepare(`UPDATE pairing_sessions
|
|
5658
|
-
SET key_epoch = ?,
|
|
5659
|
-
mobile_to_server_key = ?,
|
|
5660
|
-
server_to_mobile_key = ?,
|
|
5661
|
-
last_mobile_counter = ?,
|
|
5662
|
-
next_server_counter = ?,
|
|
5663
|
-
updated_at = ?
|
|
5664
|
-
WHERE token_hash = ?`).run(secure.keyEpoch, secure.mobileToServerKey, secure.serverToMobileKey, secure.lastMobileCounter, secure.nextServerCounter, now, tokenHash);
|
|
5665
|
-
}
|
|
5666
|
-
};
|
|
5667
|
-
async function ensurePairingSessionColumns() {
|
|
5668
|
-
const rows = await db.prepare("PRAGMA table_info(pairing_sessions)").all();
|
|
5669
|
-
const columns = new Set(resultRows(rows).map((row) => String(row.name)));
|
|
5670
|
-
for (const [column, sql] of [
|
|
5671
|
-
["client_session_id", "ALTER TABLE pairing_sessions ADD COLUMN client_session_id TEXT"],
|
|
5672
|
-
["key_epoch", "ALTER TABLE pairing_sessions ADD COLUMN key_epoch INTEGER"],
|
|
5673
|
-
["mobile_to_server_key", "ALTER TABLE pairing_sessions ADD COLUMN mobile_to_server_key TEXT"],
|
|
5674
|
-
["server_to_mobile_key", "ALTER TABLE pairing_sessions ADD COLUMN server_to_mobile_key TEXT"],
|
|
5675
|
-
["last_mobile_counter", "ALTER TABLE pairing_sessions ADD COLUMN last_mobile_counter INTEGER"],
|
|
5676
|
-
["next_server_counter", "ALTER TABLE pairing_sessions ADD COLUMN next_server_counter INTEGER"]
|
|
5677
|
-
]) if (!columns.has(column)) await db.exec(sql);
|
|
5678
|
-
const pendingRows = await db.prepare("PRAGMA table_info(pending_pairings)").all();
|
|
5679
|
-
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");
|
|
5680
|
-
}
|
|
5681
|
-
}
|
|
5682
|
-
function encodeSecureSession(session) {
|
|
5683
|
-
if (!session) return;
|
|
5684
|
-
return {
|
|
5685
|
-
keyEpoch: session.keyEpoch,
|
|
5686
|
-
lastMobileCounter: session.lastMobileCounter,
|
|
5687
|
-
mobileToServerKey: fromByteArray(session.mobileToServerKey),
|
|
5688
|
-
nextServerCounter: session.nextServerCounter,
|
|
5689
|
-
serverToMobileKey: fromByteArray(session.serverToMobileKey)
|
|
5690
|
-
};
|
|
5691
|
-
}
|
|
5692
|
-
function decodeSecureSession(row) {
|
|
5693
|
-
if (typeof row.mobileToServerKey !== "string" || typeof row.serverToMobileKey !== "string" || row.keyEpoch === null || row.lastMobileCounter === null || row.nextServerCounter === null) return;
|
|
5694
|
-
return {
|
|
5695
|
-
keyEpoch: Number(row.keyEpoch),
|
|
5696
|
-
lastMobileCounter: Number(row.lastMobileCounter),
|
|
5697
|
-
mobileToServerKey: toByteArray(row.mobileToServerKey),
|
|
5698
|
-
nextServerCounter: Number(row.nextServerCounter),
|
|
5699
|
-
serverToMobileKey: toByteArray(row.serverToMobileKey)
|
|
5700
|
-
};
|
|
5701
|
-
}
|
|
5702
|
-
function resultRows(result) {
|
|
5703
|
-
if (Array.isArray(result)) return result;
|
|
5704
|
-
if (result && typeof result === "object" && Array.isArray(result.rows)) return result.rows;
|
|
5705
|
-
return [];
|
|
5706
|
-
}
|
|
5707
|
-
//#endregion
|
|
5708
5661
|
//#region src/index.ts
|
|
5709
5662
|
const port = Number(process.env.PORT ?? 8787);
|
|
5710
5663
|
const hostname$1 = process.env.HOST ?? "0.0.0.0";
|
|
@@ -5755,6 +5708,9 @@ serve({
|
|
|
5755
5708
|
onPairApproved: ({ clientName }) => {
|
|
5756
5709
|
logRuntimeEvent("Approved", `Pairing request approved${clientName ? ` for ${clientName}` : ""}. Waiting for secure session pickup.`);
|
|
5757
5710
|
},
|
|
5711
|
+
onPairingsCleared: ({ pendingPairingsCleared, sessionsCleared }) => {
|
|
5712
|
+
logRuntimeEvent("Cleared", `Signed out ${sessionsCleared} mobile session${sessionsCleared === 1 ? "" : "s"} and removed ${pendingPairingsCleared} pending pairing request${pendingPairingsCleared === 1 ? "" : "s"}.`);
|
|
5713
|
+
},
|
|
5758
5714
|
onTokenRefreshed: ({ clientName, tokenCount }) => {
|
|
5759
5715
|
logRuntimeEvent("Refreshed", `Mobile session rotated${clientName ? ` for ${clientName}` : ""}; ${formatClientCount(tokenCount)} active.`);
|
|
5760
5716
|
}
|
|
@@ -5765,10 +5721,19 @@ serve({
|
|
|
5765
5721
|
port
|
|
5766
5722
|
}, (info) => {
|
|
5767
5723
|
const listenUrl = `http://${info.address}:${info.port}`;
|
|
5768
|
-
const
|
|
5769
|
-
|
|
5724
|
+
const connectUrlCandidates = getConnectUrlCandidates({
|
|
5725
|
+
listenUrl,
|
|
5726
|
+
port: info.port
|
|
5727
|
+
});
|
|
5728
|
+
const connectUrl = connectUrlCandidates[0]?.url ?? listenUrl;
|
|
5729
|
+
const connectUrls = connectUrlCandidates.map((candidate) => candidate.url);
|
|
5730
|
+
const pairingPayload = createPairingQrPayload({
|
|
5731
|
+
serverPublicKey: serverIdentity.publicKey,
|
|
5732
|
+
serverUrls: connectUrls.length > 0 ? connectUrls : [connectUrl]
|
|
5733
|
+
});
|
|
5770
5734
|
writeServerState({
|
|
5771
5735
|
connectUrl,
|
|
5736
|
+
connectUrlCandidates,
|
|
5772
5737
|
host: hostname$1,
|
|
5773
5738
|
listenUrl,
|
|
5774
5739
|
pairingPayload,
|
|
@@ -5779,6 +5744,7 @@ serve({
|
|
|
5779
5744
|
logRuntimeEvent("Debug", `Writing diagnostics to ${debugLogPath}`);
|
|
5780
5745
|
relayDebugLog("relay.started", {
|
|
5781
5746
|
connectUrl,
|
|
5747
|
+
connectUrlCandidates,
|
|
5782
5748
|
listenUrl,
|
|
5783
5749
|
port: info.port,
|
|
5784
5750
|
workspacePath: process.env.CODEX_RELAY_WORKSPACE_PATH ?? process.cwd()
|
|
@@ -5788,6 +5754,7 @@ serve({
|
|
|
5788
5754
|
qrcode.generate(pairingPayload, { small: true });
|
|
5789
5755
|
console.log(formatStartupInstructions({
|
|
5790
5756
|
connectUrl,
|
|
5757
|
+
connectUrlCandidates,
|
|
5791
5758
|
dangerouslyAutoApprove,
|
|
5792
5759
|
listenUrl,
|
|
5793
5760
|
pairingPayload,
|
|
@@ -5801,6 +5768,8 @@ function formatStartupInstructions(details) {
|
|
|
5801
5768
|
`${color.prompt("›")} Scan the QR code above to pair ${color.brand("Codex Relay mobile")}.`,
|
|
5802
5769
|
"",
|
|
5803
5770
|
`${color.prompt("›")} Mobile: ${color.url(details.connectUrl)}`,
|
|
5771
|
+
...formatConnectUrlGuidance(details.connectUrl),
|
|
5772
|
+
...formatConnectUrlCandidates(details.connectUrlCandidates),
|
|
5804
5773
|
`${color.prompt("›")} Server: ${color.muted(details.listenUrl)}`,
|
|
5805
5774
|
"",
|
|
5806
5775
|
`${color.prompt("›")} Pairing: ${color.url(details.pairingPayload)}`,
|
|
@@ -5817,34 +5786,23 @@ function formatStartupInstructions(details) {
|
|
|
5817
5786
|
""
|
|
5818
5787
|
].join("\n");
|
|
5819
5788
|
}
|
|
5789
|
+
function formatConnectUrlGuidance(connectUrl) {
|
|
5790
|
+
const guidance = getConnectUrlGuidance(connectUrl);
|
|
5791
|
+
return guidance ? [`${color.prompt("›")} Network: ${guidance}`] : [];
|
|
5792
|
+
}
|
|
5793
|
+
function formatConnectUrlCandidates(candidates) {
|
|
5794
|
+
if (candidates.length <= 1) return [];
|
|
5795
|
+
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)}`)];
|
|
5796
|
+
}
|
|
5820
5797
|
function logRuntimeEvent(label, message) {
|
|
5821
5798
|
console.log(`${color.prompt("›")} ${color.event(label.padEnd(8))} ${message}`);
|
|
5822
5799
|
}
|
|
5823
5800
|
function formatClientCount(tokenCount) {
|
|
5824
5801
|
return `${tokenCount} client${tokenCount === 1 ? "" : "s"}`;
|
|
5825
5802
|
}
|
|
5826
|
-
function createPairingQrPayload(serverUrl) {
|
|
5827
|
-
const url = new URL("codex-relay://pair");
|
|
5828
|
-
url.searchParams.set("serverUrl", serverUrl);
|
|
5829
|
-
url.searchParams.set("serverPublicKey", serverIdentity.publicKey);
|
|
5830
|
-
return url.toString();
|
|
5831
|
-
}
|
|
5832
5803
|
function hashClientToken(token) {
|
|
5833
5804
|
return createHash("sha256").update(token).digest("base64url");
|
|
5834
5805
|
}
|
|
5835
|
-
function getConfiguredConnectUrl() {
|
|
5836
|
-
const configuredUrl = normalizeUrl(process.env.CODEX_RELAY_PUBLIC_URL);
|
|
5837
|
-
if (configuredUrl) return configuredUrl;
|
|
5838
|
-
}
|
|
5839
|
-
function getTailscaleConnectUrl(port) {
|
|
5840
|
-
const status = getTailscaleStatus();
|
|
5841
|
-
const tailscaleIp = status?.Self?.TailscaleIPs?.find((ip) => ip.startsWith("100.") && ip.includes("."));
|
|
5842
|
-
if (tailscaleIp) return `http://${tailscaleIp}:${port}`;
|
|
5843
|
-
const dnsName = status?.Self?.DNSName?.replace(/\.$/, "");
|
|
5844
|
-
if (dnsName) return getTailscaleServeHttpsUrl(dnsName, port) ?? `http://${dnsName}:${port}`;
|
|
5845
|
-
const tailscaleHost = status?.Self?.TailscaleIPs?.find((ip) => ip.includes("."));
|
|
5846
|
-
return tailscaleHost ? `http://${tailscaleHost}:${port}` : void 0;
|
|
5847
|
-
}
|
|
5848
5806
|
async function getApprovalSecret() {
|
|
5849
5807
|
if (process.env.CODEX_RELAY_APPROVAL_SECRET) return process.env.CODEX_RELAY_APPROVAL_SECRET;
|
|
5850
5808
|
const path = await prepareCodexRelayDataPath("approval-secret");
|
|
@@ -5897,58 +5855,5 @@ async function writeBackgroundPid() {
|
|
|
5897
5855
|
function formatApprovalCommand(approvalCode, activePort) {
|
|
5898
5856
|
return activePort === 8787 ? `${npxCommand} approve ${approvalCode}` : `PORT=${activePort} ${npxCommand} approve ${approvalCode}`;
|
|
5899
5857
|
}
|
|
5900
|
-
function getLocalNetworkConnectUrl(port) {
|
|
5901
|
-
for (const addresses of Object.values(networkInterfaces())) for (const address of addresses ?? []) if (address.family === "IPv4" && !address.internal) return `http://${address.address}:${port}`;
|
|
5902
|
-
}
|
|
5903
|
-
function normalizeUrl(value) {
|
|
5904
|
-
if (!value) return;
|
|
5905
|
-
const trimmed = value.trim().replace(/\/$/, "");
|
|
5906
|
-
if (!trimmed) return;
|
|
5907
|
-
try {
|
|
5908
|
-
const url = new URL(trimmed);
|
|
5909
|
-
return url.protocol === "http:" || url.protocol === "https:" ? url.toString().replace(/\/$/, "") : void 0;
|
|
5910
|
-
} catch {
|
|
5911
|
-
return;
|
|
5912
|
-
}
|
|
5913
|
-
}
|
|
5914
|
-
function getTailscaleStatus() {
|
|
5915
|
-
try {
|
|
5916
|
-
const output = execFileSync("tailscale", ["status", "--json"], {
|
|
5917
|
-
encoding: "utf8",
|
|
5918
|
-
stdio: [
|
|
5919
|
-
"ignore",
|
|
5920
|
-
"pipe",
|
|
5921
|
-
"ignore"
|
|
5922
|
-
],
|
|
5923
|
-
timeout: 1500
|
|
5924
|
-
});
|
|
5925
|
-
return JSON.parse(output);
|
|
5926
|
-
} catch {
|
|
5927
|
-
return;
|
|
5928
|
-
}
|
|
5929
|
-
}
|
|
5930
|
-
function getTailscaleServeHttpsUrl(dnsName, port) {
|
|
5931
|
-
try {
|
|
5932
|
-
const output = execFileSync("tailscale", [
|
|
5933
|
-
"serve",
|
|
5934
|
-
"status",
|
|
5935
|
-
"--json"
|
|
5936
|
-
], {
|
|
5937
|
-
encoding: "utf8",
|
|
5938
|
-
stdio: [
|
|
5939
|
-
"ignore",
|
|
5940
|
-
"pipe",
|
|
5941
|
-
"ignore"
|
|
5942
|
-
],
|
|
5943
|
-
timeout: 1500
|
|
5944
|
-
});
|
|
5945
|
-
const serveStatus = JSON.parse(output);
|
|
5946
|
-
const portKey = String(port);
|
|
5947
|
-
const hostPort = `${dnsName}:${portKey}`;
|
|
5948
|
-
return serveStatus.TCP?.[portKey]?.HTTPS && serveStatus.Web?.[hostPort] ? `https://${hostPort}` : void 0;
|
|
5949
|
-
} catch {
|
|
5950
|
-
return;
|
|
5951
|
-
}
|
|
5952
|
-
}
|
|
5953
5858
|
//#endregion
|
|
5954
5859
|
export {};
|