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/README.md +31 -24
- package/api-schema.ts +1 -0
- package/dist/api-schema.js +2 -0
- package/dist/api-schema2.js +1349 -0
- package/dist/cli.js +39 -20
- package/dist/collaboration-mode-templates/default.md +11 -0
- package/dist/collaboration-mode-templates/execute.md +45 -0
- package/dist/collaboration-mode-templates/pair_programming.md +7 -0
- package/dist/collaboration-mode-templates/plan.md +128 -0
- package/dist/paths.js +25 -0
- package/dist/src.js +5349 -696
- package/package.json +12 -5
- package/src/api-schema.ts +1453 -0
- package/dist/src2.js +0 -2911
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 {};
|