codex-relay 1.0.1 → 1.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/src2.js DELETED
@@ -1,2911 +0,0 @@
1
- import { C as WorkspaceGitActionResponseSchema, S as WorkspaceChangesResponseSchema, T as createOpenApiDocument, _ as StreamThreadRunEventSchema, a as CreateThreadRequestSchema, b as ThreadDetailResponseSchema, c as ListThreadsResponseSchema, d as PairResponseSchema, f as RateLimitsResponseSchema, g as StatusResponseSchema, h as RunThreadRequestSchema, i as ContextWindowUsageSchema, l as ListWorkspaceDirectoriesResponseSchema, m as ResolveApprovalResponseSchema, n as CheckoutWorkspaceBranchRequestSchema, o as EncryptedPayloadSchema, p as ResolveApprovalRequestSchema, r as CommitPushWorkspaceRequestSchema, s as ListModelsResponseSchema, t as ChatMessageSchema, u as PairRequestSchema, v as SubmitThreadInputResponseSchema, w as apiPaths, x as ThreadSummarySchema, y as ThreadContextWindowResponseSchema } from "./src.js";
2
- import qrcode from "qrcode-terminal";
3
- import { execFile, execFileSync, spawn } from "node:child_process";
4
- import fs from "node:fs";
5
- import { mkdir, readFile, readdir, stat, writeFile } from "node:fs/promises";
6
- import path, { dirname, resolve } from "node:path";
7
- import { z } from "zod";
8
- import { serve } from "@hono/node-server";
9
- import pc from "picocolors";
10
- import { createHash, randomBytes } from "node:crypto";
11
- import os, { hostname, networkInterfaces } from "node:os";
12
- import { openRepository } from "es-git";
13
- import { Hono } from "hono";
14
- import { cors } from "hono/cors";
15
- import { promisify } from "node:util";
16
- import { Codex } from "@openai/codex-sdk";
17
- import { createInterface } from "node:readline";
18
- import { gcm } from "@noble/ciphers/aes.js";
19
- import { randomBytes as randomBytes$1, utf8ToBytes } from "@noble/ciphers/utils.js";
20
- import { ed25519, x25519 } from "@noble/curves/ed25519.js";
21
- import { hkdf } from "@noble/hashes/hkdf.js";
22
- import { sha256 } from "@noble/hashes/sha2.js";
23
- import { fromByteArray, toByteArray } from "base64-js";
24
- import { connect } from "@tursodatabase/database";
25
- //#region src/codex.ts
26
- function createCodexClient() {
27
- return new Codex();
28
- }
29
- function getThreadId(thread) {
30
- return thread.id ?? thread.threadId ?? void 0;
31
- }
32
- function stringifyRunResult(result) {
33
- if (typeof result === "string") return result;
34
- if (result && typeof result === "object") {
35
- const record = result;
36
- const knownText = record.finalResponse ?? record.final_response ?? record.outputText ?? record.output_text ?? record.text;
37
- if (typeof knownText === "string") return knownText;
38
- }
39
- return JSON.stringify(result, null, 2);
40
- }
41
- function extractStreamText(event) {
42
- if (!event || typeof event !== "object") return;
43
- const record = event;
44
- const item = record.item && typeof record.item === "object" ? record.item : void 0;
45
- const candidate = record.delta ?? record.text ?? record.message ?? item?.text ?? item?.aggregated_output ?? item?.command;
46
- return typeof candidate === "string" && candidate.length > 0 ? candidate : void 0;
47
- }
48
- function classifyStreamEvent(event) {
49
- if (!event || typeof event !== "object") return "status";
50
- const record = event;
51
- if (record.type === "error" || record.type === "turn.failed") return "error";
52
- switch ((record.item && typeof record.item === "object" ? record.item : void 0)?.type) {
53
- case "agent_message": return "assistant";
54
- case "reasoning": return "reasoning";
55
- case "command_execution":
56
- case "file_change":
57
- case "mcp_tool_call":
58
- case "web_search": return "tool";
59
- default: return "status";
60
- }
61
- }
62
- //#endregion
63
- //#region src/app-server.ts
64
- var CodexAppServerClient = class {
65
- child;
66
- initialized;
67
- notificationHandlers = /* @__PURE__ */ new Set();
68
- nextId = 1;
69
- pending = /* @__PURE__ */ new Map();
70
- requestHandlers = /* @__PURE__ */ new Set();
71
- readline;
72
- async listThreads(limit = 80) {
73
- return (await this.request("thread/list", {
74
- limit,
75
- sortKey: "updated_at",
76
- sortDirection: "desc",
77
- sourceKinds: [],
78
- archived: false
79
- })).data;
80
- }
81
- async readThread(threadId) {
82
- return (await this.request("thread/read", {
83
- threadId,
84
- includeTurns: true
85
- })).thread;
86
- }
87
- async listModels(limit = 80) {
88
- return (await this.request("model/list", {
89
- limit,
90
- includeHidden: false
91
- })).data;
92
- }
93
- async readRateLimits() {
94
- return this.request("account/rateLimits/read", null);
95
- }
96
- async startThread(params) {
97
- return (await this.request("thread/start", params)).thread;
98
- }
99
- async startTurn(params) {
100
- return (await this.request("turn/start", params)).turn;
101
- }
102
- onNotification(handler) {
103
- this.notificationHandlers.add(handler);
104
- return () => this.notificationHandlers.delete(handler);
105
- }
106
- onRequest(handler) {
107
- this.requestHandlers.add(handler);
108
- return () => this.requestHandlers.delete(handler);
109
- }
110
- async respondToRequest(id, result) {
111
- await this.writeJson({
112
- id,
113
- result
114
- });
115
- }
116
- async rejectRequest(id, code, message) {
117
- await this.writeJson({
118
- id,
119
- error: {
120
- code,
121
- message
122
- }
123
- });
124
- }
125
- close() {
126
- for (const pending of this.pending.values()) pending.reject(/* @__PURE__ */ new Error("Codex app-server was closed."));
127
- this.pending.clear();
128
- this.readline?.close();
129
- this.child?.kill();
130
- this.readline = void 0;
131
- this.child = void 0;
132
- this.initialized = void 0;
133
- }
134
- async request(method, params) {
135
- await this.ensureInitialized();
136
- const id = this.nextId++;
137
- return new Promise((resolve, reject) => {
138
- this.pending.set(id, {
139
- resolve: (value) => resolve(value),
140
- reject
141
- });
142
- this.child.stdin.write(`${JSON.stringify({
143
- id,
144
- method,
145
- params
146
- })}\n`, (error) => {
147
- if (error) {
148
- this.pending.delete(id);
149
- reject(error);
150
- }
151
- });
152
- });
153
- }
154
- ensureInitialized() {
155
- if (!this.initialized) this.initialized = this.start();
156
- return this.initialized;
157
- }
158
- start() {
159
- this.child = spawn(resolveCodexBinary(), [
160
- "app-server",
161
- "--listen",
162
- "stdio://"
163
- ], { env: process.env });
164
- this.readline = createInterface({
165
- input: this.child.stdout,
166
- crlfDelay: Infinity
167
- });
168
- this.readline.on("line", (line) => this.handleLine(line));
169
- this.child.once("error", (error) => this.rejectAll(error));
170
- this.child.once("exit", (code, signal) => {
171
- this.rejectAll(/* @__PURE__ */ new Error(`Codex app-server exited with ${signal ?? code ?? 1}.`));
172
- this.child = void 0;
173
- this.initialized = void 0;
174
- });
175
- return this.requestRaw("initialize", {
176
- clientInfo: {
177
- name: "codex-relay",
178
- title: "Codex Relay Mobile Server",
179
- version: "0.1.0"
180
- },
181
- capabilities: { experimentalApi: true }
182
- }).then(() => void 0);
183
- }
184
- requestRaw(method, params) {
185
- const id = this.nextId++;
186
- const request = JSON.stringify({
187
- id,
188
- method,
189
- params
190
- });
191
- return new Promise((resolve, reject) => {
192
- this.pending.set(id, {
193
- resolve: (value) => resolve(value),
194
- reject
195
- });
196
- this.child.stdin.write(`${request}\n`, (error) => {
197
- if (error) {
198
- this.pending.delete(id);
199
- reject(error);
200
- }
201
- });
202
- });
203
- }
204
- handleLine(line) {
205
- let message;
206
- try {
207
- message = JSON.parse(line);
208
- } catch {
209
- return;
210
- }
211
- if (typeof message.method === "string" && typeof message.id === "number") {
212
- const request = {
213
- id: message.id,
214
- method: message.method,
215
- params: message.params
216
- };
217
- if (this.requestHandlers.size === 0) this.rejectRequest(request.id, -32601, `No handler for ${request.method}.`);
218
- else for (const handler of this.requestHandlers) handler(request);
219
- return;
220
- }
221
- if (typeof message.method === "string") {
222
- const notification = {
223
- method: message.method,
224
- params: message.params
225
- };
226
- for (const handler of this.notificationHandlers) handler(notification);
227
- return;
228
- }
229
- if (typeof message.id !== "number") return;
230
- const pending = this.pending.get(message.id);
231
- if (!pending) return;
232
- this.pending.delete(message.id);
233
- if (message.error) pending.reject(new Error(message.error.message));
234
- else pending.resolve(message.result);
235
- }
236
- writeJson(payload) {
237
- return new Promise((resolve, reject) => {
238
- if (!this.child?.stdin) {
239
- reject(/* @__PURE__ */ new Error("Codex app-server is not running."));
240
- return;
241
- }
242
- this.child.stdin.write(`${JSON.stringify(payload)}\n`, (error) => {
243
- if (error) reject(error);
244
- else resolve();
245
- });
246
- });
247
- }
248
- rejectAll(error) {
249
- for (const pending of this.pending.values()) pending.reject(error);
250
- this.pending.clear();
251
- }
252
- };
253
- function resolveCodexBinary() {
254
- return process.env.CODEX_BIN ?? "codex";
255
- }
256
- //#endregion
257
- //#region src/context-window.ts
258
- const contextReadCandidateLimit = 128;
259
- const contextReadScanBytes = 512 * 1024;
260
- const threadLookupScanBytes = 512 * 1024;
261
- const turnLookupScanBytes = 16 * 1024;
262
- function readLatestContextWindowUsage({ threadId, turnId }) {
263
- const rolloutPath = findRecentRolloutFileForContextRead(resolveSessionsRoot(), {
264
- threadId,
265
- turnId
266
- });
267
- if (!rolloutPath) return {
268
- rolloutPath: null,
269
- usage: null
270
- };
271
- const stat = fs.statSync(rolloutPath);
272
- return {
273
- rolloutPath,
274
- usage: readRolloutUsageChunk({
275
- endExclusive: stat.size,
276
- filePath: rolloutPath,
277
- start: Math.max(0, stat.size - contextReadScanBytes)
278
- }).usage
279
- };
280
- }
281
- function resolveSessionsRoot() {
282
- const codexHome = process.env.CODEX_HOME || path.join(os.homedir(), ".codex");
283
- return path.join(codexHome, "sessions");
284
- }
285
- function findRecentRolloutFileForContextRead(root, { threadId = "", turnId = "" }) {
286
- const candidates = collectRecentRolloutFiles(root);
287
- if (candidates.length === 0) return null;
288
- if (turnId) {
289
- for (const candidate of candidates) if (rolloutFileContainsTurnId(candidate.filePath, turnId)) return candidate.filePath;
290
- }
291
- if (threadId) {
292
- const filenameMatch = candidates.find(({ filePath }) => path.basename(filePath).includes(threadId));
293
- if (filenameMatch) return filenameMatch.filePath;
294
- for (const candidate of candidates) if (rolloutFileContainsThreadId(candidate.filePath, threadId)) return candidate.filePath;
295
- }
296
- return null;
297
- }
298
- function collectRecentRolloutFiles(root) {
299
- if (!fs.existsSync(root)) return [];
300
- const stack = [root];
301
- const candidates = [];
302
- while (stack.length > 0) {
303
- const current = stack.pop();
304
- for (const entry of fs.readdirSync(current, { withFileTypes: true })) {
305
- const fullPath = path.join(current, entry.name);
306
- if (entry.isDirectory()) {
307
- stack.push(fullPath);
308
- continue;
309
- }
310
- if (entry.isFile() && entry.name.startsWith("rollout-") && entry.name.endsWith(".jsonl")) candidates.push({
311
- filePath: fullPath,
312
- mtimeMs: fs.statSync(fullPath).mtimeMs
313
- });
314
- }
315
- }
316
- return candidates.sort((left, right) => right.mtimeMs - left.mtimeMs).slice(0, contextReadCandidateLimit);
317
- }
318
- function rolloutFileContainsTurnId(filePath, turnId) {
319
- const stat = fs.statSync(filePath);
320
- const chunk = readFileSlice(filePath, 0, Math.min(stat.size, turnLookupScanBytes));
321
- return chunk.includes(`"turn_id":"${turnId}"`) || chunk.includes(`"turnId":"${turnId}"`);
322
- }
323
- function rolloutFileContainsThreadId(filePath, threadId) {
324
- const stat = fs.statSync(filePath);
325
- const chunk = readFileSlice(filePath, Math.max(0, stat.size - Math.min(stat.size, threadLookupScanBytes)), stat.size);
326
- return chunk.includes(`"thread_id":"${threadId}"`) || chunk.includes(`"threadId":"${threadId}"`) || chunk.includes(`"conversation_id":"${threadId}"`) || chunk.includes(`"conversationId":"${threadId}"`);
327
- }
328
- function readRolloutUsageChunk({ filePath, start, endExclusive }) {
329
- const lines = readFileSlice(filePath, start, endExclusive).split("\n");
330
- if (start > 0) lines.shift();
331
- let latestUsage = null;
332
- for (const line of lines) {
333
- const usage = extractContextUsageFromRolloutLine(line);
334
- if (usage) latestUsage = usage;
335
- }
336
- return { usage: latestUsage };
337
- }
338
- function readFileSlice(filePath, start, endExclusive) {
339
- const length = Math.max(0, endExclusive - start);
340
- if (length === 0) return "";
341
- const fileHandle = fs.openSync(filePath, "r");
342
- try {
343
- const buffer = Buffer.alloc(length);
344
- const bytesRead = fs.readSync(fileHandle, buffer, 0, length, start);
345
- return buffer.toString("utf8", 0, bytesRead);
346
- } finally {
347
- fs.closeSync(fileHandle);
348
- }
349
- }
350
- function extractContextUsageFromRolloutLine(rawLine) {
351
- let parsed;
352
- try {
353
- parsed = JSON.parse(rawLine.trim());
354
- } catch {
355
- return null;
356
- }
357
- if (!parsed || typeof parsed !== "object") return null;
358
- const record = parsed;
359
- if (record.type !== "event_msg" || !record.payload || typeof record.payload !== "object") return null;
360
- const payload = record.payload;
361
- if (payload.type !== "token_count" || !payload.info || typeof payload.info !== "object") return null;
362
- return contextUsageFromTokenCountInfo(payload.info);
363
- }
364
- function contextUsageFromTokenCountInfo(info) {
365
- const usageRoot = objectValue(info.last_token_usage ?? info.lastTokenUsage ?? info.total_token_usage ?? info.totalTokenUsage);
366
- const tokenLimit = positiveInteger(info.model_context_window ?? info.modelContextWindow ?? info.context_window ?? info.contextWindow);
367
- if (!tokenLimit) return null;
368
- const tokensUsed = positiveInteger(usageRoot?.total_tokens ?? usageRoot?.totalTokens) ?? sumPositiveIntegers([
369
- usageRoot?.input_tokens ?? usageRoot?.inputTokens,
370
- usageRoot?.output_tokens ?? usageRoot?.outputTokens,
371
- usageRoot?.reasoning_output_tokens ?? usageRoot?.reasoningOutputTokens
372
- ]);
373
- if (tokensUsed === null) return null;
374
- return ContextWindowUsageSchema.parse({
375
- tokenLimit,
376
- tokensUsed: Math.min(tokensUsed, tokenLimit)
377
- });
378
- }
379
- function objectValue(value) {
380
- return value && typeof value === "object" ? value : void 0;
381
- }
382
- function positiveInteger(value) {
383
- if (typeof value === "number" && Number.isFinite(value)) return Math.max(0, Math.trunc(value));
384
- if (typeof value === "string") {
385
- const parsed = Number.parseInt(value, 10);
386
- return Number.isFinite(parsed) ? Math.max(0, parsed) : null;
387
- }
388
- return null;
389
- }
390
- function sumPositiveIntegers(values) {
391
- let sum = 0;
392
- let found = false;
393
- for (const value of values) {
394
- const parsed = positiveInteger(value);
395
- if (parsed !== null) {
396
- sum += parsed;
397
- found = true;
398
- }
399
- }
400
- return found ? sum : null;
401
- }
402
- const handshakeTag = "codex-relay-e2ee-v1";
403
- function createServerIdentity() {
404
- const privateKey = ed25519.utils.randomSecretKey();
405
- return {
406
- privateKey,
407
- publicKey: bytesToBase64(ed25519.getPublicKey(privateKey))
408
- };
409
- }
410
- function createSecurePairing(input) {
411
- const serverEphemeralPrivateKey = x25519.utils.randomSecretKey();
412
- const serverEphemeralPublicKey = x25519.getPublicKey(serverEphemeralPrivateKey);
413
- const serverNonce = randomBytes$1(32);
414
- const transcript = pairingTranscript({
415
- clientEphemeralPublicKey: input.clientEphemeralPublicKey,
416
- clientNonce: input.clientNonce,
417
- keyEpoch: input.keyEpoch,
418
- approvalCode: input.approvalCode,
419
- serverEphemeralPublicKey: bytesToBase64(serverEphemeralPublicKey),
420
- serverIdentityPublicKey: input.serverIdentity.publicKey,
421
- serverNonce: bytesToBase64(serverNonce),
422
- serverUrl: input.serverUrl
423
- });
424
- const session = deriveSession(x25519.getSharedSecret(serverEphemeralPrivateKey, base64ToBytes(input.clientEphemeralPublicKey)), transcript, input.keyEpoch);
425
- const encryptedPayload = encryptWithKey(session.serverToMobileKey, "server", input.keyEpoch, 0, JSON.stringify({
426
- clientToken: input.clientToken,
427
- clientTokenExpiresAt: input.clientTokenExpiresAt
428
- }));
429
- session.nextServerCounter = 1;
430
- return {
431
- response: {
432
- encryptedPayload: encryptedPayload.ciphertext,
433
- keyEpoch: input.keyEpoch,
434
- protocolVersion: 1,
435
- serverEphemeralPublicKey: bytesToBase64(serverEphemeralPublicKey),
436
- serverNonce: bytesToBase64(serverNonce),
437
- serverSignature: bytesToBase64(ed25519.sign(transcript, input.serverIdentity.privateKey))
438
- },
439
- session
440
- };
441
- }
442
- function encryptForMobile(session, plaintext) {
443
- const envelope = encryptWithKey(session.serverToMobileKey, "server", session.keyEpoch, session.nextServerCounter, plaintext);
444
- session.nextServerCounter += 1;
445
- return envelope;
446
- }
447
- function decryptFromMobile(session, envelope) {
448
- if (!isEncryptedEnvelope(envelope) || envelope.sender !== "mobile") throw new Error("Expected encrypted mobile payload.");
449
- if (envelope.keyEpoch !== session.keyEpoch || envelope.counter <= session.lastMobileCounter) throw new Error("Rejected stale encrypted mobile payload.");
450
- const plaintext = decryptWithKey(session.mobileToServerKey, "mobile", envelope.counter, envelope.ciphertext);
451
- session.lastMobileCounter = envelope.counter;
452
- return plaintext;
453
- }
454
- function deriveSession(sharedSecret, transcript, keyEpoch) {
455
- const salt = sha256(transcript);
456
- const infoPrefix = `${handshakeTag}|${keyEpoch}|${bytesToBase64(sha256(transcript))}`;
457
- return {
458
- keyEpoch,
459
- lastMobileCounter: -1,
460
- mobileToServerKey: hkdf(sha256, sharedSecret, salt, utf8ToBytes(`${infoPrefix}|mobileToServer`), 32),
461
- nextServerCounter: 0,
462
- serverToMobileKey: hkdf(sha256, sharedSecret, salt, utf8ToBytes(`${infoPrefix}|serverToMobile`), 32)
463
- };
464
- }
465
- function pairingTranscript(input) {
466
- return new TextEncoder().encode(JSON.stringify({
467
- tag: handshakeTag,
468
- approvalCode: input.approvalCode,
469
- clientEphemeralPublicKey: input.clientEphemeralPublicKey,
470
- clientNonce: input.clientNonce,
471
- keyEpoch: input.keyEpoch,
472
- serverEphemeralPublicKey: input.serverEphemeralPublicKey,
473
- serverIdentityPublicKey: input.serverIdentityPublicKey,
474
- serverNonce: input.serverNonce,
475
- serverUrl: input.serverUrl
476
- }));
477
- }
478
- function encryptWithKey(key, sender, keyEpoch, counter, plaintext) {
479
- return {
480
- ciphertext: bytesToBase64(gcm(key, nonceFor(sender, counter)).encrypt(new TextEncoder().encode(plaintext))),
481
- counter,
482
- keyEpoch,
483
- protocolVersion: 1,
484
- sender
485
- };
486
- }
487
- function decryptWithKey(key, sender, counter, ciphertext) {
488
- const plaintext = gcm(key, nonceFor(sender, counter)).decrypt(base64ToBytes(ciphertext));
489
- return new TextDecoder().decode(plaintext);
490
- }
491
- function nonceFor(sender, counter) {
492
- const nonce = new Uint8Array(12);
493
- nonce[0] = sender === "mobile" ? 1 : 2;
494
- new DataView(nonce.buffer).setBigUint64(4, BigInt(counter), false);
495
- return nonce;
496
- }
497
- function isEncryptedEnvelope(value) {
498
- return value !== null && typeof value === "object" && "ciphertext" in value && "counter" in value && "keyEpoch" in value && "sender" in value && typeof value.ciphertext === "string" && typeof value.counter === "number" && typeof value.keyEpoch === "number" && (value.sender === "mobile" || value.sender === "server");
499
- }
500
- function bytesToBase64(bytes) {
501
- return fromByteArray(bytes);
502
- }
503
- function base64ToBytes(value) {
504
- return toByteArray(value);
505
- }
506
- //#endregion
507
- //#region src/app.ts
508
- const defaultWorkspacePath = process.cwd();
509
- const execFileAsync = promisify(execFile);
510
- const planModePromptPrefix = "Plan mode is active. Do not modify files or run implementation commands. Analyze the request, ask concise clarifying questions only if needed, and produce a concrete plan. Stop after the plan unless the user asks to execute.";
511
- const defaultWebPreviewPorts = [
512
- 3e3,
513
- 3001,
514
- 5173,
515
- 4173,
516
- 8080,
517
- 19006
518
- ];
519
- const PairApproveRequestSchema = z.object({ approvalCode: z.string().trim().min(1) });
520
- function createApp(options = {}) {
521
- const app = new Hono();
522
- const appServer = options.appServer === void 0 ? process.env.VITEST ? null : new CodexAppServerClient() : options.appServer;
523
- const codex = options.codex ?? createCodexClient();
524
- const workspacePath = resolve(options.workspacePath ?? process.env.CODEX_RELAY_WORKSPACE_PATH ?? defaultWorkspacePath);
525
- const threads = /* @__PURE__ */ new Map();
526
- const messagesByThreadId = /* @__PURE__ */ new Map();
527
- const liveThreads = /* @__PURE__ */ new Map();
528
- const pendingApprovals = /* @__PURE__ */ new Map();
529
- const queuedInputsByThreadId = /* @__PURE__ */ new Map();
530
- const steeringThreads = /* @__PURE__ */ new Set();
531
- const secureSessionsByTokenHash = /* @__PURE__ */ new Map();
532
- const threadOptions = { workingDirectory: workspacePath };
533
- app.use("*", cors());
534
- app.use("*", async (c, next) => {
535
- if (!options.pairing || c.req.method === "OPTIONS" || c.req.path.startsWith(apiPaths.pair)) {
536
- await next();
537
- return;
538
- }
539
- const token = parseBearerToken(c.req.header("authorization"));
540
- const tokenHash = token ? options.pairing.hashClientToken(token) : void 0;
541
- const validSession = tokenHash ? await options.pairing.sessions.getValidSession(tokenHash, Date.now()) : void 0;
542
- if (!tokenHash || !validSession) return c.json(apiError("unauthorized", "Pair this device with the Codex Relay server."), 401);
543
- if (options.pairing.serverIdentity && !secureSessionsByTokenHash.has(tokenHash)) return c.json(apiError("secure_session_required", "Secure session expired. Pair this device again."), 401);
544
- await next();
545
- });
546
- app.post(apiPaths.pair, async (c) => {
547
- if (!options.pairing) return c.json(apiError("pairing_disabled", "Pairing is not enabled on this server."), 404);
548
- options.pairing.onPairAttempt?.({ remoteAddress: c.req.header("x-forwarded-for") ?? c.req.header("x-real-ip") ?? c.req.header("cf-connecting-ip") });
549
- const parsed = await parsePlainJson(c.req.raw, PairRequestSchema);
550
- if (!parsed.success) return c.json(validationError(parsed.error), 400);
551
- if (!parsed.data.secure || !options.pairing.serverIdentity) return c.json(apiError("secure_pairing_required", "Pairing requires the secure QR approval flow."), 400);
552
- await options.pairing.sessions.pruneExpired(Date.now());
553
- const approvalCode = await createApprovalCode(options.pairing.sessions);
554
- const expiresAt = Date.now() + (options.pairing.approvalTtlMs ?? 300 * 1e3);
555
- await options.pairing.sessions.createPendingPairing({
556
- approved: false,
557
- approvalCode,
558
- clientEphemeralPublicKey: parsed.data.secure.clientEphemeralPublicKey,
559
- clientName: parsed.data.clientName,
560
- clientNonce: parsed.data.secure.clientNonce,
561
- expiresAt,
562
- serverUrl: new URL(c.req.url).origin
563
- });
564
- options.pairing.onPairApprovalRequested?.({
565
- approvalCode,
566
- clientName: parsed.data.clientName
567
- });
568
- const response = PairResponseSchema.parse({
569
- approvalCode,
570
- approvalExpiresAt: new Date(expiresAt).toISOString()
571
- });
572
- return c.json(response, 202);
573
- });
574
- app.get("/v1/pair/:approvalCode", async (c) => {
575
- if (!options.pairing?.serverIdentity) return c.json(apiError("pairing_disabled", "Pairing is not enabled on this server."), 404);
576
- const approvalCode = normalizeApprovalCode(c.req.param("approvalCode"));
577
- const pending = await options.pairing.sessions.getPendingPairing(approvalCode, Date.now());
578
- if (!pending) return c.json(apiError("pairing_expired", "The pairing approval code has expired."), 410);
579
- if (!pending.approved) return c.json(PairResponseSchema.parse({
580
- approvalCode,
581
- approvalExpiresAt: new Date(pending.expiresAt).toISOString()
582
- }), 202);
583
- await options.pairing.sessions.pruneExpired(Date.now());
584
- const clientToken = options.pairing.createClientToken();
585
- const expiresAt = Date.now() + options.pairing.tokenTtlMs;
586
- const tokenHash = options.pairing.hashClientToken(clientToken);
587
- const tokenCount = await options.pairing.sessions.createSession(tokenHash, {
588
- clientName: pending.clientName,
589
- expiresAt
590
- });
591
- await options.pairing.sessions.deletePendingPairing(approvalCode);
592
- options.pairing.onPaired?.({
593
- clientName: pending.clientName,
594
- tokenCount
595
- });
596
- const clientTokenExpiresAt = new Date(expiresAt).toISOString();
597
- const pairing = createSecurePairing({
598
- approvalCode,
599
- clientEphemeralPublicKey: pending.clientEphemeralPublicKey,
600
- clientNonce: pending.clientNonce,
601
- clientToken,
602
- clientTokenExpiresAt,
603
- keyEpoch: 1,
604
- serverIdentity: options.pairing.serverIdentity,
605
- serverUrl: pending.serverUrl
606
- });
607
- secureSessionsByTokenHash.set(tokenHash, pairing.session);
608
- return c.json(PairResponseSchema.parse({ secure: pairing.response }), 201);
609
- });
610
- app.post(apiPaths.pairApprove, async (c) => {
611
- if (!options.pairing) return c.json(apiError("pairing_disabled", "Pairing is not enabled on this server."), 404);
612
- if (options.pairing.approvalSecret && c.req.header("x-codex-relay-approve-secret") !== options.pairing.approvalSecret) return c.json(apiError("unauthorized", "Pairing approval must come from this machine."), 401);
613
- const parsed = await parsePlainJson(c.req.raw, PairApproveRequestSchema);
614
- if (!parsed.success) return c.json(validationError(parsed.error), 400);
615
- const approvalCode = normalizeApprovalCode(parsed.data.approvalCode);
616
- const pending = await options.pairing.sessions.approvePendingPairing(approvalCode, Date.now());
617
- if (!pending) return c.json(apiError("not_found", "No pending pairing request matches that code."), 404);
618
- options.pairing.onPairApproved?.({
619
- approvalCode,
620
- clientName: pending.clientName
621
- });
622
- return c.json({ ok: true });
623
- });
624
- app.post(apiPaths.sessionRefresh, async (c) => {
625
- if (!options.pairing) return c.json(apiError("pairing_disabled", "Pairing is not enabled on this server."), 404);
626
- const oldToken = parseBearerToken(c.req.header("authorization"));
627
- const oldSession = oldToken ? await getValidClientSession(options.pairing, oldToken) : void 0;
628
- if (!oldToken || !oldSession) return c.json(apiError("unauthorized", "Pair this device with the Codex Relay server."), 401);
629
- const clientToken = options.pairing.createClientToken();
630
- const expiresAt = Date.now() + options.pairing.tokenTtlMs;
631
- const oldTokenHash = options.pairing.hashClientToken(oldToken);
632
- const newTokenHash = options.pairing.hashClientToken(clientToken);
633
- const tokenCount = await options.pairing.sessions.rotateSession(oldTokenHash, newTokenHash, {
634
- clientName: oldSession.clientName,
635
- expiresAt
636
- });
637
- const secureSession = secureSessionsByTokenHash.get(oldTokenHash);
638
- if (secureSession) {
639
- secureSessionsByTokenHash.delete(oldTokenHash);
640
- secureSessionsByTokenHash.set(newTokenHash, secureSession);
641
- }
642
- options.pairing.onTokenRefreshed?.({
643
- clientName: oldSession.clientName,
644
- tokenCount
645
- });
646
- const response = PairResponseSchema.parse({
647
- clientToken,
648
- clientTokenExpiresAt: new Date(expiresAt).toISOString()
649
- });
650
- return secureJson(c, options.pairing, secureSessionsByTokenHash, response, 201);
651
- });
652
- app.get(apiPaths.status, (c) => {
653
- const response = StatusResponseSchema.parse({
654
- ok: true,
655
- service: "codex-relay-server",
656
- sdkAvailable: Boolean(codex),
657
- machineName: hostname(),
658
- workspacePath,
659
- threadCount: threads.size,
660
- appServerAvailable: Boolean(appServer)
661
- });
662
- return secureJson(c, options.pairing, secureSessionsByTokenHash, response);
663
- });
664
- app.get(apiPaths.workspaceDirectories, async (c) => {
665
- const targetPath = resolve(c.req.query("path") ?? workspacePath);
666
- try {
667
- if (!(await stat(targetPath)).isDirectory()) return secureJson(c, options.pairing, secureSessionsByTokenHash, apiError("invalid_workspace_path", "Workspace path must be a directory."), 400);
668
- const directories = (await readdir(targetPath, { withFileTypes: true })).filter((entry) => entry.isDirectory() && !entry.name.startsWith(".")).map((entry) => ({
669
- name: entry.name,
670
- path: resolve(targetPath, entry.name)
671
- })).sort((left, right) => left.name.localeCompare(right.name));
672
- const response = ListWorkspaceDirectoriesResponseSchema.parse({
673
- rootPath: workspacePath,
674
- path: targetPath,
675
- parentPath: dirname(targetPath) === targetPath ? null : dirname(targetPath),
676
- directories
677
- });
678
- return secureJson(c, options.pairing, secureSessionsByTokenHash, response);
679
- } catch (error) {
680
- return secureJson(c, options.pairing, secureSessionsByTokenHash, apiError("workspace_unavailable", errorMessage(error)), 400);
681
- }
682
- });
683
- app.get(apiPaths.workspaceChanges, async (c) => {
684
- try {
685
- const selectedWorkspacePath = await validateThreadWorkspacePath(workspacePath, c.req.query("workspacePath"));
686
- if (!selectedWorkspacePath.success) return secureJson(c, options.pairing, secureSessionsByTokenHash, apiError("invalid_workspace_path", selectedWorkspacePath.error), 400);
687
- const changes = await readWorkspaceChanges(selectedWorkspacePath.path);
688
- const response = WorkspaceChangesResponseSchema.parse({
689
- workspacePath: selectedWorkspacePath.path,
690
- ...changes
691
- });
692
- return secureJson(c, options.pairing, secureSessionsByTokenHash, response);
693
- } catch (error) {
694
- return secureJson(c, options.pairing, secureSessionsByTokenHash, apiError("workspace_changes_unavailable", errorMessage(error)), 400);
695
- }
696
- });
697
- app.post(apiPaths.workspaceCheckout, async (c) => {
698
- const parsed = await parseRequestJson(c, options.pairing, secureSessionsByTokenHash, CheckoutWorkspaceBranchRequestSchema);
699
- if (!parsed.success) return secureJson(c, options.pairing, secureSessionsByTokenHash, validationError(parsed.error), 400);
700
- try {
701
- const selectedWorkspacePath = await validateThreadWorkspacePath(workspacePath, parsed.data.workspacePath);
702
- if (!selectedWorkspacePath.success) return secureJson(c, options.pairing, secureSessionsByTokenHash, apiError("invalid_workspace_path", selectedWorkspacePath.error), 400);
703
- const output = await git(selectedWorkspacePath.path, ["checkout", parsed.data.branch]);
704
- const response = WorkspaceGitActionResponseSchema.parse({
705
- branch: await currentGitBranch(selectedWorkspacePath.path),
706
- message: `Checked out ${parsed.data.branch}.`,
707
- output
708
- });
709
- return secureJson(c, options.pairing, secureSessionsByTokenHash, response);
710
- } catch (error) {
711
- return secureJson(c, options.pairing, secureSessionsByTokenHash, apiError("workspace_checkout_failed", errorMessage(error)), 400);
712
- }
713
- });
714
- app.post(apiPaths.workspaceCommitPush, async (c) => {
715
- const parsed = await parseRequestJson(c, options.pairing, secureSessionsByTokenHash, CommitPushWorkspaceRequestSchema);
716
- if (!parsed.success) return secureJson(c, options.pairing, secureSessionsByTokenHash, validationError(parsed.error), 400);
717
- try {
718
- const selectedWorkspacePath = await validateThreadWorkspacePath(workspacePath, parsed.data.workspacePath);
719
- if (!selectedWorkspacePath.success) return secureJson(c, options.pairing, secureSessionsByTokenHash, apiError("invalid_workspace_path", selectedWorkspacePath.error), 400);
720
- await git(selectedWorkspacePath.path, ["add", "--all"]);
721
- const commitOutput = await git(selectedWorkspacePath.path, [
722
- "commit",
723
- "-m",
724
- parsed.data.message
725
- ]);
726
- const branch = await currentGitBranch(selectedWorkspacePath.path);
727
- const pushOutput = await git(selectedWorkspacePath.path, [
728
- "rev-parse",
729
- "--abbrev-ref",
730
- "@{upstream}"
731
- ]).catch(() => null) ? await git(selectedWorkspacePath.path, ["push"]) : branch ? await git(selectedWorkspacePath.path, [
732
- "push",
733
- "-u",
734
- "origin",
735
- branch
736
- ]) : await git(selectedWorkspacePath.path, ["push"]);
737
- const response = WorkspaceGitActionResponseSchema.parse({
738
- branch,
739
- message: "Committed and pushed workspace changes.",
740
- output: [commitOutput, pushOutput].filter(Boolean).join("\n")
741
- });
742
- return secureJson(c, options.pairing, secureSessionsByTokenHash, response);
743
- } catch (error) {
744
- return secureJson(c, options.pairing, secureSessionsByTokenHash, apiError("workspace_commit_push_failed", errorMessage(error)), 400);
745
- }
746
- });
747
- app.get(apiPaths.models, async (c) => {
748
- try {
749
- const models = appServer ? await appServer.listModels() : fallbackModels();
750
- return secureJson(c, options.pairing, secureSessionsByTokenHash, ListModelsResponseSchema.parse({ models: models.map(mapAppServerModel) }));
751
- } catch {
752
- return secureJson(c, options.pairing, secureSessionsByTokenHash, ListModelsResponseSchema.parse({ models: fallbackModels().map(mapAppServerModel) }));
753
- }
754
- });
755
- app.get(apiPaths.rateLimits, async (c) => {
756
- if (!appServer) return secureJson(c, options.pairing, secureSessionsByTokenHash, RateLimitsResponseSchema.parse({ buckets: [] }));
757
- try {
758
- const rateLimits = await appServer.readRateLimits();
759
- return secureJson(c, options.pairing, secureSessionsByTokenHash, RateLimitsResponseSchema.parse({ buckets: normalizeRateLimitBuckets(rateLimits) }));
760
- } catch (error) {
761
- return secureJson(c, options.pairing, secureSessionsByTokenHash, apiError("rate_limits_unavailable", errorMessage(error)), 502);
762
- }
763
- });
764
- app.get(apiPaths.threads, async (c) => {
765
- if (appServer) try {
766
- const appServerThreads = await appServer.listThreads();
767
- const response = ListThreadsResponseSchema.parse({
768
- threads: appServerThreads.map(mapAppServerThread),
769
- source: "app-server"
770
- });
771
- return secureJson(c, options.pairing, secureSessionsByTokenHash, response);
772
- } catch {}
773
- const response = ListThreadsResponseSchema.parse({
774
- threads: sortedThreads(threads),
775
- source: "memory"
776
- });
777
- return secureJson(c, options.pairing, secureSessionsByTokenHash, response);
778
- });
779
- app.get("/v1/threads/:threadId", async (c) => {
780
- const threadId = c.req.param("threadId");
781
- if (appServer) try {
782
- const thread = await appServer.readThread(threadId);
783
- return secureJson(c, options.pairing, secureSessionsByTokenHash, ThreadDetailResponseSchema.parse({
784
- thread: mapAppServerThread(thread),
785
- messages: mapAppServerMessages(thread)
786
- }));
787
- } catch {}
788
- const thread = threads.get(threadId);
789
- if (!thread) return secureJson(c, options.pairing, secureSessionsByTokenHash, apiError("not_found", `Thread ${threadId} is not known to this server.`), 404);
790
- return secureJson(c, options.pairing, secureSessionsByTokenHash, ThreadDetailResponseSchema.parse({
791
- thread,
792
- messages: messagesByThreadId.get(threadId) ?? []
793
- }));
794
- });
795
- app.get("/v1/threads/:threadId/context-window", async (c) => {
796
- const threadId = c.req.param("threadId");
797
- const result = readLatestContextWindowUsage({ threadId });
798
- return secureJson(c, options.pairing, secureSessionsByTokenHash, ThreadContextWindowResponseSchema.parse({
799
- rolloutPath: result.rolloutPath,
800
- threadId,
801
- usage: result.usage
802
- }));
803
- });
804
- app.get("/openapi.json", (c) => secureJson(c, options.pairing, secureSessionsByTokenHash, createOpenApiDocument()));
805
- app.post("/v1/approvals/:approvalId", async (c) => {
806
- const approvalId = c.req.param("approvalId");
807
- const pending = pendingApprovals.get(approvalId);
808
- if (!pending) return secureJson(c, options.pairing, secureSessionsByTokenHash, apiError("not_found", "This approval request is no longer pending."), 404);
809
- const parsed = await parseRequestJson(c, options.pairing, secureSessionsByTokenHash, ResolveApprovalRequestSchema);
810
- if (!parsed.success) return secureJson(c, options.pairing, secureSessionsByTokenHash, validationError(parsed.error), 400);
811
- pendingApprovals.delete(approvalId);
812
- await resolveAppServerRequest(pending, parsed.data.decision, parsed.data.answers ?? []);
813
- return secureJson(c, options.pairing, secureSessionsByTokenHash, ResolveApprovalResponseSchema.parse({ ok: true }));
814
- });
815
- app.post(apiPaths.threads, async (c) => {
816
- const parsed = await parseRequestJson(c, options.pairing, secureSessionsByTokenHash, CreateThreadRequestSchema);
817
- if (!parsed.success) return secureJson(c, options.pairing, secureSessionsByTokenHash, validationError(parsed.error), 400);
818
- const selectedWorkspacePath = await validateThreadWorkspacePath(workspacePath, parsed.data.workspacePath);
819
- if (!selectedWorkspacePath.success) return secureJson(c, options.pairing, secureSessionsByTokenHash, apiError("invalid_workspace_path", selectedWorkspacePath.error), 400);
820
- const { threadId } = appServer ? await createAppServerThreadRecord({
821
- appServer,
822
- messagesByThreadId,
823
- options: parsed.data,
824
- threads,
825
- title: parsed.data.title,
826
- workspacePath: selectedWorkspacePath.path
827
- }) : createThreadRecord({
828
- codex,
829
- liveThreads,
830
- messagesByThreadId,
831
- threads,
832
- title: parsed.data.title,
833
- prompt: parsed.data.prompt,
834
- threadOptions: buildThreadOptions({
835
- ...threadOptions,
836
- workingDirectory: selectedWorkspacePath.path
837
- }, parsed.data)
838
- });
839
- if (!parsed.data.prompt) {
840
- const response = {
841
- thread: threads.get(threadId),
842
- messages: messagesByThreadId.get(threadId) ?? []
843
- };
844
- return secureJson(c, options.pairing, secureSessionsByTokenHash, response, 201);
845
- }
846
- const response = await runPromptBuffered({
847
- codex,
848
- liveThreads,
849
- messagesByThreadId,
850
- prompt: parsed.data.prompt,
851
- attachments: parsed.data.attachments ?? [],
852
- threadId,
853
- threadOptions: {
854
- ...threadOptions,
855
- workingDirectory: selectedWorkspacePath.path
856
- },
857
- runOptions: parsed.data,
858
- threads
859
- });
860
- return secureJson(c, options.pairing, secureSessionsByTokenHash, response.body, response.status);
861
- });
862
- app.post("/v1/threads/:threadId/runs", async (c) => {
863
- const threadId = c.req.param("threadId");
864
- const knownThread = await ensureKnownThread({
865
- appServer,
866
- threadId,
867
- messagesByThreadId,
868
- threads
869
- });
870
- if (!knownThread) return secureJson(c, options.pairing, secureSessionsByTokenHash, apiError("not_found", `Thread ${threadId} is not known to this server.`), 404);
871
- const parsed = await parseRequestJson(c, options.pairing, secureSessionsByTokenHash, RunThreadRequestSchema);
872
- if (!parsed.success) return secureJson(c, options.pairing, secureSessionsByTokenHash, validationError(parsed.error), 400);
873
- const response = await runPromptBuffered({
874
- codex,
875
- liveThreads,
876
- messagesByThreadId,
877
- prompt: parsed.data.prompt,
878
- attachments: parsed.data.attachments ?? [],
879
- threadId,
880
- threadOptions: {
881
- ...threadOptions,
882
- workingDirectory: knownThread.cwd ?? workspacePath
883
- },
884
- runOptions: parsed.data,
885
- threads
886
- });
887
- return secureJson(c, options.pairing, secureSessionsByTokenHash, response.body, response.status);
888
- });
889
- app.post("/v1/threads/:threadId/input", async (c) => {
890
- const threadId = c.req.param("threadId");
891
- const knownThread = await ensureKnownThread({
892
- appServer,
893
- threadId,
894
- messagesByThreadId,
895
- threads
896
- });
897
- if (!knownThread) return secureJson(c, options.pairing, secureSessionsByTokenHash, apiError("not_found", `Thread ${threadId} is not known to this server.`), 404);
898
- if (!appServer) return secureJson(c, options.pairing, secureSessionsByTokenHash, apiError("unsupported", "Running-thread input requires the Codex app-server."), 409);
899
- if (knownThread.state !== "running") return secureJson(c, options.pairing, secureSessionsByTokenHash, apiError("thread_not_running", `Thread ${threadId} is not currently running.`), 409);
900
- const parsed = await parseRequestJson(c, options.pairing, secureSessionsByTokenHash, RunThreadRequestSchema);
901
- if (!parsed.success) return secureJson(c, options.pairing, secureSessionsByTokenHash, validationError(parsed.error), 400);
902
- const queuedInputs = queuedInputsByThreadId.get(threadId) ?? [];
903
- const queuedInput = {
904
- attachments: parsed.data.attachments ?? [],
905
- prompt: parsed.data.prompt,
906
- runOptions: parsed.data,
907
- workspacePath: knownThread.cwd ?? workspacePath
908
- };
909
- const shouldQueue = steeringThreads.has(threadId) || queuedInputs.length > 0;
910
- if (shouldQueue) {
911
- queuedInputs.push(queuedInput);
912
- queuedInputsByThreadId.set(threadId, queuedInputs);
913
- } else {
914
- steeringThreads.add(threadId);
915
- await startAppServerTurn(appServer, threadId, queuedInput);
916
- }
917
- const thread = updateThread(threads, messagesByThreadId, threadId, {
918
- state: "running",
919
- lastPrompt: promptWithAttachments(parsed.data.prompt, parsed.data.attachments ?? []),
920
- lastError: void 0
921
- });
922
- const response = SubmitThreadInputResponseSchema.parse({
923
- acceptedAs: shouldQueue ? "queued" : "steering",
924
- queueLength: queuedInputsByThreadId.get(threadId)?.length ?? 0,
925
- thread
926
- });
927
- return secureJson(c, options.pairing, secureSessionsByTokenHash, response, 202);
928
- });
929
- app.post("/v1/threads/:threadId/runs/stream", async (c) => {
930
- const threadId = c.req.param("threadId");
931
- const knownThread = await ensureKnownThread({
932
- appServer,
933
- threadId,
934
- messagesByThreadId,
935
- threads
936
- });
937
- if (!knownThread) return secureJson(c, options.pairing, secureSessionsByTokenHash, apiError("not_found", `Thread ${threadId} is not known to this server.`), 404);
938
- const parsed = await parseRequestJson(c, options.pairing, secureSessionsByTokenHash, RunThreadRequestSchema);
939
- if (!parsed.success) return secureJson(c, options.pairing, secureSessionsByTokenHash, validationError(parsed.error), 400);
940
- const encoder = new TextEncoder();
941
- const secureSession = getSecureSessionForRequest(c, options.pairing, secureSessionsByTokenHash);
942
- const stream = new ReadableStream({ start(controller) {
943
- const stopPreviewMonitor = startWebPreviewTargetMonitor({
944
- bridgeUrl: c.req.url,
945
- send(target) {
946
- sendSse(controller, encoder, secureSession, {
947
- type: "thread.preview_target.detected",
948
- threadId,
949
- target
950
- });
951
- }
952
- });
953
- runPromptStreamed({
954
- appServer,
955
- controller,
956
- codex,
957
- encoder,
958
- liveThreads,
959
- messagesByThreadId,
960
- pendingApprovals,
961
- queuedInputsByThreadId,
962
- prompt: parsed.data.prompt,
963
- attachments: parsed.data.attachments ?? [],
964
- secureSession,
965
- steeringThreads,
966
- threadId,
967
- threadOptions: {
968
- ...threadOptions,
969
- workingDirectory: knownThread.cwd ?? workspacePath
970
- },
971
- runOptions: parsed.data,
972
- threads
973
- }).finally(stopPreviewMonitor);
974
- } });
975
- return new Response(stream, { headers: {
976
- "cache-control": "no-cache",
977
- connection: "keep-alive",
978
- "content-type": "text/event-stream; charset=utf-8"
979
- } });
980
- });
981
- return app;
982
- }
983
- function parseBearerToken(value) {
984
- return (value?.match(/^Bearer\s+(.+)$/i))?.[1];
985
- }
986
- function normalizeApprovalCode(value) {
987
- const normalized = value.toUpperCase().replace(/[^A-Z0-9]/g, "").replaceAll("O", "0").replaceAll("I", "1");
988
- return normalized.length === 8 ? `${normalized.slice(0, 4)}-${normalized.slice(4)}` : normalized;
989
- }
990
- async function validateThreadWorkspacePath(rootPath, requestedPath) {
991
- const resolved = resolve(requestedPath ?? rootPath);
992
- try {
993
- if (!(await stat(resolved)).isDirectory()) return {
994
- success: false,
995
- error: "New chat workspace must be a directory."
996
- };
997
- } catch (error) {
998
- return {
999
- success: false,
1000
- error: errorMessage(error)
1001
- };
1002
- }
1003
- return {
1004
- success: true,
1005
- path: resolved
1006
- };
1007
- }
1008
- async function createApprovalCode(sessions) {
1009
- for (let attempt = 0; attempt < 8; attempt += 1) {
1010
- const code = normalizeApprovalCode(crypto.randomUUID().replace(/-/g, "").slice(0, 8));
1011
- if (!await sessions.getPendingPairing(code, Date.now())) return code;
1012
- }
1013
- throw new Error("Unable to allocate a pairing approval code.");
1014
- }
1015
- async function getValidClientSession(pairing, token) {
1016
- return pairing.sessions.getValidSession(pairing.hashClientToken(token), Date.now());
1017
- }
1018
- function createThreadRecord(input) {
1019
- const now = (/* @__PURE__ */ new Date()).toISOString();
1020
- const thread = input.codex.startThread(input.threadOptions);
1021
- const threadId = getThreadId(thread) ?? `local-${crypto.randomUUID()}`;
1022
- const metadata = ThreadSummarySchema.parse({
1023
- id: threadId,
1024
- title: input.title ?? titleFromPrompt(input.prompt) ?? "New Codex thread",
1025
- createdAt: now,
1026
- updatedAt: now,
1027
- state: "idle",
1028
- cwd: input.threadOptions?.workingDirectory,
1029
- messageCount: 0
1030
- });
1031
- input.threads.set(threadId, metadata);
1032
- input.messagesByThreadId.set(threadId, []);
1033
- input.liveThreads.set(threadId, thread);
1034
- return { threadId };
1035
- }
1036
- async function createAppServerThreadRecord(input) {
1037
- const runtime = resolveAppServerRuntime(input.options.runtimeMode, input.workspacePath);
1038
- const thread = await input.appServer.startThread({
1039
- approvalPolicy: runtime.approvalPolicy,
1040
- cwd: input.workspacePath,
1041
- experimentalRawEvents: false,
1042
- model: input.options.model ?? null,
1043
- persistExtendedHistory: true,
1044
- sandbox: runtime.sandbox
1045
- });
1046
- const metadata = mapAppServerThread({
1047
- ...thread,
1048
- name: input.title ?? thread.name,
1049
- preview: input.title ?? thread.preview
1050
- });
1051
- input.threads.set(thread.id, metadata);
1052
- input.messagesByThreadId.set(thread.id, []);
1053
- return { threadId: thread.id };
1054
- }
1055
- async function runPromptBuffered(input) {
1056
- const displayPrompt = promptWithAttachments(input.prompt, input.attachments);
1057
- const runPrompt = promptWithAttachments(promptForCollaborationMode(input.prompt, input.runOptions.collaborationMode), input.attachments);
1058
- const userMessage = appendMessage(input.messagesByThreadId, input.threadId, {
1059
- role: "user",
1060
- content: displayPrompt,
1061
- details: attachmentDetails(input.attachments)
1062
- });
1063
- updateThread(input.threads, input.messagesByThreadId, input.threadId, {
1064
- state: "running",
1065
- lastPrompt: displayPrompt,
1066
- lastError: void 0,
1067
- title: maybeReplaceDefaultTitle(input.threads.get(input.threadId)?.title, displayPrompt)
1068
- });
1069
- try {
1070
- const options = buildThreadOptions(input.threadOptions, input.runOptions);
1071
- const thread = hasExplicitRunOptions(input.runOptions) ? input.codex.resumeThread(input.threadId, options) : input.liveThreads.get(input.threadId) ?? input.codex.resumeThread(input.threadId, options);
1072
- input.liveThreads.set(input.threadId, thread);
1073
- const result = stringifyRunResult(await thread.run(runPrompt));
1074
- const actualThreadId = replaceLocalThreadId(input.threads, input.messagesByThreadId, input.liveThreads, input.threadId, getThreadId(thread));
1075
- const assistantMessage = appendMessage(input.messagesByThreadId, actualThreadId, {
1076
- role: "assistant",
1077
- content: result,
1078
- state: "completed"
1079
- });
1080
- return {
1081
- status: 200,
1082
- body: {
1083
- thread: updateThread(input.threads, input.messagesByThreadId, actualThreadId, {
1084
- state: "completed",
1085
- lastResult: result,
1086
- lastError: void 0
1087
- }),
1088
- messages: [userMessage, assistantMessage],
1089
- result
1090
- }
1091
- };
1092
- } catch (error) {
1093
- const failed = updateThread(input.threads, input.messagesByThreadId, input.threadId, {
1094
- state: "failed",
1095
- lastError: errorMessage(error)
1096
- });
1097
- appendMessage(input.messagesByThreadId, input.threadId, {
1098
- role: "error",
1099
- content: failed.lastError ?? "Codex run failed.",
1100
- state: "failed"
1101
- });
1102
- return {
1103
- status: 500,
1104
- body: apiError("codex_run_failed", failed.lastError ?? "Codex run failed.")
1105
- };
1106
- }
1107
- }
1108
- async function runPromptStreamed(input) {
1109
- if (input.appServer) {
1110
- await runAppServerPromptStreamed({
1111
- appServer: input.appServer,
1112
- attachments: input.attachments,
1113
- controller: input.controller,
1114
- encoder: input.encoder,
1115
- messagesByThreadId: input.messagesByThreadId,
1116
- pendingApprovals: input.pendingApprovals,
1117
- queuedInputsByThreadId: input.queuedInputsByThreadId,
1118
- prompt: input.prompt,
1119
- runOptions: input.runOptions,
1120
- secureSession: input.secureSession,
1121
- steeringThreads: input.steeringThreads,
1122
- threadId: input.threadId,
1123
- threads: input.threads,
1124
- workspacePath: input.threadOptions?.workingDirectory ?? defaultWorkspacePath
1125
- });
1126
- return;
1127
- }
1128
- let activeThreadId = input.threadId;
1129
- let assistantMessage;
1130
- try {
1131
- const displayPrompt = promptWithAttachments(input.prompt, input.attachments);
1132
- const runPrompt = promptWithAttachments(promptForCollaborationMode(input.prompt, input.runOptions.collaborationMode), input.attachments);
1133
- const userMessage = appendMessage(input.messagesByThreadId, activeThreadId, {
1134
- role: "user",
1135
- content: displayPrompt,
1136
- details: attachmentDetails(input.attachments)
1137
- });
1138
- let threadSummary = updateThread(input.threads, input.messagesByThreadId, activeThreadId, {
1139
- state: "running",
1140
- lastPrompt: displayPrompt,
1141
- lastError: void 0,
1142
- title: maybeReplaceDefaultTitle(input.threads.get(activeThreadId)?.title, displayPrompt)
1143
- });
1144
- sendSse(input.controller, input.encoder, input.secureSession, {
1145
- type: "thread.message.created",
1146
- thread: threadSummary,
1147
- message: userMessage
1148
- });
1149
- sendSse(input.controller, input.encoder, input.secureSession, {
1150
- type: "thread.state.changed",
1151
- thread: threadSummary
1152
- });
1153
- const options = buildThreadOptions(input.threadOptions, input.runOptions);
1154
- const thread = hasExplicitRunOptions(input.runOptions) ? input.codex.resumeThread(activeThreadId, options) : input.liveThreads.get(activeThreadId) ?? input.codex.resumeThread(activeThreadId, options);
1155
- input.liveThreads.set(activeThreadId, thread);
1156
- if (!thread.runStreamed) {
1157
- const result = stringifyRunResult(await thread.run(runPrompt));
1158
- activeThreadId = replaceLocalThreadId(input.threads, input.messagesByThreadId, input.liveThreads, activeThreadId, getThreadId(thread));
1159
- assistantMessage = appendMessage(input.messagesByThreadId, activeThreadId, {
1160
- role: "assistant",
1161
- content: result,
1162
- state: "completed"
1163
- });
1164
- threadSummary = updateThread(input.threads, input.messagesByThreadId, activeThreadId, {
1165
- state: "completed",
1166
- lastResult: result
1167
- });
1168
- sendSse(input.controller, input.encoder, input.secureSession, {
1169
- type: "thread.message.completed",
1170
- thread: threadSummary,
1171
- message: assistantMessage
1172
- });
1173
- sendSse(input.controller, input.encoder, input.secureSession, {
1174
- type: "thread.state.changed",
1175
- thread: threadSummary
1176
- });
1177
- return;
1178
- }
1179
- const streamed = await thread.runStreamed(runPrompt);
1180
- activeThreadId = replaceLocalThreadId(input.threads, input.messagesByThreadId, input.liveThreads, activeThreadId, getThreadId(thread));
1181
- assistantMessage = appendMessage(input.messagesByThreadId, activeThreadId, {
1182
- role: "assistant",
1183
- content: "",
1184
- state: "streaming"
1185
- });
1186
- threadSummary = updateThread(input.threads, input.messagesByThreadId, activeThreadId, { state: "running" });
1187
- sendSse(input.controller, input.encoder, input.secureSession, {
1188
- type: "thread.message.created",
1189
- thread: threadSummary,
1190
- message: assistantMessage
1191
- });
1192
- for await (const event of streamed.events) {
1193
- const kind = classifyStreamEvent(event);
1194
- const text = extractStreamText(event);
1195
- if (kind === "error") throw new Error(text ?? "Codex run failed.");
1196
- if (!text) continue;
1197
- if (kind === "assistant") {
1198
- assistantMessage = appendMessageDelta(input.messagesByThreadId, activeThreadId, assistantMessage.id, text);
1199
- updateThread(input.threads, input.messagesByThreadId, activeThreadId, {
1200
- state: "running",
1201
- lastResult: assistantMessage.content
1202
- });
1203
- sendSse(input.controller, input.encoder, input.secureSession, {
1204
- type: "thread.message.delta",
1205
- threadId: activeThreadId,
1206
- messageId: assistantMessage.id,
1207
- delta: text
1208
- });
1209
- } else {
1210
- const structured = structuredStreamMessage(kind, event, text);
1211
- const statusMessage = appendMessage(input.messagesByThreadId, activeThreadId, {
1212
- role: kind,
1213
- kind: structured.kind,
1214
- content: structured.content,
1215
- details: structured.details,
1216
- state: "completed"
1217
- });
1218
- threadSummary = updateThread(input.threads, input.messagesByThreadId, activeThreadId, { state: "running" });
1219
- sendSse(input.controller, input.encoder, input.secureSession, {
1220
- type: "thread.message.created",
1221
- thread: threadSummary,
1222
- message: statusMessage
1223
- });
1224
- }
1225
- }
1226
- assistantMessage = updateMessage(input.messagesByThreadId, activeThreadId, assistantMessage.id, { state: "completed" });
1227
- threadSummary = updateThread(input.threads, input.messagesByThreadId, activeThreadId, {
1228
- state: "completed",
1229
- lastResult: assistantMessage.content,
1230
- lastError: void 0
1231
- });
1232
- sendSse(input.controller, input.encoder, input.secureSession, {
1233
- type: "thread.message.completed",
1234
- thread: threadSummary,
1235
- message: assistantMessage
1236
- });
1237
- sendSse(input.controller, input.encoder, input.secureSession, {
1238
- type: "thread.state.changed",
1239
- thread: threadSummary
1240
- });
1241
- } catch (error) {
1242
- input.steeringThreads.delete(activeThreadId);
1243
- const threadSummary = updateThread(input.threads, input.messagesByThreadId, activeThreadId, {
1244
- state: "failed",
1245
- lastError: errorMessage(error)
1246
- });
1247
- const errorBody = apiError("codex_run_failed", threadSummary.lastError ?? "Codex run failed.");
1248
- appendMessage(input.messagesByThreadId, activeThreadId, {
1249
- role: "error",
1250
- content: errorBody.error.message,
1251
- state: "failed"
1252
- });
1253
- sendSse(input.controller, input.encoder, input.secureSession, {
1254
- type: "thread.error",
1255
- thread: threadSummary,
1256
- error: errorBody.error
1257
- });
1258
- } finally {
1259
- input.controller.close();
1260
- }
1261
- }
1262
- async function startAppServerTurn(appServer, threadId, input) {
1263
- const runtime = resolveAppServerRuntime(input.runOptions.runtimeMode, input.workspacePath);
1264
- return appServer.startTurn({
1265
- approvalPolicy: runtime.approvalPolicy,
1266
- collaborationMode: appServerCollaborationMode(input.runOptions),
1267
- cwd: input.workspacePath,
1268
- effort: input.runOptions.reasoningEffort ?? null,
1269
- input: [{
1270
- type: "text",
1271
- text: input.prompt,
1272
- text_elements: []
1273
- }, ...input.attachments.map((attachment) => ({
1274
- type: "image",
1275
- url: attachment.dataUri
1276
- }))],
1277
- model: input.runOptions.model ?? null,
1278
- sandboxPolicy: runtime.sandboxPolicy,
1279
- threadId
1280
- });
1281
- }
1282
- function shiftQueuedInput(queuedInputsByThreadId, threadId) {
1283
- const queuedInputs = queuedInputsByThreadId.get(threadId);
1284
- const nextInput = queuedInputs?.shift();
1285
- if (!queuedInputs || queuedInputs.length === 0) queuedInputsByThreadId.delete(threadId);
1286
- return nextInput;
1287
- }
1288
- async function runAppServerPromptStreamed(input) {
1289
- let activeThreadId = input.threadId;
1290
- let activeTurnId;
1291
- let assistantMessageId;
1292
- const prompt = promptWithAttachments(input.prompt, input.attachments);
1293
- const userMessage = appendMessage(input.messagesByThreadId, activeThreadId, {
1294
- role: "user",
1295
- content: prompt,
1296
- details: attachmentDetails(input.attachments)
1297
- });
1298
- let threadSummary = updateThread(input.threads, input.messagesByThreadId, activeThreadId, {
1299
- state: "running",
1300
- lastPrompt: prompt,
1301
- lastError: void 0,
1302
- title: maybeReplaceDefaultTitle(input.threads.get(activeThreadId)?.title, prompt)
1303
- });
1304
- sendSse(input.controller, input.encoder, input.secureSession, {
1305
- type: "thread.message.created",
1306
- thread: threadSummary,
1307
- message: userMessage
1308
- });
1309
- sendSse(input.controller, input.encoder, input.secureSession, {
1310
- type: "thread.state.changed",
1311
- thread: threadSummary
1312
- });
1313
- const cleanupRequestHandler = input.appServer.onRequest((request) => {
1314
- if (!isApprovalServerRequest(request.method)) {
1315
- input.appServer.rejectRequest(request.id, -32601, `${request.method} is not supported by Codex Relay mobile yet.`);
1316
- return;
1317
- }
1318
- const approval = approvalMessageFromRequest(request);
1319
- if (!approval || approval.threadId !== activeThreadId) {
1320
- input.appServer.rejectRequest(request.id, -32602, "Approval request is malformed.");
1321
- return;
1322
- }
1323
- input.pendingApprovals.set(approval.approvalId, {
1324
- appServer: input.appServer,
1325
- kind: approval.kind,
1326
- method: request.method,
1327
- requestId: request.id,
1328
- threadId: activeThreadId
1329
- });
1330
- const message = appendMessage(input.messagesByThreadId, activeThreadId, {
1331
- role: "status",
1332
- kind: approval.kind === "structuredUserInput" ? "structuredUserInput" : "approvalRequest",
1333
- content: approval.content,
1334
- details: approval.details,
1335
- state: "completed",
1336
- turnId: approval.turnId
1337
- });
1338
- threadSummary = updateThread(input.threads, input.messagesByThreadId, activeThreadId, { state: "running" });
1339
- sendSse(input.controller, input.encoder, input.secureSession, {
1340
- type: "thread.message.created",
1341
- thread: threadSummary,
1342
- message
1343
- });
1344
- });
1345
- const completed = new Promise((resolve, reject) => {
1346
- const cleanupNotificationHandler = input.appServer.onNotification((notification) => {
1347
- const params = recordParams(notification);
1348
- const threadId = firstString(params, ["threadId"]);
1349
- if (threadId && threadId !== activeThreadId) return;
1350
- try {
1351
- switch (notification.method) {
1352
- case "thread/status/changed": {
1353
- const status = params?.status;
1354
- threadSummary = updateThread(input.threads, input.messagesByThreadId, activeThreadId, { state: mapAppServerThreadState(status) });
1355
- sendSse(input.controller, input.encoder, input.secureSession, {
1356
- type: "thread.state.changed",
1357
- thread: threadSummary
1358
- });
1359
- return;
1360
- }
1361
- case "turn/started":
1362
- activeTurnId = firstString(params, ["turnId"]) ?? turnIdFromParams(params);
1363
- return;
1364
- case "item/started":
1365
- case "item/completed": {
1366
- const item = params?.item;
1367
- if (!item || typeof item !== "object") return;
1368
- const turnId = firstString(params, ["turnId"]) ?? activeTurnId;
1369
- const message = upsertAppServerItemMessage(input.messagesByThreadId, activeThreadId, turnId, item);
1370
- if (!message) return;
1371
- if (message.role === "assistant") assistantMessageId = message.id;
1372
- threadSummary = updateThread(input.threads, input.messagesByThreadId, activeThreadId, {
1373
- state: "running",
1374
- lastResult: message.role === "assistant" ? message.content : void 0
1375
- });
1376
- sendSse(input.controller, input.encoder, input.secureSession, {
1377
- type: notification.method === "item/completed" && message.role === "assistant" ? "thread.message.completed" : "thread.message.created",
1378
- thread: threadSummary,
1379
- message
1380
- });
1381
- return;
1382
- }
1383
- case "item/agentMessage/delta": {
1384
- const itemId = firstString(params, ["itemId"]);
1385
- const delta = firstString(params, ["delta"]);
1386
- if (!itemId || !delta) return;
1387
- if (!input.messagesByThreadId.get(activeThreadId)?.some((item) => item.id === itemId)) appendMessageWithId(input.messagesByThreadId, activeThreadId, itemId, {
1388
- role: "assistant",
1389
- content: "",
1390
- state: "streaming",
1391
- turnId: firstString(params, ["turnId"]) ?? activeTurnId
1392
- });
1393
- assistantMessageId = itemId;
1394
- const message = appendMessageDelta(input.messagesByThreadId, activeThreadId, itemId, delta);
1395
- updateThread(input.threads, input.messagesByThreadId, activeThreadId, {
1396
- state: "running",
1397
- lastResult: message.content
1398
- });
1399
- sendSse(input.controller, input.encoder, input.secureSession, {
1400
- type: "thread.message.delta",
1401
- threadId: activeThreadId,
1402
- messageId: itemId,
1403
- delta
1404
- });
1405
- return;
1406
- }
1407
- case "turn/plan/updated": {
1408
- const explanation = firstString(params, ["explanation"]);
1409
- const plan = Array.isArray(params?.plan) ? params.plan : [];
1410
- const content = [explanation, ...plan.map((step) => planStepText(step))].filter(Boolean).join("\n");
1411
- const message = appendMessage(input.messagesByThreadId, activeThreadId, {
1412
- role: "status",
1413
- kind: "plan",
1414
- content: content || "Plan updated",
1415
- details: {
1416
- explanation,
1417
- plan
1418
- },
1419
- state: "completed",
1420
- turnId: firstString(params, ["turnId"]) ?? activeTurnId
1421
- });
1422
- threadSummary = updateThread(input.threads, input.messagesByThreadId, activeThreadId, { state: "running" });
1423
- sendSse(input.controller, input.encoder, input.secureSession, {
1424
- type: "thread.message.created",
1425
- thread: threadSummary,
1426
- message
1427
- });
1428
- return;
1429
- }
1430
- case "turn/completed": {
1431
- const state = turnStatus(params) === "failed" ? "failed" : "completed";
1432
- if (assistantMessageId) {
1433
- const completedMessage = updateMessage(input.messagesByThreadId, activeThreadId, assistantMessageId, { state: "completed" });
1434
- sendSse(input.controller, input.encoder, input.secureSession, {
1435
- type: "thread.message.completed",
1436
- thread: threadSummary,
1437
- message: completedMessage
1438
- });
1439
- }
1440
- const nextQueuedInput = state === "completed" ? shiftQueuedInput(input.queuedInputsByThreadId, activeThreadId) : void 0;
1441
- threadSummary = updateThread(input.threads, input.messagesByThreadId, activeThreadId, {
1442
- state,
1443
- lastError: state === "failed" ? turnErrorMessage(params) : void 0
1444
- });
1445
- sendSse(input.controller, input.encoder, input.secureSession, {
1446
- type: "thread.state.changed",
1447
- thread: threadSummary
1448
- });
1449
- if (nextQueuedInput) {
1450
- assistantMessageId = void 0;
1451
- input.steeringThreads.add(activeThreadId);
1452
- threadSummary = updateThread(input.threads, input.messagesByThreadId, activeThreadId, {
1453
- state: "running",
1454
- lastPrompt: promptWithAttachments(nextQueuedInput.prompt, nextQueuedInput.attachments),
1455
- lastError: void 0
1456
- });
1457
- sendSse(input.controller, input.encoder, input.secureSession, {
1458
- type: "thread.state.changed",
1459
- thread: threadSummary
1460
- });
1461
- startAppServerTurn(input.appServer, activeThreadId, nextQueuedInput).then((turn) => {
1462
- activeTurnId = turn.id;
1463
- }).catch((error) => {
1464
- cleanupNotificationHandler();
1465
- reject(error);
1466
- });
1467
- return;
1468
- }
1469
- input.steeringThreads.delete(activeThreadId);
1470
- cleanupNotificationHandler();
1471
- resolve();
1472
- return;
1473
- }
1474
- }
1475
- } catch (error) {
1476
- cleanupNotificationHandler();
1477
- reject(error);
1478
- }
1479
- });
1480
- });
1481
- try {
1482
- activeTurnId = (await startAppServerTurn(input.appServer, activeThreadId, {
1483
- attachments: input.attachments,
1484
- prompt,
1485
- runOptions: input.runOptions,
1486
- workspacePath: input.workspacePath
1487
- })).id;
1488
- await completed;
1489
- } catch (error) {
1490
- const threadSummary = updateThread(input.threads, input.messagesByThreadId, activeThreadId, {
1491
- state: "failed",
1492
- lastError: errorMessage(error)
1493
- });
1494
- const errorBody = apiError("codex_run_failed", threadSummary.lastError ?? "Codex run failed.");
1495
- appendMessage(input.messagesByThreadId, activeThreadId, {
1496
- role: "error",
1497
- content: errorBody.error.message,
1498
- state: "failed"
1499
- });
1500
- sendSse(input.controller, input.encoder, input.secureSession, {
1501
- type: "thread.error",
1502
- thread: threadSummary,
1503
- error: errorBody.error
1504
- });
1505
- } finally {
1506
- cleanupRequestHandler();
1507
- input.controller.close();
1508
- }
1509
- }
1510
- async function parseRequestJson(c, pairing, secureSessionsByTokenHash, schema) {
1511
- let payload;
1512
- try {
1513
- payload = await c.req.raw.json();
1514
- } catch {
1515
- payload = {};
1516
- }
1517
- const secureSession = getSecureSessionForRequest(c, pairing, secureSessionsByTokenHash);
1518
- if (secureSession) {
1519
- const envelope = EncryptedPayloadSchema.safeParse(payload);
1520
- if (!envelope.success) return schema.safeParse({ __invalidEncryptedPayload: true });
1521
- try {
1522
- payload = JSON.parse(decryptFromMobile(secureSession, envelope.data));
1523
- } catch {
1524
- payload = { __invalidEncryptedPayload: true };
1525
- }
1526
- }
1527
- return schema.safeParse(payload);
1528
- }
1529
- async function parsePlainJson(request, schema) {
1530
- let payload;
1531
- try {
1532
- payload = await request.json();
1533
- } catch {
1534
- payload = {};
1535
- }
1536
- return schema.safeParse(payload);
1537
- }
1538
- function secureJson(c, pairing, secureSessionsByTokenHash, payload, status) {
1539
- const secureSession = getSecureSessionForRequest(c, pairing, secureSessionsByTokenHash);
1540
- if (!secureSession) return c.json(payload, status);
1541
- const encrypted = EncryptedPayloadSchema.parse(encryptForMobile(secureSession, JSON.stringify(payload)));
1542
- return c.json(encrypted, status);
1543
- }
1544
- function getSecureSessionForRequest(c, pairing, secureSessionsByTokenHash) {
1545
- const token = parseBearerToken(c.req.header("authorization"));
1546
- return token && pairing ? secureSessionsByTokenHash.get(pairing.hashClientToken(token)) : void 0;
1547
- }
1548
- function sortedThreads(threads) {
1549
- return [...threads.values()].sort((a, b) => b.updatedAt.localeCompare(a.updatedAt));
1550
- }
1551
- async function ensureKnownThread(input) {
1552
- const knownThread = input.threads.get(input.threadId);
1553
- if (knownThread) return knownThread;
1554
- if (!input.appServer) return;
1555
- try {
1556
- const appServerThread = await input.appServer.readThread(input.threadId);
1557
- const thread = mapAppServerThread(appServerThread);
1558
- input.threads.set(input.threadId, thread);
1559
- input.messagesByThreadId.set(input.threadId, mapAppServerMessages(appServerThread));
1560
- return thread;
1561
- } catch {
1562
- return;
1563
- }
1564
- }
1565
- function appendMessage(messagesByThreadId, threadId, input) {
1566
- const now = (/* @__PURE__ */ new Date()).toISOString();
1567
- const message = ChatMessageSchema.parse({
1568
- id: `msg-${crypto.randomUUID()}`,
1569
- threadId,
1570
- role: input.role,
1571
- kind: input.kind,
1572
- content: input.content,
1573
- details: input.details,
1574
- createdAt: now,
1575
- updatedAt: now,
1576
- state: input.state,
1577
- turnId: input.turnId
1578
- });
1579
- const messages = messagesByThreadId.get(threadId) ?? [];
1580
- messages.push(message);
1581
- messagesByThreadId.set(threadId, messages);
1582
- return message;
1583
- }
1584
- function appendMessageWithId(messagesByThreadId, threadId, id, input) {
1585
- const now = (/* @__PURE__ */ new Date()).toISOString();
1586
- const message = ChatMessageSchema.parse({
1587
- id,
1588
- threadId,
1589
- role: input.role,
1590
- kind: input.kind,
1591
- content: input.content,
1592
- details: input.details,
1593
- createdAt: now,
1594
- updatedAt: now,
1595
- state: input.state,
1596
- turnId: input.turnId
1597
- });
1598
- const messages = messagesByThreadId.get(threadId) ?? [];
1599
- messages.push(message);
1600
- messagesByThreadId.set(threadId, messages);
1601
- return message;
1602
- }
1603
- function upsertAppServerItemMessage(messagesByThreadId, threadId, turnId, item) {
1604
- const message = mapAppServerItem(threadId, appServerTurnShell(turnId), item);
1605
- if (!message) return;
1606
- if (messagesByThreadId.get(threadId)?.some((candidate) => candidate.id === item.id)) return updateMessage(messagesByThreadId, threadId, item.id, message);
1607
- return appendMessageWithId(messagesByThreadId, threadId, item.id, {
1608
- role: message.role,
1609
- kind: message.kind,
1610
- content: message.content,
1611
- details: message.details,
1612
- state: message.state,
1613
- turnId: message.turnId
1614
- });
1615
- }
1616
- function appendMessageDelta(messagesByThreadId, threadId, messageId, delta) {
1617
- const existing = messagesByThreadId.get(threadId)?.find((message) => message.id === messageId);
1618
- return updateMessage(messagesByThreadId, threadId, messageId, {
1619
- content: `${existing?.content ?? ""}${delta}`,
1620
- state: "streaming"
1621
- });
1622
- }
1623
- function updateMessage(messagesByThreadId, threadId, messageId, update) {
1624
- const messages = messagesByThreadId.get(threadId) ?? [];
1625
- const index = messages.findIndex((message) => message.id === messageId);
1626
- if (index === -1) throw new Error(`Unknown message: ${messageId}`);
1627
- const next = ChatMessageSchema.parse({
1628
- ...messages[index],
1629
- ...update,
1630
- updatedAt: (/* @__PURE__ */ new Date()).toISOString()
1631
- });
1632
- messages[index] = next;
1633
- return next;
1634
- }
1635
- function replaceLocalThreadId(threads, messagesByThreadId, liveThreads, currentThreadId, sdkThreadId) {
1636
- if (!sdkThreadId || sdkThreadId === currentThreadId) return currentThreadId;
1637
- const metadata = threads.get(currentThreadId);
1638
- const thread = liveThreads.get(currentThreadId);
1639
- const messages = messagesByThreadId.get(currentThreadId) ?? [];
1640
- if (!metadata) return sdkThreadId;
1641
- threads.delete(currentThreadId);
1642
- liveThreads.delete(currentThreadId);
1643
- messagesByThreadId.delete(currentThreadId);
1644
- threads.set(sdkThreadId, {
1645
- ...metadata,
1646
- id: sdkThreadId
1647
- });
1648
- messagesByThreadId.set(sdkThreadId, messages.map((message) => ({
1649
- ...message,
1650
- threadId: sdkThreadId
1651
- })));
1652
- if (thread) liveThreads.set(sdkThreadId, thread);
1653
- return sdkThreadId;
1654
- }
1655
- function updateThread(threads, messagesByThreadId, threadId, update) {
1656
- const existing = threads.get(threadId);
1657
- if (!existing) throw new Error(`Unknown thread: ${threadId}`);
1658
- const messages = messagesByThreadId.get(threadId) ?? [];
1659
- const lastMessage = [...messages].reverse().find((message) => message.role !== "status");
1660
- const next = ThreadSummarySchema.parse({
1661
- ...existing,
1662
- ...update,
1663
- messageCount: messages.length,
1664
- lastMessagePreview: lastMessage?.content ? preview(lastMessage.content) : existing.lastMessagePreview,
1665
- lastActivityAt: lastMessage?.updatedAt ?? lastMessage?.createdAt ?? existing.lastActivityAt,
1666
- updatedAt: (/* @__PURE__ */ new Date()).toISOString()
1667
- });
1668
- threads.set(threadId, next);
1669
- return next;
1670
- }
1671
- function sendSse(controller, encoder, secureSession, event) {
1672
- const parsed = StreamThreadRunEventSchema.parse(event);
1673
- const data = secureSession ? EncryptedPayloadSchema.parse(encryptForMobile(secureSession, JSON.stringify(parsed))) : parsed;
1674
- controller.enqueue(encoder.encode(`event: ${parsed.type}\n`));
1675
- controller.enqueue(encoder.encode(`data: ${JSON.stringify(data)}\n\n`));
1676
- }
1677
- function startWebPreviewTargetMonitor({ bridgeUrl, send }) {
1678
- const urls = webPreviewCandidateUrls(bridgeUrl);
1679
- const seenUrls = /* @__PURE__ */ new Set();
1680
- let stopped = false;
1681
- async function scan() {
1682
- if (stopped) return;
1683
- const targets = await detectWebPreviewTargets(urls);
1684
- for (const target of targets) {
1685
- if (stopped || seenUrls.has(target.url)) continue;
1686
- seenUrls.add(target.url);
1687
- try {
1688
- send(target);
1689
- } catch {
1690
- stopped = true;
1691
- return;
1692
- }
1693
- }
1694
- }
1695
- scan();
1696
- const interval = setInterval(() => void scan(), 1500);
1697
- return () => {
1698
- stopped = true;
1699
- clearInterval(interval);
1700
- };
1701
- }
1702
- function webPreviewCandidateUrls(bridgeUrl) {
1703
- const bridge = new URL(bridgeUrl);
1704
- const bridgePort = Number(bridge.port);
1705
- return webPreviewCandidatePorts().filter((port) => port !== bridgePort).map((port) => {
1706
- const url = new URL(bridge.toString());
1707
- url.port = String(port);
1708
- url.pathname = "/";
1709
- url.search = "";
1710
- url.hash = "";
1711
- return url.toString().replace(/\/$/, "");
1712
- });
1713
- }
1714
- function webPreviewCandidatePorts() {
1715
- const configured = process.env.CODEX_RELAY_WEB_PREVIEW_PORTS;
1716
- if (!configured) return defaultWebPreviewPorts;
1717
- const ports = configured.split(",").map((value) => Number(value.trim())).filter((port) => Number.isInteger(port) && port > 0 && port < 65536);
1718
- return ports.length > 0 ? ports : defaultWebPreviewPorts;
1719
- }
1720
- async function detectWebPreviewTargets(urls) {
1721
- return (await Promise.all(urls.map((url) => probeWebPreviewTarget(url)))).filter((target) => Boolean(target));
1722
- }
1723
- async function probeWebPreviewTarget(url) {
1724
- const controller = new AbortController();
1725
- const timeout = setTimeout(() => controller.abort(), 700);
1726
- try {
1727
- const response = await fetch(url, { signal: controller.signal });
1728
- if (!response.ok) return;
1729
- const contentType = response.headers.get("content-type") ?? "";
1730
- const text = await response.text();
1731
- if (!contentType.includes("text/html") && !looksLikeHtml(text)) return;
1732
- return {
1733
- kind: "web",
1734
- url,
1735
- port: Number(new URL(url).port),
1736
- label: webPreviewLabel(text),
1737
- source: "detected-port",
1738
- confidence: "high",
1739
- detectedAt: (/* @__PURE__ */ new Date()).toISOString()
1740
- };
1741
- } catch {
1742
- return;
1743
- } finally {
1744
- clearTimeout(timeout);
1745
- }
1746
- }
1747
- function looksLikeHtml(value) {
1748
- return /^\s*(<!doctype html|<html[\s>])/i.test(value);
1749
- }
1750
- function webPreviewLabel(html) {
1751
- if (html.includes("/@vite/client")) return "Vite";
1752
- if (html.includes("__next")) return "Next.js";
1753
- if (html.includes("expo-router") || html.includes("Expo")) return "Expo";
1754
- return "Web preview";
1755
- }
1756
- function titleFromPrompt(prompt) {
1757
- if (!prompt) return;
1758
- const firstLine = prompt.trim().split(/\r?\n/, 1)[0];
1759
- return firstLine.length > 80 ? `${firstLine.slice(0, 77)}...` : firstLine;
1760
- }
1761
- function maybeReplaceDefaultTitle(currentTitle, prompt) {
1762
- return !currentTitle || currentTitle === "New Codex thread" ? titleFromPrompt(prompt) : currentTitle;
1763
- }
1764
- function promptWithAttachments(prompt, attachments) {
1765
- if (attachments.length === 0) return prompt;
1766
- return `${prompt}\n\n${attachments.map((attachment, index) => {
1767
- const name = attachment.name ? ` (${attachment.name})` : "";
1768
- return `Attached image ${index + 1}${name}:\n${attachment.dataUri}`;
1769
- }).join("\n\n")}`;
1770
- }
1771
- function promptForCollaborationMode(prompt, collaborationMode) {
1772
- if (collaborationMode !== "plan") return prompt;
1773
- return `${planModePromptPrefix}\n\nUser request:\n${prompt}`;
1774
- }
1775
- function appServerCollaborationMode(options) {
1776
- if (options.collaborationMode !== "plan" || !options.model) return null;
1777
- return {
1778
- mode: "plan",
1779
- settings: {
1780
- developer_instructions: null,
1781
- model: options.model,
1782
- reasoning_effort: options.reasoningEffort ?? null
1783
- }
1784
- };
1785
- }
1786
- function attachmentDetails(attachments) {
1787
- if (attachments.length === 0) return;
1788
- return { attachments: attachments.map((attachment) => ({
1789
- mimeType: attachment.mimeType,
1790
- name: attachment.name,
1791
- type: attachment.type
1792
- })) };
1793
- }
1794
- function buildThreadOptions(base, options) {
1795
- const runtime = resolveRuntimeOptions(options.runtimeMode);
1796
- return {
1797
- ...base,
1798
- ...runtime,
1799
- ...options.model ? { model: options.model } : {},
1800
- ...options.reasoningEffort ? { modelReasoningEffort: options.reasoningEffort } : {},
1801
- ...options.approvalPolicy ? { approvalPolicy: options.approvalPolicy } : {},
1802
- ...options.sandboxMode ? { sandboxMode: options.sandboxMode } : {}
1803
- };
1804
- }
1805
- function resolveRuntimeOptions(runtimeMode) {
1806
- switch (runtimeMode) {
1807
- case "auto": return {
1808
- approvalPolicy: "on-failure",
1809
- sandboxMode: "workspace-write"
1810
- };
1811
- case "full-access": return {
1812
- approvalPolicy: "never",
1813
- sandboxMode: "danger-full-access"
1814
- };
1815
- default: return {
1816
- approvalPolicy: "on-request",
1817
- sandboxMode: "workspace-write"
1818
- };
1819
- }
1820
- }
1821
- function resolveAppServerRuntime(runtimeMode, workspacePath) {
1822
- const runtime = resolveRuntimeOptions(runtimeMode) ?? {};
1823
- const sandbox = runtime.sandboxMode ?? "workspace-write";
1824
- return {
1825
- approvalPolicy: runtime.approvalPolicy ?? "on-request",
1826
- sandbox,
1827
- sandboxPolicy: sandboxPolicyForMode(sandbox, workspacePath)
1828
- };
1829
- }
1830
- function sandboxPolicyForMode(sandboxMode, workspacePath) {
1831
- if (sandboxMode === "danger-full-access") return { type: "dangerFullAccess" };
1832
- if (sandboxMode === "read-only") return {
1833
- type: "readOnly",
1834
- access: { type: "fullAccess" },
1835
- networkAccess: false
1836
- };
1837
- return {
1838
- type: "workspaceWrite",
1839
- writableRoots: [workspacePath],
1840
- readOnlyAccess: { type: "fullAccess" },
1841
- networkAccess: false,
1842
- excludeTmpdirEnvVar: false,
1843
- excludeSlashTmp: false
1844
- };
1845
- }
1846
- function hasExplicitRunOptions(options) {
1847
- return Boolean(options.model || options.reasoningEffort || options.approvalPolicy || options.sandboxMode || options.collaborationMode === "plan" || options.runtimeMode);
1848
- }
1849
- function mapAppServerThread(thread) {
1850
- const createdAt = fromUnixSeconds(thread.createdAt);
1851
- const updatedAt = fromUnixSeconds(thread.updatedAt);
1852
- return ThreadSummarySchema.parse({
1853
- id: thread.id,
1854
- title: thread.name ?? preview(thread.preview || "Untitled thread"),
1855
- createdAt,
1856
- updatedAt,
1857
- state: mapAppServerThreadState(thread.status),
1858
- model: thread.modelProvider,
1859
- cwd: String(thread.cwd),
1860
- source: thread.source,
1861
- messageCount: countThreadMessages(thread),
1862
- lastMessagePreview: thread.preview ? preview(thread.preview) : void 0,
1863
- lastActivityAt: updatedAt
1864
- });
1865
- }
1866
- function mapAppServerMessages(thread) {
1867
- const messages = [];
1868
- for (const turn of thread.turns ?? []) for (const item of turn.items ?? []) {
1869
- const message = mapAppServerItem(thread.id, turn, item);
1870
- if (message) messages.push(message);
1871
- }
1872
- return messages;
1873
- }
1874
- function mapAppServerItem(threadId, turn, item) {
1875
- const timestamp = fromUnixSeconds(turn.startedAt ?? turn.completedAt ?? Date.now() / 1e3);
1876
- const base = {
1877
- id: item.id,
1878
- threadId,
1879
- createdAt: timestamp,
1880
- updatedAt: turn.completedAt ? fromUnixSeconds(turn.completedAt) : timestamp,
1881
- turnId: turn.id,
1882
- state: "completed"
1883
- };
1884
- switch (item.type) {
1885
- case "userMessage": {
1886
- const userItem = item;
1887
- return ChatMessageSchema.parse({
1888
- ...base,
1889
- role: "user",
1890
- content: userItem.content.map((content) => content.text ?? content.path ?? content.url ?? "").filter(Boolean).join("\n\n")
1891
- });
1892
- }
1893
- case "agentMessage": {
1894
- const agentItem = item;
1895
- return ChatMessageSchema.parse({
1896
- ...base,
1897
- role: "assistant",
1898
- content: agentItem.text
1899
- });
1900
- }
1901
- case "reasoning": {
1902
- const reasoningItem = item;
1903
- const summary = compactStringList(reasoningItem.summary);
1904
- const content = compactStringList(reasoningItem.content);
1905
- const text = [...summary, ...content].join("\n\n") || "Reasoning";
1906
- return ChatMessageSchema.parse({
1907
- ...base,
1908
- role: "reasoning",
1909
- kind: "thinking",
1910
- content: text,
1911
- details: {
1912
- summary,
1913
- content
1914
- }
1915
- });
1916
- }
1917
- case "commandExecution": {
1918
- const commandItem = item;
1919
- return ChatMessageSchema.parse({
1920
- ...base,
1921
- role: "tool",
1922
- kind: "commandExecution",
1923
- content: commandItem.command,
1924
- details: {
1925
- command: commandItem.command,
1926
- cwd: commandItem.cwd ?? void 0,
1927
- exitCode: commandItem.exitCode ?? void 0,
1928
- output: commandItem.aggregatedOutput ?? void 0,
1929
- status: commandItem.status ?? void 0
1930
- }
1931
- });
1932
- }
1933
- case "fileChange": {
1934
- const fileItem = item;
1935
- const changes = fileItem.changes ?? [];
1936
- return ChatMessageSchema.parse({
1937
- ...base,
1938
- role: "tool",
1939
- kind: "fileChange",
1940
- content: summarizeFileChanges(changes),
1941
- details: {
1942
- changes,
1943
- patch: fileItem.patch ?? void 0
1944
- }
1945
- });
1946
- }
1947
- case "mcpToolCall": {
1948
- const toolItem = item;
1949
- return ChatMessageSchema.parse({
1950
- ...base,
1951
- role: "tool",
1952
- kind: "toolActivity",
1953
- content: `${toolItem.server}.${toolItem.tool}`,
1954
- details: {
1955
- server: toolItem.server,
1956
- status: toolItem.status ?? void 0,
1957
- tool: toolItem.tool
1958
- }
1959
- });
1960
- }
1961
- case "webSearch": {
1962
- const searchItem = item;
1963
- return ChatMessageSchema.parse({
1964
- ...base,
1965
- role: "tool",
1966
- kind: "webSearch",
1967
- content: searchItem.query,
1968
- details: {
1969
- query: searchItem.query,
1970
- status: searchItem.status ?? void 0
1971
- }
1972
- });
1973
- }
1974
- default: return mapUnknownAppServerItem(threadId, turn, item);
1975
- }
1976
- }
1977
- function mapUnknownAppServerItem(threadId, turn, item) {
1978
- const timestamp = fromUnixSeconds(turn.startedAt ?? turn.completedAt ?? Date.now() / 1e3);
1979
- const type = "type" in item ? String(item.type) : "unknown";
1980
- const kind = kindFromProtocolType(type) ?? "unknown";
1981
- return ChatMessageSchema.parse({
1982
- id: item.id,
1983
- threadId,
1984
- role: "status",
1985
- kind,
1986
- content: type,
1987
- createdAt: timestamp,
1988
- updatedAt: turn.completedAt ? fromUnixSeconds(turn.completedAt) : timestamp,
1989
- turnId: turn.id,
1990
- state: "completed",
1991
- details: { type }
1992
- });
1993
- }
1994
- function appServerTurnShell(turnId) {
1995
- return {
1996
- id: turnId ?? "turn-live",
1997
- items: [],
1998
- status: "running",
1999
- startedAt: Date.now() / 1e3,
2000
- completedAt: null
2001
- };
2002
- }
2003
- function mapAppServerModel(model) {
2004
- return {
2005
- id: model.id,
2006
- model: model.model,
2007
- displayName: model.displayName,
2008
- description: model.description,
2009
- isDefault: Boolean(model.isDefault),
2010
- defaultReasoningEffort: model.defaultReasoningEffort,
2011
- supportedReasoningEfforts: model.supportedReasoningEfforts?.map((effort) => effort.reasoningEffort) ?? []
2012
- };
2013
- }
2014
- function normalizeRateLimitBuckets(rateLimits) {
2015
- const keyed = objectRecord(rateLimits.rateLimitsByLimitId);
2016
- if (keyed) return Object.values(keyed).flatMap((value) => {
2017
- const bucket = normalizeRateLimitBucket(value);
2018
- return bucket ? [bucket] : [];
2019
- });
2020
- const bucket = normalizeRateLimitBucket(rateLimits.rateLimits);
2021
- return bucket ? [bucket] : [];
2022
- }
2023
- function normalizeRateLimitBucket(value) {
2024
- const record = objectRecord(value);
2025
- if (!record) return;
2026
- const limitId = firstString(record, [
2027
- "limitId",
2028
- "limit_id",
2029
- "id"
2030
- ]);
2031
- if (!limitId) return;
2032
- return {
2033
- limitId,
2034
- limitName: firstString(record, [
2035
- "limitName",
2036
- "limit_name",
2037
- "name"
2038
- ]) ?? null,
2039
- planType: firstString(record, ["planType", "plan_type"]) ?? null,
2040
- primary: normalizeRateLimitWindow(record.primary),
2041
- secondary: normalizeRateLimitWindow(record.secondary),
2042
- rateLimitReachedType: firstString(record, ["rateLimitReachedType", "rate_limit_reached_type"]) ?? null
2043
- };
2044
- }
2045
- function normalizeRateLimitWindow(value) {
2046
- const record = objectRecord(value);
2047
- if (!record) return null;
2048
- const usedPercent = firstNumber(record, ["usedPercent", "used_percent"]);
2049
- if (usedPercent === void 0) return null;
2050
- return {
2051
- usedPercent: Math.max(0, Math.min(100, Math.round(usedPercent))),
2052
- windowDurationMins: firstNumber(record, ["windowDurationMins", "window_duration_mins"]) ?? null,
2053
- resetsAt: firstNumber(record, ["resetsAt", "resets_at"]) ?? null
2054
- };
2055
- }
2056
- function objectRecord(value) {
2057
- return value && typeof value === "object" ? value : void 0;
2058
- }
2059
- function fallbackModels() {
2060
- return [{
2061
- id: "gpt-5.5",
2062
- model: "gpt-5.5",
2063
- displayName: "GPT-5.5",
2064
- description: "Default Codex model",
2065
- isDefault: true,
2066
- defaultReasoningEffort: "medium",
2067
- supportedReasoningEfforts: [
2068
- { reasoningEffort: "low" },
2069
- { reasoningEffort: "medium" },
2070
- { reasoningEffort: "high" },
2071
- { reasoningEffort: "xhigh" }
2072
- ]
2073
- }];
2074
- }
2075
- function mapAppServerThreadState(status) {
2076
- if (typeof status === "string") {
2077
- if (status === "running" || status === "active") return "running";
2078
- if (status === "failed" || status === "systemError") return "failed";
2079
- if (status === "completed") return "completed";
2080
- }
2081
- if (status && typeof status === "object" && "type" in status) {
2082
- const type = String(status.type);
2083
- if (type === "active") return "running";
2084
- if (type === "systemError") return "failed";
2085
- }
2086
- return "idle";
2087
- }
2088
- function countThreadMessages(thread) {
2089
- return thread.turns?.reduce((count, turn) => count + turn.items.filter((item) => item.type === "userMessage" || item.type === "agentMessage").length, 0) ?? 0;
2090
- }
2091
- function fromUnixSeconds(value) {
2092
- return (/* @__PURE__ */ new Date(value * 1e3)).toISOString();
2093
- }
2094
- function preview(content) {
2095
- const normalized = content.replace(/\s+/g, " ").trim();
2096
- return normalized.length > 140 ? `${normalized.slice(0, 137)}...` : normalized;
2097
- }
2098
- function compactStringList(value) {
2099
- return value?.map((item) => item.trim()).filter(Boolean) ?? [];
2100
- }
2101
- function summarizeFileChanges(changes) {
2102
- if (changes.length === 0) return "Files changed";
2103
- const paths = changes.map((change) => change.path).filter(Boolean);
2104
- const shown = paths.slice(0, 3).join(", ");
2105
- const suffix = paths.length > 3 ? ` and ${paths.length - 3} more` : "";
2106
- return `${changes.length} file${changes.length === 1 ? "" : "s"} changed: ${shown}${suffix}`;
2107
- }
2108
- function structuredStreamMessage(role, event, fallbackContent) {
2109
- const item = eventItem(event);
2110
- const type = item?.type ? String(item.type) : void 0;
2111
- if (role === "reasoning") {
2112
- const summary = stringArray(item?.summary);
2113
- const content = stringArray(item?.content);
2114
- return {
2115
- kind: "thinking",
2116
- content: [...summary, ...content].join("\n\n") || fallbackContent,
2117
- details: {
2118
- content,
2119
- summary,
2120
- type
2121
- }
2122
- };
2123
- }
2124
- switch (type) {
2125
- case "command_execution": {
2126
- const command = firstString(item, ["command", "cmd"]) ?? fallbackContent;
2127
- return {
2128
- kind: "commandExecution",
2129
- content: command,
2130
- details: {
2131
- command,
2132
- cwd: firstString(item, ["cwd", "working_directory"]),
2133
- exitCode: firstNumber(item, ["exit_code", "exitCode"]),
2134
- output: firstString(item, [
2135
- "aggregated_output",
2136
- "aggregatedOutput",
2137
- "output"
2138
- ]),
2139
- status: firstString(item, ["status"]),
2140
- type
2141
- }
2142
- };
2143
- }
2144
- case "file_change": {
2145
- const changes = Array.isArray(item?.changes) ? item.changes : [];
2146
- return {
2147
- kind: "fileChange",
2148
- content: summarizeFileChanges(normalizeFileChanges(changes)),
2149
- details: {
2150
- changes,
2151
- patch: firstString(item, ["patch"]),
2152
- type
2153
- }
2154
- };
2155
- }
2156
- case "mcp_tool_call": return {
2157
- kind: "toolActivity",
2158
- content: [firstString(item, ["server"]), firstString(item, ["tool", "name"])].filter(Boolean).join(".") || fallbackContent,
2159
- details: {
2160
- server: firstString(item, ["server"]),
2161
- status: firstString(item, ["status"]),
2162
- tool: firstString(item, ["tool", "name"]),
2163
- type
2164
- }
2165
- };
2166
- case "web_search": return {
2167
- kind: "webSearch",
2168
- content: firstString(item, ["query"]) ?? fallbackContent,
2169
- details: {
2170
- query: firstString(item, ["query"]),
2171
- status: firstString(item, ["status"]),
2172
- type
2173
- }
2174
- };
2175
- default: return {
2176
- kind: kindFromProtocolType(type) ?? (role === "tool" ? "toolActivity" : "unknown"),
2177
- content: fallbackContent,
2178
- details: { type }
2179
- };
2180
- }
2181
- }
2182
- function isApprovalServerRequest(method) {
2183
- return method === "item/commandExecution/requestApproval" || method === "item/fileChange/requestApproval" || method === "item/permissions/requestApproval" || method === "item/tool/requestUserInput" || method === "mcpServer/elicitation/request" || method === "execCommandApproval" || method === "applyPatchApproval";
2184
- }
2185
- function approvalMessageFromRequest(request) {
2186
- const params = recordParams(request);
2187
- const threadId = firstString(params, ["threadId", "conversationId"]);
2188
- const turnId = firstString(params, ["turnId"]) ?? void 0;
2189
- if (!threadId) return;
2190
- const approvalId = `approval-${request.id}`;
2191
- switch (request.method) {
2192
- case "item/commandExecution/requestApproval": {
2193
- const command = firstString(params, ["command"]) ?? "Command execution";
2194
- return {
2195
- approvalId,
2196
- content: command,
2197
- details: {
2198
- approvalId,
2199
- approvalKind: "commandExecution",
2200
- command,
2201
- cwd: firstString(params, ["cwd"]),
2202
- reason: firstString(params, ["reason"]),
2203
- availableDecisions: Array.isArray(params?.availableDecisions) ? params.availableDecisions : void 0
2204
- },
2205
- kind: "commandExecution",
2206
- threadId,
2207
- turnId
2208
- };
2209
- }
2210
- case "execCommandApproval": {
2211
- const command = stringArray(params?.command).join(" ") || "Command execution";
2212
- return {
2213
- approvalId,
2214
- content: command,
2215
- details: {
2216
- approvalId,
2217
- approvalKind: "commandExecution",
2218
- command,
2219
- cwd: firstString(params, ["cwd"]),
2220
- reason: firstString(params, ["reason"])
2221
- },
2222
- kind: "commandExecution",
2223
- threadId,
2224
- turnId
2225
- };
2226
- }
2227
- case "item/fileChange/requestApproval": return {
2228
- approvalId,
2229
- content: firstString(params, ["reason"]) ?? "Approve file changes",
2230
- details: {
2231
- approvalId,
2232
- approvalKind: "fileChange",
2233
- grantRoot: firstString(params, ["grantRoot"]),
2234
- reason: firstString(params, ["reason"])
2235
- },
2236
- kind: "fileChange",
2237
- threadId,
2238
- turnId
2239
- };
2240
- case "applyPatchApproval": return {
2241
- approvalId,
2242
- content: firstString(params, ["reason"]) ?? "Approve file changes",
2243
- details: {
2244
- approvalId,
2245
- approvalKind: "fileChange",
2246
- changes: params?.fileChanges,
2247
- reason: firstString(params, ["reason"])
2248
- },
2249
- kind: "fileChange",
2250
- threadId,
2251
- turnId
2252
- };
2253
- case "item/permissions/requestApproval": return {
2254
- approvalId,
2255
- content: firstString(params, ["reason"]) ?? "Approve additional permissions",
2256
- details: {
2257
- approvalId,
2258
- approvalKind: "permissions",
2259
- cwd: firstString(params, ["cwd"]),
2260
- permissions: params?.permissions,
2261
- reason: firstString(params, ["reason"])
2262
- },
2263
- kind: "permissions",
2264
- threadId,
2265
- turnId
2266
- };
2267
- case "item/tool/requestUserInput": return {
2268
- approvalId,
2269
- content: "Input requested",
2270
- details: {
2271
- approvalId,
2272
- approvalKind: "structuredUserInput",
2273
- questions: Array.isArray(params?.questions) ? params.questions : []
2274
- },
2275
- kind: "structuredUserInput",
2276
- threadId,
2277
- turnId
2278
- };
2279
- case "mcpServer/elicitation/request": return {
2280
- approvalId,
2281
- content: firstString(params, ["message"]) ?? "MCP input requested",
2282
- details: {
2283
- approvalId,
2284
- approvalKind: "mcpElicitation",
2285
- message: firstString(params, ["message"]),
2286
- mode: firstString(params, ["mode"]),
2287
- serverName: firstString(params, ["serverName"]),
2288
- url: firstString(params, ["url"])
2289
- },
2290
- kind: "mcpElicitation",
2291
- threadId,
2292
- turnId
2293
- };
2294
- default: return;
2295
- }
2296
- }
2297
- async function resolveAppServerRequest(pending, decision, answers) {
2298
- switch (pending.kind) {
2299
- case "commandExecution":
2300
- await pending.appServer.respondToRequest(pending.requestId, { decision: pending.method === "execCommandApproval" ? legacyApprovalDecision(decision) : commandApprovalDecision(decision) });
2301
- return;
2302
- case "fileChange":
2303
- await pending.appServer.respondToRequest(pending.requestId, { decision: pending.method === "applyPatchApproval" ? legacyApprovalDecision(decision) : fileChangeApprovalDecision(decision) });
2304
- return;
2305
- case "permissions":
2306
- await pending.appServer.respondToRequest(pending.requestId, {
2307
- permissions: decision === "approve" || decision === "approve-for-session" ? {} : {},
2308
- scope: decision === "approve-for-session" ? "session" : "turn",
2309
- strictAutoReview: decision === "deny" || decision === "cancel"
2310
- });
2311
- return;
2312
- case "structuredUserInput":
2313
- await pending.appServer.respondToRequest(pending.requestId, { answers });
2314
- return;
2315
- case "mcpElicitation":
2316
- await pending.appServer.respondToRequest(pending.requestId, {
2317
- action: decision === "approve" || decision === "approve-for-session" ? "accept" : "decline",
2318
- content: answers.length > 0 ? { answers } : null,
2319
- _meta: null
2320
- });
2321
- return;
2322
- }
2323
- }
2324
- function legacyApprovalDecision(decision) {
2325
- switch (decision) {
2326
- case "approve": return "approved";
2327
- case "approve-for-session": return "approved_for_session";
2328
- case "cancel": return "abort";
2329
- default: return "denied";
2330
- }
2331
- }
2332
- function commandApprovalDecision(decision) {
2333
- switch (decision) {
2334
- case "approve": return "accept";
2335
- case "approve-for-session": return "acceptForSession";
2336
- case "cancel": return "cancel";
2337
- default: return "decline";
2338
- }
2339
- }
2340
- function fileChangeApprovalDecision(decision) {
2341
- switch (decision) {
2342
- case "approve": return "accept";
2343
- case "approve-for-session": return "acceptForSession";
2344
- case "cancel": return "cancel";
2345
- default: return "decline";
2346
- }
2347
- }
2348
- function kindFromProtocolType(type) {
2349
- switch (type) {
2350
- case "plan":
2351
- case "turn_plan_updated":
2352
- case "turn/plan/updated": return "plan";
2353
- case "request_user_input":
2354
- case "structured_user_input":
2355
- case "structuredUserInput": return "structuredUserInput";
2356
- case "approval_request":
2357
- case "approvalRequest": return "approvalRequest";
2358
- case "subagent_action":
2359
- case "subagentAction": return "subagentAction";
2360
- default: return;
2361
- }
2362
- }
2363
- function eventItem(event) {
2364
- if (!event || typeof event !== "object") return;
2365
- const record = event;
2366
- return record.item && typeof record.item === "object" ? record.item : record;
2367
- }
2368
- function recordParams(message) {
2369
- return message.params && typeof message.params === "object" ? message.params : void 0;
2370
- }
2371
- function turnIdFromParams(params) {
2372
- const turn = params?.turn;
2373
- return turn && typeof turn === "object" ? firstString(turn, ["id"]) : void 0;
2374
- }
2375
- function turnStatus(params) {
2376
- const turn = params?.turn;
2377
- if (turn && typeof turn === "object") {
2378
- const status = turn.status;
2379
- return typeof status === "string" ? status : void 0;
2380
- }
2381
- }
2382
- function turnErrorMessage(params) {
2383
- const turn = params?.turn;
2384
- if (!turn || typeof turn !== "object") return;
2385
- const error = turn.error;
2386
- return error && typeof error === "object" ? firstString(error, ["message"]) : void 0;
2387
- }
2388
- function planStepText(step) {
2389
- if (!step || typeof step !== "object") return;
2390
- const record = step;
2391
- const text = firstString(record, ["text", "step"]);
2392
- const status = firstString(record, ["status"]);
2393
- return text ? `${status ? `${status}: ` : ""}${text}` : void 0;
2394
- }
2395
- function firstString(record, keys) {
2396
- for (const key of keys) {
2397
- const value = record?.[key];
2398
- if (typeof value === "string" && value.trim()) return value;
2399
- }
2400
- }
2401
- function firstNumber(record, keys) {
2402
- for (const key of keys) {
2403
- const value = record?.[key];
2404
- if (typeof value === "number") return value;
2405
- }
2406
- }
2407
- function stringArray(value) {
2408
- if (!Array.isArray(value)) return [];
2409
- return value.filter((item) => typeof item === "string" && item.trim().length > 0);
2410
- }
2411
- function normalizeFileChanges(value) {
2412
- return value.flatMap((item) => {
2413
- if (!item || typeof item !== "object") return [];
2414
- const record = item;
2415
- const path = firstString(record, ["path"]);
2416
- const kind = firstString(record, ["kind", "type"]) ?? "modified";
2417
- return path ? [{
2418
- path,
2419
- kind
2420
- }] : [];
2421
- });
2422
- }
2423
- async function readWorkspaceChanges(workspacePath) {
2424
- const repo = await openRepository(workspacePath);
2425
- const [currentBranch, branches] = await Promise.all([currentGitBranch(workspacePath), listGitBranches(workspacePath)]);
2426
- const statusEntries = collectIterator(repo.statuses().iter());
2427
- const statusByPath = new Map(statusEntries.map((entry) => [entry.path(), entry]));
2428
- const status = statusEntries.map((entry) => formatStatusLine(entry.path(), entry.status())).join("\n");
2429
- const structuredDiff = createWorkspaceDiff(repo);
2430
- const diff = await git(workspacePath, [
2431
- "diff",
2432
- "--no-ext-diff",
2433
- "--no-color",
2434
- "HEAD",
2435
- "--"
2436
- ]).catch(() => structuredDiff.print());
2437
- const patchesByPath = splitDiffByPath(diff);
2438
- structuredDiff.findSimilar({ renames: true });
2439
- const stats = structuredDiff.stats();
2440
- const filesByPath = /* @__PURE__ */ new Map();
2441
- for (const delta of collectIterator(structuredDiff.deltas())) {
2442
- const path = delta.newFile().path() ?? delta.oldFile().path();
2443
- if (!path) continue;
2444
- const patch = patchesByPath.get(path) ?? patchesByPath.get(delta.oldFile().path() ?? "") ?? "";
2445
- const lineStats = countPatchLines(patch);
2446
- const statusEntry = statusByPath.get(path) ?? statusByPath.get(delta.oldFile().path() ?? "");
2447
- filesByPath.set(path, {
2448
- additions: lineStats.additions,
2449
- deletions: lineStats.deletions,
2450
- isBinary: delta.newFile().isBinary() || delta.oldFile().isBinary(),
2451
- oldPath: delta.oldFile().path(),
2452
- path,
2453
- patch,
2454
- stagedStatus: statusEntry?.headToIndex()?.status() ?? null,
2455
- status: delta.status(),
2456
- worktreeStatus: statusEntry?.indexToWorkdir()?.status() ?? null
2457
- });
2458
- }
2459
- for (const entry of statusEntries) {
2460
- if (filesByPath.has(entry.path())) continue;
2461
- filesByPath.set(entry.path(), {
2462
- additions: 0,
2463
- deletions: 0,
2464
- isBinary: false,
2465
- oldPath: null,
2466
- path: entry.path(),
2467
- patch: "",
2468
- stagedStatus: entry.headToIndex()?.status() ?? null,
2469
- status: statusNameFromStatus(entry.status()),
2470
- worktreeStatus: entry.indexToWorkdir()?.status() ?? null
2471
- });
2472
- }
2473
- const files = [...filesByPath.values()].sort((left, right) => left.path.localeCompare(right.path));
2474
- return {
2475
- branches,
2476
- currentBranch,
2477
- diff,
2478
- files,
2479
- hasChanges: files.length > 0 || Boolean(status.trim() || diff.trim()),
2480
- status,
2481
- stats: {
2482
- additions: Number(stats.insertions),
2483
- deletions: Number(stats.deletions),
2484
- filesChanged: files.length || Number(stats.filesChanged)
2485
- }
2486
- };
2487
- }
2488
- async function currentGitBranch(workspacePath) {
2489
- return (await git(workspacePath, ["branch", "--show-current"]).catch(() => "")).trim() || null;
2490
- }
2491
- async function listGitBranches(workspacePath) {
2492
- return (await git(workspacePath, [
2493
- "branch",
2494
- "--format=%(HEAD)%09%(refname:short)",
2495
- "--sort=refname"
2496
- ]).catch(() => "")).split("\n").map((line) => {
2497
- const [headMarker, name] = line.split(" ");
2498
- return {
2499
- current: headMarker === "*",
2500
- name: name?.trim() ?? ""
2501
- };
2502
- }).filter((branch) => branch.name.length > 0);
2503
- }
2504
- function createWorkspaceDiff(repo) {
2505
- const options = {
2506
- includeUntracked: true,
2507
- recurseUntrackedDirs: true,
2508
- showUntrackedContent: true
2509
- };
2510
- try {
2511
- return repo.diffTreeToWorkdirWithIndex(repo.head().peelToTree(), options);
2512
- } catch {
2513
- return repo.diffIndexToWorkdir(void 0, options);
2514
- }
2515
- }
2516
- function collectIterator(iterator) {
2517
- const items = [];
2518
- for (let result = iterator.next(); !result.done; result = iterator.next()) items.push(result.value);
2519
- return items;
2520
- }
2521
- function splitDiffByPath(diff) {
2522
- const patches = /* @__PURE__ */ new Map();
2523
- const sections = diff.split(/(?=^diff --git )/m).filter(Boolean);
2524
- for (const section of sections) {
2525
- const header = section.match(/^diff --git a\/(.+) b\/(.+)$/m);
2526
- if (!header) continue;
2527
- const oldPath = header[1];
2528
- const newPath = header[2];
2529
- patches.set(newPath, section.trimEnd());
2530
- patches.set(oldPath, section.trimEnd());
2531
- }
2532
- return patches;
2533
- }
2534
- function countPatchLines(patch) {
2535
- let additions = 0;
2536
- let deletions = 0;
2537
- for (const line of patch.split("\n")) {
2538
- if (line.startsWith("+++") || line.startsWith("---")) continue;
2539
- if (line.startsWith("+")) additions += 1;
2540
- else if (line.startsWith("-")) deletions += 1;
2541
- }
2542
- return {
2543
- additions,
2544
- deletions
2545
- };
2546
- }
2547
- function formatStatusLine(path, status) {
2548
- if (status.ignored) return `!! ${path}`;
2549
- if (status.wtNew && !status.indexNew) return `?? ${path}`;
2550
- return `${statusIndexCode(status)}${statusWorktreeCode(status)} ${path}`;
2551
- }
2552
- function statusIndexCode(status) {
2553
- if (status.conflicted) return "U";
2554
- if (status.indexRenamed) return "R";
2555
- if (status.indexNew) return "A";
2556
- if (status.indexDeleted) return "D";
2557
- if (status.indexTypechange) return "T";
2558
- return status.indexModified ? "M" : " ";
2559
- }
2560
- function statusWorktreeCode(status) {
2561
- if (status.conflicted) return "U";
2562
- if (status.wtRenamed) return "R";
2563
- if (status.wtDeleted) return "D";
2564
- if (status.wtTypechange) return "T";
2565
- if (status.wtUnreadable) return "?";
2566
- return status.wtModified ? "M" : " ";
2567
- }
2568
- function statusNameFromStatus(status) {
2569
- if (status.conflicted) return "Conflicted";
2570
- if (status.indexNew || status.wtNew) return "Added";
2571
- if (status.indexDeleted || status.wtDeleted) return "Deleted";
2572
- if (status.indexRenamed || status.wtRenamed) return "Renamed";
2573
- if (status.indexTypechange || status.wtTypechange) return "Typechange";
2574
- if (status.ignored) return "Ignored";
2575
- return status.current ? "Unmodified" : "Modified";
2576
- }
2577
- async function git(cwd, args) {
2578
- const { stdout } = await execFileAsync("git", args, {
2579
- cwd,
2580
- maxBuffer: 16 * 1024 * 1024
2581
- });
2582
- return stdout.trimEnd();
2583
- }
2584
- function validationError(error) {
2585
- return apiError("invalid_request", "Request body did not match the Codex Relay API schema.", error.issues.map((issue) => `${issue.path.join(".") || "body"}: ${issue.message}`));
2586
- }
2587
- function apiError(code, message, issues) {
2588
- return { error: {
2589
- code,
2590
- message,
2591
- issues
2592
- } };
2593
- }
2594
- function errorMessage(error) {
2595
- return error instanceof Error ? error.message : "Codex run failed.";
2596
- }
2597
- //#endregion
2598
- //#region src/pairing-store.ts
2599
- async function createTursoPairingSessionStore(path) {
2600
- if (path !== ":memory:") await mkdir(dirname(path), { recursive: true });
2601
- const db = await connect(path);
2602
- await db.exec(`
2603
- CREATE TABLE IF NOT EXISTS pairing_sessions (
2604
- token_hash TEXT PRIMARY KEY,
2605
- client_name TEXT,
2606
- expires_at INTEGER NOT NULL,
2607
- created_at INTEGER NOT NULL,
2608
- updated_at INTEGER NOT NULL
2609
- );
2610
-
2611
- CREATE TABLE IF NOT EXISTS pending_pairings (
2612
- approval_code TEXT PRIMARY KEY,
2613
- client_name TEXT,
2614
- client_ephemeral_public_key TEXT NOT NULL,
2615
- client_nonce TEXT NOT NULL,
2616
- server_url TEXT NOT NULL,
2617
- approved INTEGER NOT NULL DEFAULT 0,
2618
- expires_at INTEGER NOT NULL,
2619
- created_at INTEGER NOT NULL,
2620
- updated_at INTEGER NOT NULL
2621
- );
2622
- `);
2623
- async function countActive(now) {
2624
- const row = await db.prepare("SELECT COUNT(*) AS count FROM pairing_sessions WHERE expires_at > ?").get(now);
2625
- return Number(row?.count ?? 0);
2626
- }
2627
- async function deleteSession(tokenHash) {
2628
- await db.prepare("DELETE FROM pairing_sessions WHERE token_hash = ?").run(tokenHash);
2629
- }
2630
- async function deletePendingPairing(approvalCode) {
2631
- await db.prepare("DELETE FROM pending_pairings WHERE approval_code = ?").run(approvalCode);
2632
- }
2633
- async function getPendingPairing(approvalCode, now) {
2634
- const row = await db.prepare(`SELECT approval_code AS approvalCode,
2635
- client_name AS clientName,
2636
- client_ephemeral_public_key AS clientEphemeralPublicKey,
2637
- client_nonce AS clientNonce,
2638
- server_url AS serverUrl,
2639
- approved,
2640
- expires_at AS expiresAt
2641
- FROM pending_pairings
2642
- WHERE approval_code = ?`).get(approvalCode);
2643
- if (!row) return;
2644
- const expiresAt = Number(row.expiresAt);
2645
- if (now > expiresAt) {
2646
- await deletePendingPairing(approvalCode);
2647
- return;
2648
- }
2649
- return {
2650
- approvalCode: String(row.approvalCode),
2651
- approved: Number(row.approved) === 1,
2652
- clientEphemeralPublicKey: String(row.clientEphemeralPublicKey),
2653
- clientName: typeof row.clientName === "string" ? row.clientName : void 0,
2654
- clientNonce: String(row.clientNonce),
2655
- expiresAt,
2656
- serverUrl: String(row.serverUrl)
2657
- };
2658
- }
2659
- return {
2660
- async approvePendingPairing(approvalCode, now) {
2661
- const pending = await getPendingPairing(approvalCode, now);
2662
- if (!pending) return;
2663
- await db.prepare("UPDATE pending_pairings SET approved = 1, updated_at = ? WHERE approval_code = ?").run(now, approvalCode);
2664
- return {
2665
- ...pending,
2666
- approved: true
2667
- };
2668
- },
2669
- countActive,
2670
- async createPendingPairing(pairing) {
2671
- const now = Date.now();
2672
- await db.prepare(`INSERT INTO pending_pairings (
2673
- approval_code,
2674
- client_name,
2675
- client_ephemeral_public_key,
2676
- client_nonce,
2677
- server_url,
2678
- approved,
2679
- expires_at,
2680
- created_at,
2681
- updated_at
2682
- )
2683
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`).run(pairing.approvalCode, pairing.clientName ?? null, pairing.clientEphemeralPublicKey, pairing.clientNonce, pairing.serverUrl, pairing.approved ? 1 : 0, pairing.expiresAt, now, now);
2684
- },
2685
- async createSession(tokenHash, session) {
2686
- const now = Date.now();
2687
- await db.prepare(`INSERT INTO pairing_sessions (token_hash, client_name, expires_at, created_at, updated_at)
2688
- VALUES (?, ?, ?, ?, ?)`).run(tokenHash, session.clientName ?? null, session.expiresAt, now, now);
2689
- return countActive(now);
2690
- },
2691
- deleteSession,
2692
- deletePendingPairing,
2693
- getPendingPairing,
2694
- async getValidSession(tokenHash, now) {
2695
- const row = await db.prepare("SELECT client_name AS clientName, expires_at AS expiresAt FROM pairing_sessions WHERE token_hash = ?").get(tokenHash);
2696
- if (!row) return;
2697
- const expiresAt = Number(row.expiresAt);
2698
- if (now > expiresAt) {
2699
- await deleteSession(tokenHash);
2700
- return;
2701
- }
2702
- return {
2703
- clientName: typeof row.clientName === "string" ? row.clientName : void 0,
2704
- expiresAt
2705
- };
2706
- },
2707
- async pruneExpired(now) {
2708
- await db.prepare("DELETE FROM pairing_sessions WHERE expires_at <= ?").run(now);
2709
- await db.prepare("DELETE FROM pending_pairings WHERE expires_at <= ?").run(now);
2710
- },
2711
- async rotateSession(oldTokenHash, newTokenHash, session) {
2712
- const now = Date.now();
2713
- await db.transaction(async () => {
2714
- await db.prepare("DELETE FROM pairing_sessions WHERE token_hash = ?").run(oldTokenHash);
2715
- await db.prepare(`INSERT INTO pairing_sessions (token_hash, client_name, expires_at, created_at, updated_at)
2716
- VALUES (?, ?, ?, ?, ?)`).run(newTokenHash, session.clientName ?? null, session.expiresAt, now, now);
2717
- })();
2718
- return countActive(now);
2719
- }
2720
- };
2721
- }
2722
- //#endregion
2723
- //#region src/index.ts
2724
- const port = Number(process.env.PORT ?? 8787);
2725
- const hostname$1 = process.env.HOST ?? "0.0.0.0";
2726
- const clientTokenTtlMs = 10080 * 60 * 1e3;
2727
- const serverIdentity = createServerIdentity();
2728
- const approvalSecret = await getApprovalSecret();
2729
- const colors = pc.createColors(!process.env.NO_COLOR && process.env.TERM !== "dumb");
2730
- const color = {
2731
- brand: colors.cyan,
2732
- code: colors.yellow,
2733
- command: colors.green,
2734
- event: colors.magenta,
2735
- muted: colors.gray,
2736
- prompt: colors.cyan,
2737
- url: colors.blue
2738
- };
2739
- serve({
2740
- fetch: createApp({ pairing: {
2741
- approvalSecret,
2742
- serverIdentity,
2743
- createClientToken: () => randomBytes(32).toString("base64url"),
2744
- hashClientToken,
2745
- sessions: await createTursoPairingSessionStore(process.env.CODEX_RELAY_AUTH_DB_PATH ?? resolve(process.cwd(), ".codex-relay/auth.db")),
2746
- tokenTtlMs: clientTokenTtlMs,
2747
- onPaired: ({ clientName, tokenCount }) => {
2748
- logRuntimeEvent("Paired", `Mobile client connected${clientName ? ` from ${clientName}` : ""}; ${formatClientCount(tokenCount)} active.`);
2749
- },
2750
- onPairAttempt: ({ remoteAddress }) => {
2751
- logRuntimeEvent("Pairing", `Handshake received${remoteAddress ? ` from ${remoteAddress}` : ""}.`);
2752
- },
2753
- onPairApprovalRequested: ({ clientName }) => {
2754
- logRuntimeEvent("Approval", `Pairing approval requested${clientName ? ` from ${clientName}` : ""}. Use the code shown in the mobile app to approve locally.`);
2755
- },
2756
- onPairApproved: ({ clientName }) => {
2757
- logRuntimeEvent("Approved", `Pairing request approved${clientName ? ` for ${clientName}` : ""}. Waiting for secure session pickup.`);
2758
- },
2759
- onTokenRefreshed: ({ clientName, tokenCount }) => {
2760
- logRuntimeEvent("Refreshed", `Mobile session rotated${clientName ? ` for ${clientName}` : ""}; ${formatClientCount(tokenCount)} active.`);
2761
- }
2762
- } }).fetch,
2763
- hostname: hostname$1,
2764
- port
2765
- }, (info) => {
2766
- const listenUrl = `http://${info.address}:${info.port}`;
2767
- const connectUrl = getConfiguredConnectUrl() ?? getTailscaleConnectUrl(info.port) ?? getLocalNetworkConnectUrl(info.port) ?? listenUrl;
2768
- const pairingPayload = createPairingQrPayload(connectUrl);
2769
- writeServerState({
2770
- connectUrl,
2771
- host: hostname$1,
2772
- listenUrl,
2773
- pairingPayload,
2774
- port: info.port
2775
- });
2776
- writeBackgroundPid();
2777
- console.log("");
2778
- qrcode.generate(pairingPayload, { small: true });
2779
- console.log(formatStartupInstructions({
2780
- connectUrl,
2781
- listenUrl,
2782
- pairingPayload,
2783
- port: info.port
2784
- }));
2785
- });
2786
- function formatStartupInstructions(details) {
2787
- return [
2788
- "",
2789
- ...[
2790
- `${color.prompt("›")} Scan the QR code above to pair ${color.brand("Codex Relay mobile")}.`,
2791
- "",
2792
- `${color.prompt("›")} Mobile: ${color.url(details.connectUrl)}`,
2793
- `${color.prompt("›")} Server: ${color.muted(details.listenUrl)}`,
2794
- "",
2795
- `${color.prompt("›")} Pairing: ${color.url(details.pairingPayload)}`,
2796
- "",
2797
- `${color.prompt("›")} Waiting for pairing requests`,
2798
- `${color.prompt("›")} Approve a device with ${color.command(formatApprovalCommand("<code>", details.port))}`
2799
- ],
2800
- ""
2801
- ].join("\n");
2802
- }
2803
- function logRuntimeEvent(label, message) {
2804
- console.log(`${color.prompt("›")} ${color.event(label.padEnd(8))} ${message}`);
2805
- }
2806
- function formatClientCount(tokenCount) {
2807
- return `${tokenCount} client${tokenCount === 1 ? "" : "s"}`;
2808
- }
2809
- function createPairingQrPayload(serverUrl) {
2810
- const url = new URL("codex-relay://pair");
2811
- url.searchParams.set("serverUrl", serverUrl);
2812
- url.searchParams.set("serverPublicKey", serverIdentity.publicKey);
2813
- return url.toString();
2814
- }
2815
- function hashClientToken(token) {
2816
- return createHash("sha256").update(token).digest("base64url");
2817
- }
2818
- function getConfiguredConnectUrl() {
2819
- const configuredUrl = normalizeUrl(process.env.CODEX_RELAY_PUBLIC_URL);
2820
- if (configuredUrl) return configuredUrl;
2821
- }
2822
- function getTailscaleConnectUrl(port) {
2823
- const status = getTailscaleStatus();
2824
- const tailscaleIp = status?.Self?.TailscaleIPs?.find((ip) => ip.startsWith("100.") && ip.includes("."));
2825
- if (tailscaleIp) return `http://${tailscaleIp}:${port}`;
2826
- const dnsName = status?.Self?.DNSName?.replace(/\.$/, "");
2827
- if (dnsName) return getTailscaleServeHttpsUrl(dnsName, port) ?? `http://${dnsName}:${port}`;
2828
- const tailscaleHost = status?.Self?.TailscaleIPs?.find((ip) => ip.includes("."));
2829
- return tailscaleHost ? `http://${tailscaleHost}:${port}` : void 0;
2830
- }
2831
- async function getApprovalSecret() {
2832
- if (process.env.CODEX_RELAY_APPROVAL_SECRET) return process.env.CODEX_RELAY_APPROVAL_SECRET;
2833
- const path = resolve(process.cwd(), ".codex-relay/approval-secret");
2834
- try {
2835
- return (await readFile(path, "utf8")).trim();
2836
- } catch {
2837
- const secret = randomBytes(32).toString("base64url");
2838
- await mkdir(dirname(path), { recursive: true });
2839
- await writeFile(path, `${secret}\n`, { mode: 384 });
2840
- return secret;
2841
- }
2842
- }
2843
- async function writeServerState(details) {
2844
- const path = resolve(process.cwd(), ".codex-relay/server-state.json");
2845
- await mkdir(dirname(path), { recursive: true });
2846
- await writeFile(path, `${JSON.stringify(details)}\n`, { mode: 384 });
2847
- }
2848
- async function writeBackgroundPid() {
2849
- const path = process.env.CODEX_RELAY_PID_PATH;
2850
- if (!path) return;
2851
- await mkdir(dirname(path), { recursive: true });
2852
- await writeFile(path, `${process.pid}\n`, { mode: 384 });
2853
- }
2854
- function formatApprovalCommand(approvalCode, activePort) {
2855
- return activePort === 8787 ? `npx codex-relay approve ${approvalCode}` : `PORT=${activePort} npx codex-relay approve ${approvalCode}`;
2856
- }
2857
- function getLocalNetworkConnectUrl(port) {
2858
- for (const addresses of Object.values(networkInterfaces())) for (const address of addresses ?? []) if (address.family === "IPv4" && !address.internal) return `http://${address.address}:${port}`;
2859
- }
2860
- function normalizeUrl(value) {
2861
- if (!value) return;
2862
- const trimmed = value.trim().replace(/\/$/, "");
2863
- if (!trimmed) return;
2864
- try {
2865
- const url = new URL(trimmed);
2866
- return url.protocol === "http:" || url.protocol === "https:" ? url.toString().replace(/\/$/, "") : void 0;
2867
- } catch {
2868
- return;
2869
- }
2870
- }
2871
- function getTailscaleStatus() {
2872
- try {
2873
- const output = execFileSync("tailscale", ["status", "--json"], {
2874
- encoding: "utf8",
2875
- stdio: [
2876
- "ignore",
2877
- "pipe",
2878
- "ignore"
2879
- ],
2880
- timeout: 1500
2881
- });
2882
- return JSON.parse(output);
2883
- } catch {
2884
- return;
2885
- }
2886
- }
2887
- function getTailscaleServeHttpsUrl(dnsName, port) {
2888
- try {
2889
- const output = execFileSync("tailscale", [
2890
- "serve",
2891
- "status",
2892
- "--json"
2893
- ], {
2894
- encoding: "utf8",
2895
- stdio: [
2896
- "ignore",
2897
- "pipe",
2898
- "ignore"
2899
- ],
2900
- timeout: 1500
2901
- });
2902
- const serveStatus = JSON.parse(output);
2903
- const portKey = String(port);
2904
- const hostPort = `${dnsName}:${portKey}`;
2905
- return serveStatus.TCP?.[portKey]?.HTTPS && serveStatus.Web?.[hostPort] ? `https://${hostPort}` : void 0;
2906
- } catch {
2907
- return;
2908
- }
2909
- }
2910
- //#endregion
2911
- export {};