@ycodium-ai/agent-codex 0.2.2098
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 +26 -0
- package/assets/icon.svg +7 -0
- package/dist/index.js +3834 -0
- package/package.json +41 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,3834 @@
|
|
|
1
|
+
// src/index.ts
|
|
2
|
+
import { definePlugin } from "@ycodium-ai/plugin-api";
|
|
3
|
+
|
|
4
|
+
// src/card.ts
|
|
5
|
+
var CODEX_DRIVER_KIND = "codex";
|
|
6
|
+
var CODEX_DEFAULT_LAUNCH_ARGS = "app-server";
|
|
7
|
+
var CODEX_CARD = {
|
|
8
|
+
driverKind: CODEX_DRIVER_KIND,
|
|
9
|
+
displayName: "Codex",
|
|
10
|
+
iconUrl: "./assets/icon.svg",
|
|
11
|
+
commandName: "codex",
|
|
12
|
+
launchArgs: CODEX_DEFAULT_LAUNCH_ARGS,
|
|
13
|
+
homeEnvVar: "CODEX_HOME",
|
|
14
|
+
environment: [],
|
|
15
|
+
install: { kind: "npm" },
|
|
16
|
+
signIn: [{ kind: "command", command: "codex login" }],
|
|
17
|
+
deviceCodeSignInArgs: ["login", "--device-auth"],
|
|
18
|
+
npmPackage: "@openai/codex",
|
|
19
|
+
homebrewFormula: "codex",
|
|
20
|
+
selfUpdateArgs: [],
|
|
21
|
+
docsUrl: "https://docs.codex.dev",
|
|
22
|
+
setupNote: "",
|
|
23
|
+
defaultModel: "gpt-5.6-sol",
|
|
24
|
+
defaultTextGenerationModel: "gpt-5.6-luna",
|
|
25
|
+
customModelIdExample: "gpt-6.7-codex-ultra-preview",
|
|
26
|
+
modelSlugAliases: {
|
|
27
|
+
"gpt-5-codex": "gpt-5.4",
|
|
28
|
+
"5.4": "gpt-5.4",
|
|
29
|
+
"5.3": "gpt-5.3-codex",
|
|
30
|
+
"gpt-5.3": "gpt-5.3-codex",
|
|
31
|
+
"5.3-spark": "gpt-5.3-codex-spark",
|
|
32
|
+
"gpt-5.3-spark": "gpt-5.3-codex-spark"
|
|
33
|
+
},
|
|
34
|
+
modelsDevProviderIds: ["openai"],
|
|
35
|
+
modelsDevSlugPrefixed: false,
|
|
36
|
+
modelSlugPrefix: "",
|
|
37
|
+
mcpTransports: ["stdio", "http"],
|
|
38
|
+
pluginManaged: true,
|
|
39
|
+
verifiedSkills: true,
|
|
40
|
+
everydayHarness: true,
|
|
41
|
+
capabilities: {
|
|
42
|
+
turnSteering: true,
|
|
43
|
+
sessionModelSwitch: "in-session",
|
|
44
|
+
exclusiveSessionWriter: true,
|
|
45
|
+
runtimeModeSwitch: "next-turn",
|
|
46
|
+
contextCompaction: "native"
|
|
47
|
+
},
|
|
48
|
+
credentialSources: [],
|
|
49
|
+
diagnostics: [],
|
|
50
|
+
configSchema: {
|
|
51
|
+
type: "object",
|
|
52
|
+
properties: {
|
|
53
|
+
authMode: { type: "string", enum: ["default", "chatgpt", "apiKey"] },
|
|
54
|
+
apiBaseUrl: { type: "string" },
|
|
55
|
+
enabled: { type: "boolean" },
|
|
56
|
+
binaryPath: { type: "string" },
|
|
57
|
+
homePath: { type: "string" },
|
|
58
|
+
shadowHomePath: { type: "string" },
|
|
59
|
+
launchArgs: { type: "string" },
|
|
60
|
+
customModels: { type: "array" },
|
|
61
|
+
driver: { type: "string" },
|
|
62
|
+
config: { type: "object" },
|
|
63
|
+
environment: { type: "array" }
|
|
64
|
+
},
|
|
65
|
+
additionalProperties: true
|
|
66
|
+
},
|
|
67
|
+
modelConnectivity: {
|
|
68
|
+
dialects: ["openai-responses"],
|
|
69
|
+
acceptsExternalEndpoints: true,
|
|
70
|
+
nativePathSuffix: "/responses",
|
|
71
|
+
credentialEnvKey: "CODEX_API_KEY",
|
|
72
|
+
supportsNativeSubscription: true
|
|
73
|
+
}
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
// src/codexInstance.ts
|
|
77
|
+
import * as NodeProcess from "node:process";
|
|
78
|
+
|
|
79
|
+
// src/codexAccount.ts
|
|
80
|
+
import * as NodeFS from "node:fs";
|
|
81
|
+
import * as NodePath from "node:path";
|
|
82
|
+
var AUTH_FILE_NAME = "auth.json";
|
|
83
|
+
var isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
84
|
+
var readString = (record, key) => {
|
|
85
|
+
const value = record[key];
|
|
86
|
+
if (typeof value !== "string") return void 0;
|
|
87
|
+
const trimmed = value.trim();
|
|
88
|
+
return trimmed.length > 0 ? trimmed : void 0;
|
|
89
|
+
};
|
|
90
|
+
var readNumber = (record, key) => {
|
|
91
|
+
const value = record[key];
|
|
92
|
+
return typeof value === "number" && Number.isFinite(value) ? value : void 0;
|
|
93
|
+
};
|
|
94
|
+
var parseJsonRecord = (raw) => {
|
|
95
|
+
let parsed;
|
|
96
|
+
try {
|
|
97
|
+
parsed = JSON.parse(raw);
|
|
98
|
+
} catch {
|
|
99
|
+
return void 0;
|
|
100
|
+
}
|
|
101
|
+
return isRecord(parsed) ? parsed : void 0;
|
|
102
|
+
};
|
|
103
|
+
var directoryExists = (homePath) => {
|
|
104
|
+
try {
|
|
105
|
+
return NodeFS.statSync(homePath).isDirectory();
|
|
106
|
+
} catch {
|
|
107
|
+
return false;
|
|
108
|
+
}
|
|
109
|
+
};
|
|
110
|
+
var readCredentialFile = (path) => {
|
|
111
|
+
try {
|
|
112
|
+
return NodeFS.readFileSync(path, "utf8");
|
|
113
|
+
} catch {
|
|
114
|
+
return void 0;
|
|
115
|
+
}
|
|
116
|
+
};
|
|
117
|
+
var signedOutProbe = (exists) => ({
|
|
118
|
+
state: "signed-out",
|
|
119
|
+
directoryExists: exists,
|
|
120
|
+
email: void 0,
|
|
121
|
+
name: void 0,
|
|
122
|
+
expiresAt: void 0
|
|
123
|
+
});
|
|
124
|
+
var unreadableProbe = (exists) => ({
|
|
125
|
+
state: "unreadable",
|
|
126
|
+
directoryExists: exists,
|
|
127
|
+
email: void 0,
|
|
128
|
+
name: void 0,
|
|
129
|
+
expiresAt: void 0
|
|
130
|
+
});
|
|
131
|
+
var epochMillisToIso = (ms) => {
|
|
132
|
+
if (ms === void 0) return void 0;
|
|
133
|
+
const date = new Date(ms);
|
|
134
|
+
return Number.isNaN(date.getTime()) ? void 0 : date.toISOString();
|
|
135
|
+
};
|
|
136
|
+
function decodeJwtClaims(token) {
|
|
137
|
+
if (token === void 0) return void 0;
|
|
138
|
+
const segments = token.split(".");
|
|
139
|
+
if (segments.length !== 3) return void 0;
|
|
140
|
+
try {
|
|
141
|
+
const json = Buffer.from(segments[1] ?? "", "base64url").toString("utf8");
|
|
142
|
+
return parseJsonRecord(json);
|
|
143
|
+
} catch {
|
|
144
|
+
return void 0;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
var readCodexAccount = (homePath) => {
|
|
148
|
+
const exists = directoryExists(homePath);
|
|
149
|
+
const raw = readCredentialFile(NodePath.join(homePath, AUTH_FILE_NAME));
|
|
150
|
+
if (raw === void 0) return signedOutProbe(exists);
|
|
151
|
+
const parsed = parseJsonRecord(raw);
|
|
152
|
+
if (parsed === void 0 || !("OPENAI_API_KEY" in parsed || "tokens" in parsed)) {
|
|
153
|
+
return unreadableProbe(exists);
|
|
154
|
+
}
|
|
155
|
+
const tokens = parsed["tokens"];
|
|
156
|
+
const accessToken = isRecord(tokens) ? readString(tokens, "access_token") : void 0;
|
|
157
|
+
const apiKey = readString(parsed, "OPENAI_API_KEY");
|
|
158
|
+
if (accessToken === void 0 && apiKey === void 0) return signedOutProbe(exists);
|
|
159
|
+
const claims = isRecord(tokens) ? decodeJwtClaims(readString(tokens, "id_token")) : void 0;
|
|
160
|
+
const expiresAtSeconds = claims === void 0 ? void 0 : readNumber(claims, "exp");
|
|
161
|
+
const expiresAtMs = expiresAtSeconds === void 0 ? void 0 : expiresAtSeconds * 1e3;
|
|
162
|
+
const expiresAt = epochMillisToIso(expiresAtMs);
|
|
163
|
+
const nowMs = Date.now();
|
|
164
|
+
const expired = expiresAt !== void 0 && expiresAtMs !== void 0 && expiresAtMs <= nowMs;
|
|
165
|
+
return {
|
|
166
|
+
state: expired ? "expired" : "signed-in",
|
|
167
|
+
directoryExists: exists,
|
|
168
|
+
email: claims === void 0 ? void 0 : readString(claims, "email"),
|
|
169
|
+
name: void 0,
|
|
170
|
+
expiresAt
|
|
171
|
+
};
|
|
172
|
+
};
|
|
173
|
+
|
|
174
|
+
// src/config.ts
|
|
175
|
+
var CODEX_INSTANCE_API_KEY_ENV = "YCODIUM_CODEX_API_KEY";
|
|
176
|
+
var CODEX_INSTANCE_MODEL_PROVIDER = "ycodium_api";
|
|
177
|
+
function isRecord2(value) {
|
|
178
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
179
|
+
}
|
|
180
|
+
function readString2(value) {
|
|
181
|
+
return typeof value === "string" ? value : "";
|
|
182
|
+
}
|
|
183
|
+
function readTrimmed(value) {
|
|
184
|
+
return readString2(value).trim();
|
|
185
|
+
}
|
|
186
|
+
function readAuthMode(value) {
|
|
187
|
+
return value === "chatgpt" || value === "apiKey" ? value : "default";
|
|
188
|
+
}
|
|
189
|
+
function readEnvironment(value) {
|
|
190
|
+
if (!Array.isArray(value)) return {};
|
|
191
|
+
const env = {};
|
|
192
|
+
for (const entry of value) {
|
|
193
|
+
if (!isRecord2(entry)) continue;
|
|
194
|
+
const name = readTrimmed(entry["name"]);
|
|
195
|
+
if (name.length === 0) continue;
|
|
196
|
+
env[name] = readString2(entry["value"]);
|
|
197
|
+
}
|
|
198
|
+
return env;
|
|
199
|
+
}
|
|
200
|
+
function parseCodexInstanceConfig(config) {
|
|
201
|
+
if (!isRecord2(config)) {
|
|
202
|
+
return {
|
|
203
|
+
binaryPath: "",
|
|
204
|
+
launchArgs: CODEX_DEFAULT_LAUNCH_ARGS,
|
|
205
|
+
homePath: "",
|
|
206
|
+
shadowHomePath: "",
|
|
207
|
+
homeEnvVar: "",
|
|
208
|
+
authMode: "default",
|
|
209
|
+
apiBaseUrl: "",
|
|
210
|
+
environment: {},
|
|
211
|
+
envelopeDriver: void 0
|
|
212
|
+
};
|
|
213
|
+
}
|
|
214
|
+
const nested = isRecord2(config["config"]) ? config["config"] : void 0;
|
|
215
|
+
const settings = nested ?? config;
|
|
216
|
+
return {
|
|
217
|
+
binaryPath: readTrimmed(settings["binaryPath"]),
|
|
218
|
+
launchArgs: readString2(settings["launchArgs"]) || CODEX_DEFAULT_LAUNCH_ARGS,
|
|
219
|
+
homePath: readTrimmed(settings["homePath"]) || readTrimmed(config["homePath"]),
|
|
220
|
+
shadowHomePath: readTrimmed(settings["shadowHomePath"]),
|
|
221
|
+
homeEnvVar: readTrimmed(settings["homeEnvVar"]) || readTrimmed(config["homeEnvVar"]),
|
|
222
|
+
authMode: readAuthMode(settings["authMode"]),
|
|
223
|
+
apiBaseUrl: readTrimmed(settings["apiBaseUrl"]),
|
|
224
|
+
environment: readEnvironment(config["environment"]),
|
|
225
|
+
envelopeDriver: readTrimmed(config["driver"]) || void 0
|
|
226
|
+
};
|
|
227
|
+
}
|
|
228
|
+
function tokenizeLaunchArgs(value) {
|
|
229
|
+
const input = value.trim();
|
|
230
|
+
if (!input) return [];
|
|
231
|
+
const tokens = [];
|
|
232
|
+
let current = "";
|
|
233
|
+
let quote;
|
|
234
|
+
let quoted = false;
|
|
235
|
+
for (let index = 0; index < input.length; index++) {
|
|
236
|
+
const char = input[index];
|
|
237
|
+
if (char === void 0) continue;
|
|
238
|
+
if (quote) {
|
|
239
|
+
if (char === quote) {
|
|
240
|
+
quote = void 0;
|
|
241
|
+
quoted = true;
|
|
242
|
+
} else if (char === "\\" && quote === '"') {
|
|
243
|
+
const next = input[index + 1];
|
|
244
|
+
if (next !== void 0 && ['"', "\\", "$", "`"].includes(next)) {
|
|
245
|
+
current += next;
|
|
246
|
+
index++;
|
|
247
|
+
} else {
|
|
248
|
+
current += char;
|
|
249
|
+
}
|
|
250
|
+
} else {
|
|
251
|
+
current += char;
|
|
252
|
+
}
|
|
253
|
+
continue;
|
|
254
|
+
}
|
|
255
|
+
if (char === "'" || char === '"') {
|
|
256
|
+
quote = char;
|
|
257
|
+
quoted = true;
|
|
258
|
+
} else if (/\s/.test(char)) {
|
|
259
|
+
if (current || quoted) {
|
|
260
|
+
tokens.push(current);
|
|
261
|
+
current = "";
|
|
262
|
+
quoted = false;
|
|
263
|
+
}
|
|
264
|
+
} else if (char === "\\") {
|
|
265
|
+
const next = input[index + 1];
|
|
266
|
+
if (next !== void 0 && /\s/.test(next)) {
|
|
267
|
+
current += next;
|
|
268
|
+
index++;
|
|
269
|
+
} else {
|
|
270
|
+
current += char;
|
|
271
|
+
}
|
|
272
|
+
} else {
|
|
273
|
+
current += char;
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
if (current || quoted) tokens.push(current);
|
|
277
|
+
return tokens;
|
|
278
|
+
}
|
|
279
|
+
function codexExecLaunchArgs(launchArgs) {
|
|
280
|
+
const args = tokenizeLaunchArgs(launchArgs ?? "");
|
|
281
|
+
const execArgs = [];
|
|
282
|
+
for (let index = 0; index < args.length; index++) {
|
|
283
|
+
const arg = args[index];
|
|
284
|
+
if (arg === void 0) continue;
|
|
285
|
+
if (arg === "--strict-config" || arg.startsWith("--config=") || arg.startsWith("-c=")) {
|
|
286
|
+
execArgs.push(arg);
|
|
287
|
+
} else if (arg === "--config" || arg === "-c" || arg === "--enable" || arg === "--disable") {
|
|
288
|
+
const value = args[index + 1];
|
|
289
|
+
if (value !== void 0 && !value.startsWith("-")) {
|
|
290
|
+
execArgs.push(arg, value);
|
|
291
|
+
index++;
|
|
292
|
+
}
|
|
293
|
+
} else if (arg.startsWith("--enable=") || arg.startsWith("--disable=")) {
|
|
294
|
+
execArgs.push(arg);
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
return execArgs;
|
|
298
|
+
}
|
|
299
|
+
function hasConfiguredMcpServer(appServerArgs) {
|
|
300
|
+
return appServerArgs.some((arg) => arg.includes("mcp_servers."));
|
|
301
|
+
}
|
|
302
|
+
function encodeCodexConfigOverrides(overrides) {
|
|
303
|
+
return overrides.map(({ key, value }) => `-c ${JSON.stringify(`${key}=${JSON.stringify(value)}`)}`).join(" ");
|
|
304
|
+
}
|
|
305
|
+
function modelIdFromSelection(selection) {
|
|
306
|
+
if (typeof selection !== "object" || selection === null || Array.isArray(selection)) {
|
|
307
|
+
return void 0;
|
|
308
|
+
}
|
|
309
|
+
const model = selection["model"];
|
|
310
|
+
return typeof model === "string" ? model.trim() || void 0 : void 0;
|
|
311
|
+
}
|
|
312
|
+
function modelSelectionStringOption(selection, optionId) {
|
|
313
|
+
if (typeof selection !== "object" || selection === null || Array.isArray(selection)) {
|
|
314
|
+
return void 0;
|
|
315
|
+
}
|
|
316
|
+
const options = selection["options"];
|
|
317
|
+
if (!Array.isArray(options)) return void 0;
|
|
318
|
+
for (const entry of options) {
|
|
319
|
+
if (typeof entry !== "object" || entry === null || Array.isArray(entry)) continue;
|
|
320
|
+
const id = entry["id"];
|
|
321
|
+
if (id !== optionId) continue;
|
|
322
|
+
const value = entry["value"];
|
|
323
|
+
return typeof value === "string" ? value.trim() || void 0 : void 0;
|
|
324
|
+
}
|
|
325
|
+
return void 0;
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
// src/paths.ts
|
|
329
|
+
import * as NodeOS from "node:os";
|
|
330
|
+
import * as NodePath2 from "node:path";
|
|
331
|
+
function expandHomePath(value) {
|
|
332
|
+
if (value === "~") return NodeOS.homedir();
|
|
333
|
+
if (value.startsWith("~/") || value.startsWith("~\\")) {
|
|
334
|
+
return NodePath2.join(NodeOS.homedir(), value.slice(2));
|
|
335
|
+
}
|
|
336
|
+
return value;
|
|
337
|
+
}
|
|
338
|
+
function compactEnv(env) {
|
|
339
|
+
const out = {};
|
|
340
|
+
for (const [key, value] of Object.entries(env)) {
|
|
341
|
+
if (value !== void 0) out[key] = value;
|
|
342
|
+
}
|
|
343
|
+
return out;
|
|
344
|
+
}
|
|
345
|
+
function nowIso() {
|
|
346
|
+
return (/* @__PURE__ */ new Date()).toISOString();
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
// src/connection.ts
|
|
350
|
+
function codexApiConnectionError(recipe, environment) {
|
|
351
|
+
if (recipe.authMode !== "apiKey") return null;
|
|
352
|
+
try {
|
|
353
|
+
const url = new URL(recipe.apiBaseUrl);
|
|
354
|
+
if (url.protocol !== "http:" && url.protocol !== "https:") {
|
|
355
|
+
return "Codex API base URL must use HTTP or HTTPS.";
|
|
356
|
+
}
|
|
357
|
+
} catch {
|
|
358
|
+
return "Codex API base URL is missing or invalid.";
|
|
359
|
+
}
|
|
360
|
+
return environment[CODEX_INSTANCE_API_KEY_ENV]?.trim() ? null : "An API key is required for this Codex account.";
|
|
361
|
+
}
|
|
362
|
+
function connectionArgv(recipe, layout) {
|
|
363
|
+
const args = [...tokenizeLaunchArgs(recipe.launchArgs)];
|
|
364
|
+
const override = (key, value) => {
|
|
365
|
+
args.push("-c", `${key}=${JSON.stringify(value)}`);
|
|
366
|
+
};
|
|
367
|
+
if (layout.mode === "authOverlay" || recipe.authMode !== "default") {
|
|
368
|
+
override("sqlite_home", layout.sharedHomePath);
|
|
369
|
+
}
|
|
370
|
+
if (layout.mode === "authOverlay") {
|
|
371
|
+
override("cli_auth_credentials_store", "file");
|
|
372
|
+
}
|
|
373
|
+
if (recipe.authMode === "chatgpt") {
|
|
374
|
+
override("model_provider", "openai");
|
|
375
|
+
override("forced_login_method", "chatgpt");
|
|
376
|
+
} else if (recipe.authMode === "apiKey") {
|
|
377
|
+
override("model_provider", CODEX_INSTANCE_MODEL_PROVIDER);
|
|
378
|
+
override(`model_providers.${CODEX_INSTANCE_MODEL_PROVIDER}.name`, "Ycodium API");
|
|
379
|
+
override(`model_providers.${CODEX_INSTANCE_MODEL_PROVIDER}.base_url`, recipe.apiBaseUrl);
|
|
380
|
+
override(
|
|
381
|
+
`model_providers.${CODEX_INSTANCE_MODEL_PROVIDER}.env_key`,
|
|
382
|
+
CODEX_INSTANCE_API_KEY_ENV
|
|
383
|
+
);
|
|
384
|
+
override(`model_providers.${CODEX_INSTANCE_MODEL_PROVIDER}.requires_openai_auth`, false);
|
|
385
|
+
override(`model_providers.${CODEX_INSTANCE_MODEL_PROVIDER}.wire_api`, "responses");
|
|
386
|
+
}
|
|
387
|
+
return args;
|
|
388
|
+
}
|
|
389
|
+
function codexSpawnArgs(recipe, layout) {
|
|
390
|
+
const needsOverrides = layout.mode === "authOverlay" || recipe.authMode !== "default";
|
|
391
|
+
if (!needsOverrides) return tokenizeLaunchArgs(recipe.launchArgs);
|
|
392
|
+
return connectionArgv(recipe, layout);
|
|
393
|
+
}
|
|
394
|
+
function crushInheritedApiKeys(env) {
|
|
395
|
+
const out = { ...env };
|
|
396
|
+
delete out["OPENAI_API_KEY"];
|
|
397
|
+
delete out["CODEX_API_KEY"];
|
|
398
|
+
return out;
|
|
399
|
+
}
|
|
400
|
+
function buildCodexSpawnEnv(input) {
|
|
401
|
+
const merged = {
|
|
402
|
+
...compactEnv(input.processEnv),
|
|
403
|
+
...input.layout.effectiveHomePath !== void 0 ? { CODEX_HOME: input.layout.effectiveHomePath } : {},
|
|
404
|
+
...input.recipe.environment
|
|
405
|
+
};
|
|
406
|
+
const withoutHomeOverride = { ...merged };
|
|
407
|
+
if (input.processEnv["HOME"] !== void 0) {
|
|
408
|
+
withoutHomeOverride["HOME"] = input.processEnv["HOME"];
|
|
409
|
+
} else {
|
|
410
|
+
delete withoutHomeOverride["HOME"];
|
|
411
|
+
}
|
|
412
|
+
return input.crushApiKeys ? crushInheritedApiKeys(withoutHomeOverride) : withoutHomeOverride;
|
|
413
|
+
}
|
|
414
|
+
function nativeLoginSpawnEnv(processEnv, isolatedHome) {
|
|
415
|
+
const merged = compactEnv(processEnv);
|
|
416
|
+
delete merged["OPENAI_API_KEY"];
|
|
417
|
+
delete merged["CODEX_API_KEY"];
|
|
418
|
+
delete merged["CODEX_HOME"];
|
|
419
|
+
merged["CODEX_HOME"] = isolatedHome;
|
|
420
|
+
return merged;
|
|
421
|
+
}
|
|
422
|
+
function shouldCrushApiKeys(authMode) {
|
|
423
|
+
return authMode === "chatgpt";
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
// src/homeLayout.ts
|
|
427
|
+
import * as NodeFSP from "node:fs/promises";
|
|
428
|
+
import * as NodeOS2 from "node:os";
|
|
429
|
+
import * as NodePath3 from "node:path";
|
|
430
|
+
var KNOWN_SHARED_DIRECTORIES = [
|
|
431
|
+
"sessions",
|
|
432
|
+
"archived_sessions",
|
|
433
|
+
"sqlite",
|
|
434
|
+
"shell_snapshots",
|
|
435
|
+
"worktrees",
|
|
436
|
+
"skills",
|
|
437
|
+
"plugins",
|
|
438
|
+
"cache",
|
|
439
|
+
"logs",
|
|
440
|
+
"mcp-oauth-locks"
|
|
441
|
+
];
|
|
442
|
+
var PRIVATE_ENTRY_NAMES = /* @__PURE__ */ new Set(["auth.json", "models_cache.json"]);
|
|
443
|
+
var SHADOW_LOCAL_ENTRY_NAMES = /* @__PURE__ */ new Set(["log", "memories", "tmp"]);
|
|
444
|
+
var REPLACEABLE_SHARED_RUNTIME_DIRECTORIES = /* @__PURE__ */ new Set(["mcp-oauth-locks"]);
|
|
445
|
+
function resolveCodexHomeDirectory(value) {
|
|
446
|
+
const expanded = value && value.trim().length > 0 ? expandHomePath(value) : NodePath3.join(NodeOS2.homedir(), ".codex");
|
|
447
|
+
return NodePath3.resolve(expanded);
|
|
448
|
+
}
|
|
449
|
+
function resolveCodexHomeLayout(input) {
|
|
450
|
+
const configuredHome = input.homePath.trim() || (input.environmentHome?.trim() ?? "");
|
|
451
|
+
const sharedHomePath = resolveCodexHomeDirectory(configuredHome || void 0);
|
|
452
|
+
const shadowHomePath = input.shadowHomePath.trim();
|
|
453
|
+
if (shadowHomePath.length === 0) {
|
|
454
|
+
return {
|
|
455
|
+
mode: "direct",
|
|
456
|
+
sharedHomePath,
|
|
457
|
+
effectiveHomePath: configuredHome.length > 0 ? sharedHomePath : void 0,
|
|
458
|
+
continuationKey: `codex:home:${sharedHomePath}`
|
|
459
|
+
};
|
|
460
|
+
}
|
|
461
|
+
return {
|
|
462
|
+
mode: "authOverlay",
|
|
463
|
+
sharedHomePath,
|
|
464
|
+
effectiveHomePath: NodePath3.resolve(expandHomePath(shadowHomePath)),
|
|
465
|
+
continuationKey: `codex:home:${sharedHomePath}`
|
|
466
|
+
};
|
|
467
|
+
}
|
|
468
|
+
async function readLinkState(linkPath) {
|
|
469
|
+
try {
|
|
470
|
+
const target = await NodeFSP.readlink(linkPath);
|
|
471
|
+
return { _tag: "Symlink", target };
|
|
472
|
+
} catch (error) {
|
|
473
|
+
const code = typeof error === "object" && error !== null && "code" in error ? String(error.code) : "";
|
|
474
|
+
if (code === "ENOENT") return { _tag: "Missing" };
|
|
475
|
+
if (code === "EINVAL") return { _tag: "NotSymlink" };
|
|
476
|
+
throw error;
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
async function createEntryLink(target, link) {
|
|
480
|
+
const type = await NodeFSP.stat(target).then(
|
|
481
|
+
(info) => info.isDirectory() ? "junction" : "file",
|
|
482
|
+
() => "file"
|
|
483
|
+
);
|
|
484
|
+
await NodeFSP.symlink(target, link, type);
|
|
485
|
+
}
|
|
486
|
+
async function removePrivateSymlink(effectiveHomePath, entryName) {
|
|
487
|
+
const privatePath = NodePath3.join(effectiveHomePath, entryName);
|
|
488
|
+
const state = await readLinkState(privatePath);
|
|
489
|
+
if (state._tag === "Symlink") await NodeFSP.rm(privatePath, { force: true });
|
|
490
|
+
}
|
|
491
|
+
async function ensureSymlink(input) {
|
|
492
|
+
const target = NodePath3.join(input.sharedHomePath, input.entryName);
|
|
493
|
+
const link = NodePath3.join(input.effectiveHomePath, input.entryName);
|
|
494
|
+
const state = await readLinkState(link);
|
|
495
|
+
if (state._tag === "NotSymlink") {
|
|
496
|
+
if (!REPLACEABLE_SHARED_RUNTIME_DIRECTORIES.has(input.entryName)) {
|
|
497
|
+
throw new Error(
|
|
498
|
+
`Cannot create Codex shadow home entry '${input.entryName}' because '${link}' already exists and is not a symlink.`
|
|
499
|
+
);
|
|
500
|
+
}
|
|
501
|
+
await NodeFSP.rm(link, { recursive: true, force: true });
|
|
502
|
+
await createEntryLink(target, link);
|
|
503
|
+
return;
|
|
504
|
+
}
|
|
505
|
+
if (state._tag === "Missing") {
|
|
506
|
+
await createEntryLink(target, link);
|
|
507
|
+
return;
|
|
508
|
+
}
|
|
509
|
+
const resolvedExisting = NodePath3.resolve(NodePath3.dirname(link), state.target);
|
|
510
|
+
if (resolvedExisting !== target) {
|
|
511
|
+
await NodeFSP.rm(link, { force: true });
|
|
512
|
+
await createEntryLink(target, link);
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
async function materializeCodexShadowHome(layout, log) {
|
|
516
|
+
if (layout.mode !== "authOverlay") return;
|
|
517
|
+
const effectiveHomePath = layout.effectiveHomePath;
|
|
518
|
+
if (!effectiveHomePath) return;
|
|
519
|
+
if (layout.sharedHomePath === effectiveHomePath) {
|
|
520
|
+
throw new Error(
|
|
521
|
+
`Codex shadow home path '${effectiveHomePath}' must be different from the shared home path '${layout.sharedHomePath}'.`
|
|
522
|
+
);
|
|
523
|
+
}
|
|
524
|
+
await NodeFSP.mkdir(layout.sharedHomePath, { recursive: true });
|
|
525
|
+
await NodeFSP.mkdir(effectiveHomePath, { recursive: true });
|
|
526
|
+
await Promise.all(
|
|
527
|
+
KNOWN_SHARED_DIRECTORIES.map(
|
|
528
|
+
(directory) => NodeFSP.mkdir(NodePath3.join(layout.sharedHomePath, directory), { recursive: true })
|
|
529
|
+
)
|
|
530
|
+
);
|
|
531
|
+
let sharedEntryNames = [];
|
|
532
|
+
try {
|
|
533
|
+
sharedEntryNames = await NodeFSP.readdir(layout.sharedHomePath);
|
|
534
|
+
} catch (error) {
|
|
535
|
+
throw new Error(
|
|
536
|
+
`Codex shadow home filesystem operation 'readDirectory' failed for '${layout.sharedHomePath}': ${String(error)}`,
|
|
537
|
+
{ cause: error }
|
|
538
|
+
);
|
|
539
|
+
}
|
|
540
|
+
const entries = new Set(KNOWN_SHARED_DIRECTORIES);
|
|
541
|
+
for (const entryName of sharedEntryNames) {
|
|
542
|
+
if (!PRIVATE_ENTRY_NAMES.has(entryName) && !SHADOW_LOCAL_ENTRY_NAMES.has(entryName)) {
|
|
543
|
+
entries.add(entryName);
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
for (const entryName of PRIVATE_ENTRY_NAMES) {
|
|
547
|
+
if (entryName === "auth.json") continue;
|
|
548
|
+
await removePrivateSymlink(effectiveHomePath, entryName);
|
|
549
|
+
}
|
|
550
|
+
for (const entryName of entries) {
|
|
551
|
+
if (PRIVATE_ENTRY_NAMES.has(entryName)) continue;
|
|
552
|
+
try {
|
|
553
|
+
await ensureSymlink({
|
|
554
|
+
sharedHomePath: layout.sharedHomePath,
|
|
555
|
+
effectiveHomePath,
|
|
556
|
+
entryName
|
|
557
|
+
});
|
|
558
|
+
} catch (error) {
|
|
559
|
+
log?.warn("Codex shadow home entry left unlinked.", {
|
|
560
|
+
entryName,
|
|
561
|
+
effectiveHomePath,
|
|
562
|
+
reason: error instanceof Error ? error.message : String(error)
|
|
563
|
+
});
|
|
564
|
+
}
|
|
565
|
+
}
|
|
566
|
+
const authPath = NodePath3.join(effectiveHomePath, "auth.json");
|
|
567
|
+
const authState = await readLinkState(authPath);
|
|
568
|
+
if (authState._tag === "Symlink") {
|
|
569
|
+
throw new Error(
|
|
570
|
+
`Codex shadow home private entry 'auth.json' at '${authPath}' must be a real file, not a symlink.`
|
|
571
|
+
);
|
|
572
|
+
}
|
|
573
|
+
}
|
|
574
|
+
function accountHomeFromLayout(layout) {
|
|
575
|
+
return layout.effectiveHomePath ?? layout.sharedHomePath;
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
// src/protocol.ts
|
|
579
|
+
var CODEX_INITIALIZE_PARAMS = {
|
|
580
|
+
clientInfo: { name: "ycodium", title: "Ycodium", version: "0" },
|
|
581
|
+
capabilities: { experimentalApi: true }
|
|
582
|
+
};
|
|
583
|
+
var ACTIVE_WRITER_ATTEMPTS = 5;
|
|
584
|
+
var ACTIVE_WRITER_DELAY_MS = 200;
|
|
585
|
+
var DEFAULT_MODEL = "gpt-5.6-sol";
|
|
586
|
+
var DEFAULT_TEXT_GENERATION_REASONING_EFFORT = "low";
|
|
587
|
+
function runtimeModeToThreadConfig(mode) {
|
|
588
|
+
switch (mode) {
|
|
589
|
+
case "approval-required":
|
|
590
|
+
return { approvalPolicy: "untrusted", sandbox: "read-only", approvalsReviewer: "user" };
|
|
591
|
+
case "auto-accept-edits":
|
|
592
|
+
return {
|
|
593
|
+
approvalPolicy: "on-request",
|
|
594
|
+
sandbox: "workspace-write",
|
|
595
|
+
approvalsReviewer: "user"
|
|
596
|
+
};
|
|
597
|
+
case "auto":
|
|
598
|
+
return {
|
|
599
|
+
approvalPolicy: "on-request",
|
|
600
|
+
sandbox: "workspace-write",
|
|
601
|
+
approvalsReviewer: "auto_review"
|
|
602
|
+
};
|
|
603
|
+
case "full-access":
|
|
604
|
+
default:
|
|
605
|
+
return { approvalPolicy: "never", sandbox: "danger-full-access", approvalsReviewer: "user" };
|
|
606
|
+
}
|
|
607
|
+
}
|
|
608
|
+
function runtimeModeToTurnSandboxPolicy(mode) {
|
|
609
|
+
switch (mode) {
|
|
610
|
+
case "approval-required":
|
|
611
|
+
return { type: "readOnly" };
|
|
612
|
+
case "auto-accept-edits":
|
|
613
|
+
case "auto":
|
|
614
|
+
return { type: "workspaceWrite" };
|
|
615
|
+
case "full-access":
|
|
616
|
+
default:
|
|
617
|
+
return { type: "dangerFullAccess" };
|
|
618
|
+
}
|
|
619
|
+
}
|
|
620
|
+
function isRecord3(value) {
|
|
621
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
622
|
+
}
|
|
623
|
+
function asString(value) {
|
|
624
|
+
return typeof value === "string" ? value : null;
|
|
625
|
+
}
|
|
626
|
+
function asNumber(value) {
|
|
627
|
+
return typeof value === "number" && Number.isFinite(value) ? value : null;
|
|
628
|
+
}
|
|
629
|
+
function decodeThreadOpen(result) {
|
|
630
|
+
const thread = isRecord3(result) ? isRecord3(result["thread"]) ? result["thread"] : result : null;
|
|
631
|
+
const id = thread ? asString(thread["id"]) : null;
|
|
632
|
+
if (!id) throw new Error("codex thread/start|resume returned no thread id");
|
|
633
|
+
return {
|
|
634
|
+
threadId: id,
|
|
635
|
+
...asString(thread?.["cwd"]) ? { cwd: asString(thread["cwd"]) } : {},
|
|
636
|
+
...asString(thread?.["model"]) ? { model: asString(thread["model"]) } : {}
|
|
637
|
+
};
|
|
638
|
+
}
|
|
639
|
+
function decodeTurnStart(result) {
|
|
640
|
+
const turn = isRecord3(result) ? isRecord3(result["turn"]) ? result["turn"] : result : null;
|
|
641
|
+
const id = turn ? asString(turn["id"]) : asString(isRecord3(result) ? result["turnId"] : null);
|
|
642
|
+
if (!id) throw new Error("codex turn/start returned no turn id");
|
|
643
|
+
return { turnId: id };
|
|
644
|
+
}
|
|
645
|
+
function inProgressTurnIdFromThreadRead(result) {
|
|
646
|
+
if (!isRecord3(result)) return void 0;
|
|
647
|
+
const thread = isRecord3(result["thread"]) ? result["thread"] : result;
|
|
648
|
+
const turns = Array.isArray(thread["turns"]) ? thread["turns"] : [];
|
|
649
|
+
for (const turn of turns) {
|
|
650
|
+
if (!isRecord3(turn)) continue;
|
|
651
|
+
const status = asString(turn["status"]);
|
|
652
|
+
if (status === "inProgress" || status === "in_progress" || status === "active") {
|
|
653
|
+
return asString(turn["id"]) ?? void 0;
|
|
654
|
+
}
|
|
655
|
+
}
|
|
656
|
+
const active = asString(thread["activeTurnId"]) ?? asString(thread["inProgressTurnId"]);
|
|
657
|
+
return active ?? void 0;
|
|
658
|
+
}
|
|
659
|
+
var RECOVERABLE_THREAD_RESUME_ERROR_SNIPPETS = [
|
|
660
|
+
"not found",
|
|
661
|
+
"missing thread",
|
|
662
|
+
"no such thread",
|
|
663
|
+
"unknown thread",
|
|
664
|
+
"does not exist",
|
|
665
|
+
"no rollout found"
|
|
666
|
+
];
|
|
667
|
+
function isRecoverableThreadResumeError(message) {
|
|
668
|
+
const lower = message.toLowerCase();
|
|
669
|
+
if (!lower.includes("thread")) return false;
|
|
670
|
+
return RECOVERABLE_THREAD_RESUME_ERROR_SNIPPETS.some((snippet) => lower.includes(snippet));
|
|
671
|
+
}
|
|
672
|
+
function isActiveWriterConflict(message) {
|
|
673
|
+
return message.toLowerCase().includes("already has an active writer");
|
|
674
|
+
}
|
|
675
|
+
var BENIGN_ERROR_LOG_SNIPPETS = [
|
|
676
|
+
"state db missing rollout path for thread",
|
|
677
|
+
"state db record_discrepancy: find_thread_path_by_id_str_in_subdir, falling_back"
|
|
678
|
+
];
|
|
679
|
+
function isBenignErrorLog(message) {
|
|
680
|
+
const lower = message.toLowerCase();
|
|
681
|
+
return BENIGN_ERROR_LOG_SNIPPETS.some((snippet) => lower.includes(snippet));
|
|
682
|
+
}
|
|
683
|
+
var FATAL_CODEX_STDERR_SNIPPETS = ["failed to connect to websocket"];
|
|
684
|
+
function isFatalStderr(message) {
|
|
685
|
+
const lower = message.toLowerCase();
|
|
686
|
+
return FATAL_CODEX_STDERR_SNIPPETS.some((snippet) => lower.includes(snippet));
|
|
687
|
+
}
|
|
688
|
+
var CHILD_AGENT_EVENT_METHODS = /* @__PURE__ */ new Set([
|
|
689
|
+
"turn/started",
|
|
690
|
+
"turn/completed",
|
|
691
|
+
"thread/status/changed",
|
|
692
|
+
"thread/tokenUsage/updated",
|
|
693
|
+
"item/started",
|
|
694
|
+
"item/completed",
|
|
695
|
+
"thread/closed",
|
|
696
|
+
"error"
|
|
697
|
+
]);
|
|
698
|
+
var CHILD_CHATTER_METHODS = /* @__PURE__ */ new Set([
|
|
699
|
+
"item/agentMessage/delta",
|
|
700
|
+
"item/reasoning/delta",
|
|
701
|
+
"item/reasoning/summaryDelta",
|
|
702
|
+
"item/reasoning/summaryPartAdded",
|
|
703
|
+
"item/plan/delta",
|
|
704
|
+
"thread/name/updated",
|
|
705
|
+
"thread/archived",
|
|
706
|
+
"thread/unarchived",
|
|
707
|
+
"thread/compacted",
|
|
708
|
+
"thread/started"
|
|
709
|
+
]);
|
|
710
|
+
var MEMORY_CONSOLIDATION_NAME = "memory-consolidation";
|
|
711
|
+
function isMemoryConsolidationThread(name) {
|
|
712
|
+
return (name ?? "").toLowerCase().includes(MEMORY_CONSOLIDATION_NAME);
|
|
713
|
+
}
|
|
714
|
+
function parseCodexResumeCursor(value) {
|
|
715
|
+
if (!isRecord3(value)) return void 0;
|
|
716
|
+
const threadId = asString(value["threadId"])?.trim();
|
|
717
|
+
return threadId ? { threadId } : void 0;
|
|
718
|
+
}
|
|
719
|
+
function conversationIdFromNotification(params) {
|
|
720
|
+
if (!isRecord3(params)) return void 0;
|
|
721
|
+
return asString(params["threadId"]) ?? asString(params["conversationId"]) ?? (isRecord3(params["thread"]) ? asString(params["thread"]["id"]) : null) ?? (isRecord3(params["turn"]) ? asString(params["turn"]["threadId"]) : null) ?? void 0;
|
|
722
|
+
}
|
|
723
|
+
function itemTypeOf(params) {
|
|
724
|
+
if (!isRecord3(params)) return void 0;
|
|
725
|
+
const item = isRecord3(params["item"]) ? params["item"] : params;
|
|
726
|
+
return asString(item["type"]) ?? asString(item["itemType"]) ?? void 0;
|
|
727
|
+
}
|
|
728
|
+
function jsonRpcErrorMessage(error) {
|
|
729
|
+
if (isRecord3(error) && typeof error["message"] === "string") return error["message"];
|
|
730
|
+
try {
|
|
731
|
+
return JSON.stringify(error);
|
|
732
|
+
} catch {
|
|
733
|
+
return String(error);
|
|
734
|
+
}
|
|
735
|
+
}
|
|
736
|
+
|
|
737
|
+
// src/rpc.ts
|
|
738
|
+
var STDERR_TAIL_LIMIT = 4e3;
|
|
739
|
+
var DEFAULT_REQUEST_TIMEOUT_MS = 3e4;
|
|
740
|
+
var ANSI_ESCAPE_CHAR = String.fromCharCode(27);
|
|
741
|
+
var ANSI_RE = new RegExp(`${ANSI_ESCAPE_CHAR}\\[[0-9;]*m`, "g");
|
|
742
|
+
var CODEX_STDERR_LOG_REGEX = /\b(TRACE|DEBUG|INFO|WARN|ERROR)\b/;
|
|
743
|
+
function classifyStderrLine(line) {
|
|
744
|
+
const stripped = line.replace(ANSI_RE, "").trim();
|
|
745
|
+
if (stripped.length === 0) return "drop";
|
|
746
|
+
const match = CODEX_STDERR_LOG_REGEX.exec(stripped);
|
|
747
|
+
if (match === null) return "error";
|
|
748
|
+
const level = match[1];
|
|
749
|
+
if (level === "ERROR") return "error";
|
|
750
|
+
if (level === "WARN") return "warn";
|
|
751
|
+
return "drop";
|
|
752
|
+
}
|
|
753
|
+
async function createCodexRpcClient(handle, handlers) {
|
|
754
|
+
const pending = /* @__PURE__ */ new Map();
|
|
755
|
+
const decoder = new TextDecoder();
|
|
756
|
+
let nextId = 1;
|
|
757
|
+
let stderrTail = "";
|
|
758
|
+
let exitReason = null;
|
|
759
|
+
let closed = false;
|
|
760
|
+
const failAll = (reason) => {
|
|
761
|
+
exitReason = reason;
|
|
762
|
+
for (const [, entry] of pending) {
|
|
763
|
+
entry.reject(new Error(`codex app-server ${entry.method} failed: ${reason}`));
|
|
764
|
+
}
|
|
765
|
+
pending.clear();
|
|
766
|
+
};
|
|
767
|
+
const dispatch = (line) => {
|
|
768
|
+
if (line.trim().length === 0) return;
|
|
769
|
+
let message;
|
|
770
|
+
try {
|
|
771
|
+
message = JSON.parse(line);
|
|
772
|
+
} catch {
|
|
773
|
+
return;
|
|
774
|
+
}
|
|
775
|
+
if (!message || typeof message !== "object") return;
|
|
776
|
+
const typed = message;
|
|
777
|
+
const hasId = typeof typed.id === "number";
|
|
778
|
+
const hasMethod = typeof typed.method === "string";
|
|
779
|
+
if (hasId && hasMethod) {
|
|
780
|
+
handlers.onServerRequest({
|
|
781
|
+
id: typed.id,
|
|
782
|
+
method: typed.method,
|
|
783
|
+
params: typed.params
|
|
784
|
+
});
|
|
785
|
+
return;
|
|
786
|
+
}
|
|
787
|
+
if (hasId && !hasMethod) {
|
|
788
|
+
const entry = pending.get(typed.id);
|
|
789
|
+
if (entry === void 0) return;
|
|
790
|
+
pending.delete(typed.id);
|
|
791
|
+
if (typed.error !== void 0 && typed.error !== null) {
|
|
792
|
+
entry.reject(
|
|
793
|
+
new Error(`codex app-server ${entry.method} failed: ${jsonRpcErrorMessage(typed.error)}`)
|
|
794
|
+
);
|
|
795
|
+
return;
|
|
796
|
+
}
|
|
797
|
+
entry.resolve(typed.result);
|
|
798
|
+
return;
|
|
799
|
+
}
|
|
800
|
+
if (hasMethod) {
|
|
801
|
+
handlers.onNotification({ method: typed.method, params: typed.params });
|
|
802
|
+
}
|
|
803
|
+
};
|
|
804
|
+
const pumpStdout = (async () => {
|
|
805
|
+
let buffer = "";
|
|
806
|
+
for await (const chunk of handle.stdout) {
|
|
807
|
+
buffer += decoder.decode(chunk, { stream: true });
|
|
808
|
+
let index = buffer.indexOf("\n");
|
|
809
|
+
while (index >= 0) {
|
|
810
|
+
dispatch(buffer.slice(0, index));
|
|
811
|
+
buffer = buffer.slice(index + 1);
|
|
812
|
+
index = buffer.indexOf("\n");
|
|
813
|
+
}
|
|
814
|
+
}
|
|
815
|
+
if (buffer.length > 0) dispatch(buffer);
|
|
816
|
+
})().catch(() => void 0);
|
|
817
|
+
const pumpStderr = (async () => {
|
|
818
|
+
const errorDecoder = new TextDecoder();
|
|
819
|
+
let buffer = "";
|
|
820
|
+
for await (const chunk of handle.stderr) {
|
|
821
|
+
buffer += errorDecoder.decode(chunk, { stream: true });
|
|
822
|
+
stderrTail = `${stderrTail}${buffer}`.slice(-STDERR_TAIL_LIMIT);
|
|
823
|
+
let index = buffer.indexOf("\n");
|
|
824
|
+
while (index >= 0) {
|
|
825
|
+
handlers.onStderrLine(buffer.slice(0, index));
|
|
826
|
+
buffer = buffer.slice(index + 1);
|
|
827
|
+
index = buffer.indexOf("\n");
|
|
828
|
+
}
|
|
829
|
+
}
|
|
830
|
+
if (buffer.length > 0) handlers.onStderrLine(buffer);
|
|
831
|
+
})().catch(() => void 0);
|
|
832
|
+
void handle.exited.then((code) => {
|
|
833
|
+
failAll(
|
|
834
|
+
`the process exited with code ${String(code)}${stderrTail.length > 0 ? `: ${stderrTail.trim()}` : ""}`
|
|
835
|
+
);
|
|
836
|
+
handlers.onExit?.(code);
|
|
837
|
+
}).catch(() => {
|
|
838
|
+
failAll("the process ended unexpectedly");
|
|
839
|
+
handlers.onExit?.(null);
|
|
840
|
+
});
|
|
841
|
+
const writeLine = async (payload, jsonRpcId) => {
|
|
842
|
+
try {
|
|
843
|
+
await handle.writeStdin(`${JSON.stringify(payload)}
|
|
844
|
+
`);
|
|
845
|
+
if (jsonRpcId !== void 0) handlers.onRequestSettled?.(jsonRpcId, "written");
|
|
846
|
+
} catch (error) {
|
|
847
|
+
if (jsonRpcId !== void 0) handlers.onRequestSettled?.(jsonRpcId, "write-failed");
|
|
848
|
+
throw error;
|
|
849
|
+
}
|
|
850
|
+
};
|
|
851
|
+
const request = async (method, params, timeoutMs) => {
|
|
852
|
+
if (exitReason !== null) throw new Error(`codex app-server ${method} failed: ${exitReason}`);
|
|
853
|
+
const id = nextId;
|
|
854
|
+
nextId += 1;
|
|
855
|
+
const settled = new Promise((resolve2, reject) => {
|
|
856
|
+
pending.set(id, { resolve: resolve2, reject, method });
|
|
857
|
+
});
|
|
858
|
+
await writeLine({ id, method, ...params === void 0 ? {} : { params } });
|
|
859
|
+
const limit = timeoutMs ?? DEFAULT_REQUEST_TIMEOUT_MS;
|
|
860
|
+
if (limit <= 0) return await settled;
|
|
861
|
+
let timer;
|
|
862
|
+
try {
|
|
863
|
+
return await Promise.race([
|
|
864
|
+
settled,
|
|
865
|
+
new Promise((_, reject) => {
|
|
866
|
+
timer = setTimeout(() => {
|
|
867
|
+
pending.delete(id);
|
|
868
|
+
reject(new Error(`codex app-server ${method} timed out after ${String(limit)}ms`));
|
|
869
|
+
}, limit);
|
|
870
|
+
})
|
|
871
|
+
]);
|
|
872
|
+
} finally {
|
|
873
|
+
if (timer !== void 0) clearTimeout(timer);
|
|
874
|
+
}
|
|
875
|
+
};
|
|
876
|
+
const notify = async (method, params) => {
|
|
877
|
+
await writeLine({ method, ...params === void 0 ? {} : { params } });
|
|
878
|
+
};
|
|
879
|
+
const respond = async (id, result) => {
|
|
880
|
+
await writeLine({ id, result }, id);
|
|
881
|
+
};
|
|
882
|
+
const respondError = async (id, code, message) => {
|
|
883
|
+
await writeLine({ id, error: { code, message } }, id);
|
|
884
|
+
};
|
|
885
|
+
const close = async () => {
|
|
886
|
+
if (closed) return;
|
|
887
|
+
closed = true;
|
|
888
|
+
try {
|
|
889
|
+
await handle.closeStdin();
|
|
890
|
+
} catch {
|
|
891
|
+
}
|
|
892
|
+
try {
|
|
893
|
+
await handle.kill("SIGTERM");
|
|
894
|
+
} catch {
|
|
895
|
+
}
|
|
896
|
+
failAll("the client was closed");
|
|
897
|
+
await Promise.all([pumpStdout, pumpStderr]);
|
|
898
|
+
};
|
|
899
|
+
return { request, notify, respond, respondError, close, pid: handle.pid };
|
|
900
|
+
}
|
|
901
|
+
|
|
902
|
+
// src/approvalGrant.ts
|
|
903
|
+
function codexApprovalGrantKey(kind, identity) {
|
|
904
|
+
if (kind === "mcp-elicitation") return `codex:mcp:${identity || "unknown"}`;
|
|
905
|
+
return `codex:${kind}`;
|
|
906
|
+
}
|
|
907
|
+
function createApprovalGrantStore() {
|
|
908
|
+
const grants = /* @__PURE__ */ new Map();
|
|
909
|
+
return {
|
|
910
|
+
remember: (key, decision) => {
|
|
911
|
+
grants.set(key, decision);
|
|
912
|
+
},
|
|
913
|
+
lookup: (key) => grants.get(key),
|
|
914
|
+
clear: () => {
|
|
915
|
+
grants.clear();
|
|
916
|
+
}
|
|
917
|
+
};
|
|
918
|
+
}
|
|
919
|
+
function wireDecisionFromPersist(kind, persist) {
|
|
920
|
+
if (kind === "command" || kind === "file-change") return "acceptForSession";
|
|
921
|
+
return persist;
|
|
922
|
+
}
|
|
923
|
+
|
|
924
|
+
// src/declineReasonRelay.ts
|
|
925
|
+
function createDeclineReasonRelay() {
|
|
926
|
+
const queued = /* @__PURE__ */ new Map();
|
|
927
|
+
return {
|
|
928
|
+
queue: (threadId, reason) => {
|
|
929
|
+
const trimmed = reason.trim();
|
|
930
|
+
if (trimmed.length === 0) return;
|
|
931
|
+
const list = queued.get(threadId) ?? [];
|
|
932
|
+
list.push(trimmed);
|
|
933
|
+
queued.set(threadId, list);
|
|
934
|
+
},
|
|
935
|
+
take: (threadId) => {
|
|
936
|
+
const list = queued.get(threadId);
|
|
937
|
+
if (list === void 0 || list.length === 0) return void 0;
|
|
938
|
+
queued.delete(threadId);
|
|
939
|
+
return list.map((reason) => `I declined that request. ${reason}`).join("\n");
|
|
940
|
+
},
|
|
941
|
+
clear: (threadId) => {
|
|
942
|
+
queued.delete(threadId);
|
|
943
|
+
}
|
|
944
|
+
};
|
|
945
|
+
}
|
|
946
|
+
|
|
947
|
+
// src/developerInstructions.ts
|
|
948
|
+
var YCODIUM_BROWSER_TOOL_INSTRUCTIONS = `
|
|
949
|
+
|
|
950
|
+
## Ycodium collaborative browser
|
|
951
|
+
|
|
952
|
+
You are running inside Ycodium. The \`ycodium\` MCP server is the product-native collaborative browser shared with the user. When it exposes \`preview_*\` tools, prefer those tools for browser navigation, inspection, interaction, screenshots, and recordings.
|
|
953
|
+
|
|
954
|
+
For browser work, first call \`preview_status\`. If no automation-capable preview is attached, call \`preview_open\` before concluding that the browser is unavailable. Then use \`preview_navigate\`, \`preview_snapshot\`, and the focused interaction tools. Prefer snapshot-provided locators over coordinates.
|
|
955
|
+
|
|
956
|
+
Do not switch to global browser skills, Chrome, Node REPL browser automation, standalone Playwright, or agent-browser merely because the preview is initially closed or a first call fails. Use an alternative browser system only when the Ycodium preview tools are absent, the user explicitly requests another browser, or \`preview_open\` returns an explicit unsupported/unavailable error. A failed Ycodium preview tool call should be inspected and retried with corrected arguments when the error is actionable.
|
|
957
|
+
`;
|
|
958
|
+
var YCODIUM_THREAD_TOOL_INSTRUCTIONS = `
|
|
959
|
+
|
|
960
|
+
## Ycodium child threads
|
|
961
|
+
|
|
962
|
+
The \`ycodium\` MCP server can start child threads on this environment. When \`start_thread\` is listed, use it to hand a self-contained piece of work to another thread \u2014 optionally on a different provider instance or model \u2014 instead of doing everything inline or spawning your own background processes.
|
|
963
|
+
|
|
964
|
+
Call \`list_providers\` and \`list_models\` first when you want a specific provider, model, or effort level; passing an effort a model does not offer is rejected rather than ignored. Pass \`isolate: true\` when the child must not share your working tree.
|
|
965
|
+
|
|
966
|
+
\`start_thread\` returns as soon as the child is running. You do not need to poll: a message arrives in this thread when the child finishes, fails, stops, or needs an approval, and it names the child thread id. Use \`get_thread_activity\` to read the full transcript behind a truncated summary, \`send_message\` to follow up, \`respond_to_permission\` to answer its approval requests, and \`stop_thread\` to end it. You can only steer child threads you started yourself.
|
|
967
|
+
`;
|
|
968
|
+
var threadToolInstructions = (threadToolsAvailable) => threadToolsAvailable ? YCODIUM_THREAD_TOOL_INSTRUCTIONS : "";
|
|
969
|
+
var browserToolInstructions = (browserToolsAvailable) => browserToolsAvailable ? YCODIUM_BROWSER_TOOL_INSTRUCTIONS : "";
|
|
970
|
+
var codexPlanModeDeveloperInstructions = (browserToolsAvailable, threadToolsAvailable = browserToolsAvailable) => `<collaboration_mode># Plan Mode (Conversational)
|
|
971
|
+
|
|
972
|
+
You work in 3 phases, and you should *chat your way* to a great plan before finalizing it. A great plan is very detailed-intent- and implementation-wise-so that it can be handed to another engineer or agent to be implemented right away. It must be **decision complete**, where the implementer does not need to make any decisions.
|
|
973
|
+
|
|
974
|
+
## Mode rules (strict)
|
|
975
|
+
|
|
976
|
+
You are in **Plan Mode** until a developer message explicitly ends it.
|
|
977
|
+
|
|
978
|
+
Plan Mode is not changed by user intent, tone, or imperative language. If a user asks for execution while still in Plan Mode, treat it as a request to **plan the execution**, not perform it.
|
|
979
|
+
|
|
980
|
+
## Plan Mode vs update_plan tool
|
|
981
|
+
|
|
982
|
+
Plan Mode is a collaboration mode that can involve requesting user input and eventually issuing a \`<proposed_plan>\` block.
|
|
983
|
+
|
|
984
|
+
Separately, \`update_plan\` is a checklist/progress/TODOs tool; it does not enter or exit Plan Mode. Do not confuse it with Plan mode or try to use it while in Plan mode. If you try to use \`update_plan\` in Plan mode, it will return an error.
|
|
985
|
+
|
|
986
|
+
## Execution vs. mutation in Plan Mode
|
|
987
|
+
|
|
988
|
+
You may explore and execute **non-mutating** actions that improve the plan. You must not perform **mutating** actions.
|
|
989
|
+
|
|
990
|
+
### Allowed (non-mutating, plan-improving)
|
|
991
|
+
|
|
992
|
+
Actions that gather truth, reduce ambiguity, or validate feasibility without changing repo-tracked state. Examples:
|
|
993
|
+
|
|
994
|
+
* Reading or searching files, configs, schemas, types, manifests, and docs
|
|
995
|
+
* Static analysis, inspection, and repo exploration
|
|
996
|
+
* Dry-run style commands when they do not edit repo-tracked files
|
|
997
|
+
* Tests, builds, or checks that may write to caches or build artifacts (for example, \`target/\`, \`.cache/\`, or snapshots) so long as they do not edit repo-tracked files
|
|
998
|
+
|
|
999
|
+
### Not allowed (mutating, plan-executing)
|
|
1000
|
+
|
|
1001
|
+
Actions that implement the plan or change repo-tracked state. Examples:
|
|
1002
|
+
|
|
1003
|
+
* Editing or writing files
|
|
1004
|
+
* Running formatters or linters that rewrite files
|
|
1005
|
+
* Applying patches, migrations, or codegen that updates repo-tracked files
|
|
1006
|
+
* Side-effectful commands whose purpose is to carry out the plan rather than refine it
|
|
1007
|
+
|
|
1008
|
+
When in doubt: if the action would reasonably be described as "doing the work" rather than "planning the work," do not do it.
|
|
1009
|
+
|
|
1010
|
+
## PHASE 1 - Ground in the environment (explore first, ask second)
|
|
1011
|
+
|
|
1012
|
+
Begin by grounding yourself in the actual environment. Eliminate unknowns in the prompt by discovering facts, not by asking the user. Resolve all questions that can be answered through exploration or inspection. Identify missing or ambiguous details only if they cannot be derived from the environment. Silent exploration between turns is allowed and encouraged.
|
|
1013
|
+
|
|
1014
|
+
Before asking the user any question, perform at least one targeted non-mutating exploration pass (for example: search relevant files, inspect likely entrypoints/configs, confirm current implementation shape), unless no local environment/repo is available.
|
|
1015
|
+
|
|
1016
|
+
Exception: you may ask clarifying questions about the user's prompt before exploring, ONLY if there are obvious ambiguities or contradictions in the prompt itself. However, if ambiguity might be resolved by exploring, always prefer exploring first.
|
|
1017
|
+
|
|
1018
|
+
Do not ask questions that can be answered from the repo or system (for example, "where is this struct?" or "which UI component should we use?" when exploration can make it clear). Only ask once you have exhausted reasonable non-mutating exploration.
|
|
1019
|
+
|
|
1020
|
+
## PHASE 2 - Intent chat (what they actually want)
|
|
1021
|
+
|
|
1022
|
+
* Keep asking until you can clearly state: goal + success criteria, audience, in/out of scope, constraints, current state, and the key preferences/tradeoffs.
|
|
1023
|
+
* Bias toward questions over guessing: if any high-impact ambiguity remains, do NOT plan yet-ask.
|
|
1024
|
+
|
|
1025
|
+
## PHASE 3 - Implementation chat (what/how we'll build)
|
|
1026
|
+
|
|
1027
|
+
* Once intent is stable, keep asking until the spec is decision complete: approach, interfaces (APIs/schemas/I/O), data flow, edge cases/failure modes, testing + acceptance criteria, rollout/monitoring, and any migrations/compat constraints.
|
|
1028
|
+
|
|
1029
|
+
## Asking questions
|
|
1030
|
+
|
|
1031
|
+
Critical rules:
|
|
1032
|
+
|
|
1033
|
+
* Strongly prefer using the \`request_user_input\` tool to ask any questions.
|
|
1034
|
+
* Offer only meaningful multiple-choice options; don't include filler choices that are obviously wrong or irrelevant.
|
|
1035
|
+
* In rare cases where an unavoidable, important question can't be expressed with reasonable multiple-choice options (due to extreme ambiguity), you may ask it directly without the tool.
|
|
1036
|
+
|
|
1037
|
+
You SHOULD ask many questions, but each question must:
|
|
1038
|
+
|
|
1039
|
+
* materially change the spec/plan, OR
|
|
1040
|
+
* confirm/lock an assumption, OR
|
|
1041
|
+
* choose between meaningful tradeoffs.
|
|
1042
|
+
* not be answerable by non-mutating commands.
|
|
1043
|
+
|
|
1044
|
+
Use the \`request_user_input\` tool only for decisions that materially change the plan, for confirming important assumptions, or for information that cannot be discovered via non-mutating exploration.
|
|
1045
|
+
|
|
1046
|
+
## Two kinds of unknowns (treat differently)
|
|
1047
|
+
|
|
1048
|
+
1. **Discoverable facts** (repo/system truth): explore first.
|
|
1049
|
+
|
|
1050
|
+
* Before asking, run targeted searches and check likely sources of truth (configs/manifests/entrypoints/schemas/types/constants).
|
|
1051
|
+
* Ask only if: multiple plausible candidates; nothing found but you need a missing identifier/context; or ambiguity is actually product intent.
|
|
1052
|
+
* If asking, present concrete candidates (paths/service names) + recommend one.
|
|
1053
|
+
* Never ask questions you can answer from your environment (e.g., "where is this struct").
|
|
1054
|
+
|
|
1055
|
+
2. **Preferences/tradeoffs** (not discoverable): ask early.
|
|
1056
|
+
|
|
1057
|
+
* These are intent or implementation preferences that cannot be derived from exploration.
|
|
1058
|
+
* Provide 2-4 mutually exclusive options + a recommended default.
|
|
1059
|
+
* If unanswered, proceed with the recommended option and record it as an assumption in the final plan.
|
|
1060
|
+
|
|
1061
|
+
## Finalization rule
|
|
1062
|
+
|
|
1063
|
+
Only output the final plan when it is decision complete and leaves no decisions to the implementer.
|
|
1064
|
+
|
|
1065
|
+
When you present the official plan, wrap it in a \`<proposed_plan>\` block so the client can render it specially:
|
|
1066
|
+
|
|
1067
|
+
1) The opening tag must be on its own line.
|
|
1068
|
+
2) Start the plan content on the next line (no text on the same line as the tag).
|
|
1069
|
+
3) The closing tag must be on its own line.
|
|
1070
|
+
4) Use Markdown inside the block.
|
|
1071
|
+
5) Keep the tags exactly as \`<proposed_plan>\` and \`</proposed_plan>\` (do not translate or rename them), even if the plan content is in another language.
|
|
1072
|
+
|
|
1073
|
+
Example:
|
|
1074
|
+
|
|
1075
|
+
<proposed_plan>
|
|
1076
|
+
plan content
|
|
1077
|
+
</proposed_plan>
|
|
1078
|
+
|
|
1079
|
+
plan content should be human and agent digestible. The final plan must be plan-only, concise by default, and include:
|
|
1080
|
+
|
|
1081
|
+
* A clear title
|
|
1082
|
+
* A brief summary section
|
|
1083
|
+
* Important changes or additions to public APIs/interfaces/types
|
|
1084
|
+
* Test cases and scenarios
|
|
1085
|
+
* Explicit assumptions and defaults chosen where needed
|
|
1086
|
+
|
|
1087
|
+
When possible, prefer a compact structure with 3-5 short sections, usually: Summary, Key Changes or Implementation Changes, Test Plan, and Assumptions. Do not include a separate Scope section unless scope boundaries are genuinely important to avoid mistakes.
|
|
1088
|
+
|
|
1089
|
+
Prefer grouped implementation bullets by subsystem or behavior over file-by-file inventories. Mention files only when needed to disambiguate a non-obvious change, and avoid naming more than 3 paths unless extra specificity is necessary to prevent mistakes. Prefer behavior-level descriptions over symbol-by-symbol removal lists. For v1 feature-addition plans, do not invent detailed schema, validation, precedence, fallback, or wire-shape policy unless the request establishes it or it is needed to prevent a concrete implementation mistake; prefer the intended capability and minimum interface/behavior changes.
|
|
1090
|
+
|
|
1091
|
+
Keep bullets short and avoid explanatory sub-bullets unless they are needed to prevent ambiguity. Prefer the minimum detail needed for implementation safety, not exhaustive coverage. Within each section, compress related changes into a few high-signal bullets and omit branch-by-branch logic, repeated invariants, and long lists of unaffected behavior unless they are necessary to prevent a likely implementation mistake. Avoid repeated repo facts and irrelevant edge-case or rollout detail. For straightforward refactors, keep the plan to a compact summary, key edits, tests, and assumptions. If the user asks for more detail, then expand.
|
|
1092
|
+
|
|
1093
|
+
Do not ask "should I proceed?" in the final output. The user can easily switch out of Plan mode and request implementation if you have included a \`<proposed_plan>\` block in your response. Alternatively, they can decide to stay in Plan mode and continue refining the plan.
|
|
1094
|
+
|
|
1095
|
+
Only produce at most one \`<proposed_plan>\` block per turn, and only when you are presenting a complete spec.
|
|
1096
|
+
|
|
1097
|
+
If the user stays in Plan mode and asks for revisions after a prior \`<proposed_plan>\`, any new \`<proposed_plan>\` must be a complete replacement. If the user indicates that the prior plan is not acceptable but does not provide enough information to produce a complete replacement, address the concern and continue planning without producing a \`<proposed_plan>\` block. If the follow-up neither requires changes nor calls the plan into question (e.g. clarifying question), answer it before the block, then reproduce the prior \`<proposed_plan>\` unchanged.
|
|
1098
|
+
${browserToolInstructions(browserToolsAvailable)}${threadToolInstructions(threadToolsAvailable)}
|
|
1099
|
+
</collaboration_mode>`;
|
|
1100
|
+
var codexDefaultModeDeveloperInstructions = (browserToolsAvailable, threadToolsAvailable = browserToolsAvailable) => `<collaboration_mode># Collaboration Mode: Default
|
|
1101
|
+
|
|
1102
|
+
You are now in Default mode. Any previous instructions for other modes (e.g. Plan mode) are no longer active.
|
|
1103
|
+
|
|
1104
|
+
Your active mode changes only when new developer instructions with a different \`<collaboration_mode>...</collaboration_mode>\` change it; user requests or tool descriptions do not change mode by themselves. Known mode names are Default and Plan.
|
|
1105
|
+
|
|
1106
|
+
## request_user_input availability
|
|
1107
|
+
|
|
1108
|
+
Use the \`request_user_input\` tool only when it is listed in the available tools for this turn.
|
|
1109
|
+
|
|
1110
|
+
In Default mode, strongly prefer making reasonable assumptions and executing the user's request rather than stopping to ask questions. If you absolutely must ask a question because the answer cannot be discovered from local context and a reasonable assumption would be risky, ask the user directly with a concise plain-text question. Never write a multiple choice question as a textual assistant message.
|
|
1111
|
+
${browserToolInstructions(browserToolsAvailable)}${threadToolInstructions(threadToolsAvailable)}
|
|
1112
|
+
</collaboration_mode>`;
|
|
1113
|
+
function toSingleLine(value) {
|
|
1114
|
+
return value.replaceAll(/\s+/g, " ").trim();
|
|
1115
|
+
}
|
|
1116
|
+
function buildCodexDeveloperInstructions(interactionMode, runtime, browserToolsAvailable = true, threadToolsAvailable = browserToolsAvailable) {
|
|
1117
|
+
const base = interactionMode === "plan" ? codexPlanModeDeveloperInstructions(browserToolsAvailable, threadToolsAvailable) : codexDefaultModeDeveloperInstructions(browserToolsAvailable, threadToolsAvailable);
|
|
1118
|
+
return `${base}
|
|
1119
|
+
|
|
1120
|
+
<runtime_info>In case you're asked: you are running in Ycodium through the Codex harness, as ${toSingleLine(runtime.model)} with ${toSingleLine(runtime.reasoningEffort)} reasoning effort. No need to mention this otherwise.</runtime_info>`;
|
|
1121
|
+
}
|
|
1122
|
+
|
|
1123
|
+
// src/imageMime.ts
|
|
1124
|
+
var IMAGE_EXTENSION_BY_MIME_TYPE = {
|
|
1125
|
+
"image/avif": ".avif",
|
|
1126
|
+
"image/bmp": ".bmp",
|
|
1127
|
+
"image/gif": ".gif",
|
|
1128
|
+
"image/heic": ".heic",
|
|
1129
|
+
"image/heif": ".heif",
|
|
1130
|
+
"image/jpeg": ".jpg",
|
|
1131
|
+
"image/jpg": ".jpg",
|
|
1132
|
+
"image/png": ".png",
|
|
1133
|
+
"image/svg+xml": ".svg",
|
|
1134
|
+
"image/tiff": ".tiff",
|
|
1135
|
+
"image/webp": ".webp"
|
|
1136
|
+
};
|
|
1137
|
+
var SAFE_IMAGE_FILE_EXTENSIONS = /* @__PURE__ */ new Set([
|
|
1138
|
+
".avif",
|
|
1139
|
+
".bmp",
|
|
1140
|
+
".gif",
|
|
1141
|
+
".heic",
|
|
1142
|
+
".heif",
|
|
1143
|
+
".ico",
|
|
1144
|
+
".jpeg",
|
|
1145
|
+
".jpg",
|
|
1146
|
+
".png",
|
|
1147
|
+
".svg",
|
|
1148
|
+
".tiff",
|
|
1149
|
+
".webp"
|
|
1150
|
+
]);
|
|
1151
|
+
function isBase64Char(code) {
|
|
1152
|
+
return code >= 97 && code <= 122 || code >= 65 && code <= 90 || code >= 48 && code <= 57 || code === 43 || code === 47 || code === 61;
|
|
1153
|
+
}
|
|
1154
|
+
function isBase64Whitespace(code) {
|
|
1155
|
+
return code === 13 || code === 10 || code === 32;
|
|
1156
|
+
}
|
|
1157
|
+
function parseBase64DataUrl(dataUrl) {
|
|
1158
|
+
const trimmed = dataUrl.trim();
|
|
1159
|
+
if (trimmed.slice(0, 5).toLowerCase() !== "data:") return null;
|
|
1160
|
+
const commaIndex = trimmed.indexOf(",");
|
|
1161
|
+
if (commaIndex === -1) return null;
|
|
1162
|
+
const header = trimmed.slice(5, commaIndex);
|
|
1163
|
+
if (header.length === 0) return null;
|
|
1164
|
+
const headerParts = [];
|
|
1165
|
+
for (const part of header.split(";")) {
|
|
1166
|
+
const partTrimmed = part.trim();
|
|
1167
|
+
if (partTrimmed.length > 0) headerParts.push(partTrimmed);
|
|
1168
|
+
}
|
|
1169
|
+
if (headerParts.length < 2) return null;
|
|
1170
|
+
const trailingToken = headerParts.at(-1)?.toLowerCase();
|
|
1171
|
+
if (trailingToken !== "base64") return null;
|
|
1172
|
+
const mimeType = headerParts[0]?.toLowerCase();
|
|
1173
|
+
if (!mimeType) return null;
|
|
1174
|
+
const payload = trimmed.slice(commaIndex + 1);
|
|
1175
|
+
const runs = [];
|
|
1176
|
+
let runStart = -1;
|
|
1177
|
+
for (let index = 0; index < payload.length; index += 1) {
|
|
1178
|
+
const code = payload.charCodeAt(index);
|
|
1179
|
+
if (isBase64Char(code)) {
|
|
1180
|
+
if (runStart === -1) runStart = index;
|
|
1181
|
+
continue;
|
|
1182
|
+
}
|
|
1183
|
+
if (!isBase64Whitespace(code)) return null;
|
|
1184
|
+
if (runStart !== -1) {
|
|
1185
|
+
runs.push(payload.slice(runStart, index));
|
|
1186
|
+
runStart = -1;
|
|
1187
|
+
}
|
|
1188
|
+
}
|
|
1189
|
+
if (runStart !== -1) runs.push(payload.slice(runStart));
|
|
1190
|
+
const base64 = runs.length === 1 ? runs[0] : runs.join("");
|
|
1191
|
+
if (base64.length === 0 || base64.length % 4 !== 0) return null;
|
|
1192
|
+
const firstPad = base64.indexOf("=");
|
|
1193
|
+
if (firstPad !== -1) {
|
|
1194
|
+
if (base64.length - firstPad > 2) return null;
|
|
1195
|
+
for (let index = firstPad; index < base64.length; index += 1) {
|
|
1196
|
+
if (base64.charCodeAt(index) !== 61) return null;
|
|
1197
|
+
}
|
|
1198
|
+
}
|
|
1199
|
+
return { mimeType, base64 };
|
|
1200
|
+
}
|
|
1201
|
+
function inferImageExtension(input) {
|
|
1202
|
+
const key = input.mimeType.toLowerCase();
|
|
1203
|
+
const fromMime = Object.hasOwn(IMAGE_EXTENSION_BY_MIME_TYPE, key) ? IMAGE_EXTENSION_BY_MIME_TYPE[key] : void 0;
|
|
1204
|
+
if (fromMime) return fromMime;
|
|
1205
|
+
const fileName = input.fileName?.trim() ?? "";
|
|
1206
|
+
const extensionMatch = /\.([a-z0-9]{1,8})$/i.exec(fileName);
|
|
1207
|
+
const fileNameExtension = extensionMatch ? `.${extensionMatch[1].toLowerCase()}` : "";
|
|
1208
|
+
if (SAFE_IMAGE_FILE_EXTENSIONS.has(fileNameExtension)) return fileNameExtension;
|
|
1209
|
+
return ".bin";
|
|
1210
|
+
}
|
|
1211
|
+
|
|
1212
|
+
// src/events.ts
|
|
1213
|
+
var CODEX_PROVIDER = "codex";
|
|
1214
|
+
var eventSeq = 0;
|
|
1215
|
+
function nextEventId() {
|
|
1216
|
+
eventSeq += 1;
|
|
1217
|
+
return `codex-evt-${eventSeq}`;
|
|
1218
|
+
}
|
|
1219
|
+
function trimText(value) {
|
|
1220
|
+
if (typeof value !== "string") return void 0;
|
|
1221
|
+
const trimmed = value.trim();
|
|
1222
|
+
return trimmed.length > 0 ? trimmed : void 0;
|
|
1223
|
+
}
|
|
1224
|
+
function nowIso2() {
|
|
1225
|
+
return (/* @__PURE__ */ new Date()).toISOString();
|
|
1226
|
+
}
|
|
1227
|
+
function normalizeItemType(value) {
|
|
1228
|
+
if (!value) return "unknown";
|
|
1229
|
+
return value.replace(/([a-z])([A-Z])/g, "$1 $2").replace(/[_/.-]+/g, " ").toLowerCase().trim();
|
|
1230
|
+
}
|
|
1231
|
+
function toCanonicalItemType(raw) {
|
|
1232
|
+
const type = normalizeItemType(raw);
|
|
1233
|
+
if (type.includes("user") && type.includes("message")) return "user";
|
|
1234
|
+
if (type.includes("agent") && type.includes("message")) return "assistant";
|
|
1235
|
+
if (type.includes("assistant")) return "assistant";
|
|
1236
|
+
if (type.includes("reason") || type.includes("thought")) return "reasoning";
|
|
1237
|
+
if (type.includes("plan") || type.includes("todo")) return "plan";
|
|
1238
|
+
if (type.includes("command")) return "command";
|
|
1239
|
+
if (type.includes("file change") || type.includes("patch") || type.includes("edit")) {
|
|
1240
|
+
return "fileChange";
|
|
1241
|
+
}
|
|
1242
|
+
if (type.includes("mcp")) return "mcp";
|
|
1243
|
+
if (type.includes("dynamic tool")) return "dynamicTool";
|
|
1244
|
+
if (type.includes("collab")) return "collab";
|
|
1245
|
+
if (type.includes("web search")) return "webSearch";
|
|
1246
|
+
if (type.includes("image")) return "image";
|
|
1247
|
+
if (type.includes("review entered")) return "reviewEntered";
|
|
1248
|
+
if (type.includes("review exited")) return "reviewExited";
|
|
1249
|
+
if (type.includes("compact")) return "compact";
|
|
1250
|
+
if (type.includes("error")) return "error";
|
|
1251
|
+
return "unknown";
|
|
1252
|
+
}
|
|
1253
|
+
function itemIdOf(params) {
|
|
1254
|
+
if (!isRecord3(params)) return void 0;
|
|
1255
|
+
const item = isRecord3(params["item"]) ? params["item"] : params;
|
|
1256
|
+
return asString(item["id"]) ?? asString(params["itemId"]) ?? void 0;
|
|
1257
|
+
}
|
|
1258
|
+
function turnIdOf(params) {
|
|
1259
|
+
if (!isRecord3(params)) return void 0;
|
|
1260
|
+
return asString(params["turnId"]) ?? (isRecord3(params["turn"]) ? asString(params["turn"]["id"]) : null) ?? void 0;
|
|
1261
|
+
}
|
|
1262
|
+
function itemTitle(params) {
|
|
1263
|
+
if (!isRecord3(params)) return void 0;
|
|
1264
|
+
const item = isRecord3(params["item"]) ? params["item"] : params;
|
|
1265
|
+
return trimText(item["title"]) ?? trimText(item["name"]) ?? trimText(item["command"]) ?? trimText(item["summary"]) ?? void 0;
|
|
1266
|
+
}
|
|
1267
|
+
function itemDetail(params) {
|
|
1268
|
+
if (!isRecord3(params)) return void 0;
|
|
1269
|
+
const item = isRecord3(params["item"]) ? params["item"] : params;
|
|
1270
|
+
const query = isRecord3(item["action"]) ? item["action"]["queries"] : item["query"];
|
|
1271
|
+
if (Array.isArray(query)) {
|
|
1272
|
+
const joined = query.filter((entry) => typeof entry === "string").join(", ");
|
|
1273
|
+
if (joined.length > 0) return joined;
|
|
1274
|
+
}
|
|
1275
|
+
return trimText(item["text"]) ?? trimText(item["summary"]) ?? trimText(item["path"]) ?? trimText(item["savedPath"]) ?? trimText(item["prompt"]) ?? (typeof query === "string" ? query : void 0);
|
|
1276
|
+
}
|
|
1277
|
+
function toTurnStatus(status) {
|
|
1278
|
+
switch (status) {
|
|
1279
|
+
case "failed":
|
|
1280
|
+
return "failed";
|
|
1281
|
+
case "cancelled":
|
|
1282
|
+
case "canceled":
|
|
1283
|
+
return "cancelled";
|
|
1284
|
+
case "interrupted":
|
|
1285
|
+
return "interrupted";
|
|
1286
|
+
default:
|
|
1287
|
+
return "completed";
|
|
1288
|
+
}
|
|
1289
|
+
}
|
|
1290
|
+
function toThreadState(status) {
|
|
1291
|
+
if (status === "idle") return "idle";
|
|
1292
|
+
if (status === "systemError" || status === "error") return "error";
|
|
1293
|
+
return "active";
|
|
1294
|
+
}
|
|
1295
|
+
function contentStreamKindFromMethod(method) {
|
|
1296
|
+
if (method.includes("agentMessage")) return "assistant";
|
|
1297
|
+
if (method.includes("reasoning")) return "reasoning";
|
|
1298
|
+
if (method.includes("plan")) return "plan";
|
|
1299
|
+
return void 0;
|
|
1300
|
+
}
|
|
1301
|
+
function normalizeCodexTokenUsage(params) {
|
|
1302
|
+
if (!isRecord3(params)) return void 0;
|
|
1303
|
+
const usage = isRecord3(params["usage"]) ? params["usage"] : params;
|
|
1304
|
+
const last = isRecord3(usage["last"]) ? usage["last"] : usage;
|
|
1305
|
+
const total = isRecord3(usage["total"]) ? usage["total"] : void 0;
|
|
1306
|
+
const usedTokens = asNumber(last["totalTokens"]) ?? asNumber(last["total_tokens"]);
|
|
1307
|
+
if (usedTokens === null || usedTokens <= 0) return void 0;
|
|
1308
|
+
const totalProcessed = total ? asNumber(total["totalTokens"]) ?? asNumber(total["total_tokens"]) : null;
|
|
1309
|
+
const payload = {
|
|
1310
|
+
usedTokens,
|
|
1311
|
+
compactsAutomatically: true
|
|
1312
|
+
};
|
|
1313
|
+
if (totalProcessed !== null && totalProcessed > usedTokens) {
|
|
1314
|
+
payload["totalProcessedTokens"] = totalProcessed;
|
|
1315
|
+
}
|
|
1316
|
+
const maxTokens = asNumber(params["modelContextWindow"]) ?? asNumber(usage["modelContextWindow"]);
|
|
1317
|
+
if (maxTokens !== null && maxTokens > 0) payload["maxTokens"] = maxTokens;
|
|
1318
|
+
const lastInput = asNumber(last["inputTokens"]) ?? asNumber(last["input_tokens"]);
|
|
1319
|
+
const lastOutput = asNumber(last["outputTokens"]) ?? asNumber(last["output_tokens"]);
|
|
1320
|
+
if (lastInput !== null) payload["lastInputTokens"] = lastInput;
|
|
1321
|
+
if (lastOutput !== null) payload["lastOutputTokens"] = lastOutput;
|
|
1322
|
+
return payload;
|
|
1323
|
+
}
|
|
1324
|
+
function turnCompletedControlFlow(params) {
|
|
1325
|
+
const turn = isRecord3(params) ? isRecord3(params["turn"]) ? params["turn"] : params : {};
|
|
1326
|
+
const status = toTurnStatus(asString(turn["status"]) ?? void 0);
|
|
1327
|
+
const error = isRecord3(turn["error"]) ? trimText(turn["error"]["message"]) : void 0;
|
|
1328
|
+
return error ? { status, errorMessage: error } : { status };
|
|
1329
|
+
}
|
|
1330
|
+
function toApplyPatchFileChanges(params) {
|
|
1331
|
+
if (!isRecord3(params)) return [];
|
|
1332
|
+
const item = isRecord3(params["item"]) ? params["item"] : params;
|
|
1333
|
+
const changes = item["changes"] ?? item["fileChanges"] ?? params["fileChanges"];
|
|
1334
|
+
if (!isRecord3(changes) && !Array.isArray(changes)) return [];
|
|
1335
|
+
const entries = Array.isArray(changes) ? changes : Object.entries(changes).map(([path, change]) => ({
|
|
1336
|
+
path,
|
|
1337
|
+
...isRecord3(change) ? change : {}
|
|
1338
|
+
}));
|
|
1339
|
+
const out = [];
|
|
1340
|
+
for (const entry of entries) {
|
|
1341
|
+
if (!isRecord3(entry)) continue;
|
|
1342
|
+
const path = asString(entry["path"]) ?? asString(entry["file"]);
|
|
1343
|
+
if (!path) continue;
|
|
1344
|
+
const kind = (asString(entry["type"]) ?? asString(entry["kind"]) ?? "update").toLowerCase();
|
|
1345
|
+
const movePath = asString(entry["move_path"]) ?? asString(entry["movePath"]);
|
|
1346
|
+
if (kind === "add") out.push({ path, kind: "add" });
|
|
1347
|
+
else if (kind === "delete") out.push({ path, kind: "delete" });
|
|
1348
|
+
else if (movePath) out.push({ path, kind: "rename", to: movePath });
|
|
1349
|
+
else out.push({ path, kind: "modify" });
|
|
1350
|
+
}
|
|
1351
|
+
return out;
|
|
1352
|
+
}
|
|
1353
|
+
function toUserInputQuestions(params) {
|
|
1354
|
+
if (!isRecord3(params)) return void 0;
|
|
1355
|
+
const questions = Array.isArray(params["questions"]) ? params["questions"] : [];
|
|
1356
|
+
const mapped = [];
|
|
1357
|
+
for (const question of questions) {
|
|
1358
|
+
if (!isRecord3(question)) return void 0;
|
|
1359
|
+
const id = asString(question["id"]);
|
|
1360
|
+
const header = asString(question["header"]);
|
|
1361
|
+
const text = asString(question["question"]);
|
|
1362
|
+
const options = Array.isArray(question["options"]) ? question["options"] : [];
|
|
1363
|
+
if (!id || !header || !text || options.length === 0) return void 0;
|
|
1364
|
+
mapped.push({
|
|
1365
|
+
id,
|
|
1366
|
+
header,
|
|
1367
|
+
question: text,
|
|
1368
|
+
options,
|
|
1369
|
+
multiSelect: false
|
|
1370
|
+
});
|
|
1371
|
+
}
|
|
1372
|
+
return mapped.length > 0 ? mapped : void 0;
|
|
1373
|
+
}
|
|
1374
|
+
function toCodexUserInputAnswers(answers) {
|
|
1375
|
+
if (typeof answers === "string") return [answers];
|
|
1376
|
+
if (Array.isArray(answers) && answers.every((entry) => typeof entry === "string")) {
|
|
1377
|
+
return answers;
|
|
1378
|
+
}
|
|
1379
|
+
if (isRecord3(answers) && Array.isArray(answers["answers"])) {
|
|
1380
|
+
const values = answers["answers"];
|
|
1381
|
+
if (values.every((entry) => typeof entry === "string")) return values;
|
|
1382
|
+
}
|
|
1383
|
+
return void 0;
|
|
1384
|
+
}
|
|
1385
|
+
function pathLeaf(value) {
|
|
1386
|
+
if (!value) return void 0;
|
|
1387
|
+
const parts = value.split("/").filter((part) => part.length > 0);
|
|
1388
|
+
return parts.at(-1);
|
|
1389
|
+
}
|
|
1390
|
+
function eventRaw(source, method, payload) {
|
|
1391
|
+
return { source, method, payload };
|
|
1392
|
+
}
|
|
1393
|
+
function baseEvent(input) {
|
|
1394
|
+
return {
|
|
1395
|
+
eventId: nextEventId(),
|
|
1396
|
+
provider: CODEX_PROVIDER,
|
|
1397
|
+
threadId: input.threadId,
|
|
1398
|
+
createdAt: nowIso2(),
|
|
1399
|
+
type: input.type,
|
|
1400
|
+
payload: input.payload,
|
|
1401
|
+
...input.providerInstanceId ? { providerInstanceId: input.providerInstanceId } : {},
|
|
1402
|
+
...input.turnId ? { turnId: input.turnId } : {},
|
|
1403
|
+
...input.itemId ? { itemId: input.itemId } : {},
|
|
1404
|
+
...input.requestId ? { requestId: input.requestId } : {},
|
|
1405
|
+
...input.method ? {
|
|
1406
|
+
raw: eventRaw(
|
|
1407
|
+
input.source ?? "codex.app-server.notification",
|
|
1408
|
+
input.method,
|
|
1409
|
+
input.payload
|
|
1410
|
+
)
|
|
1411
|
+
} : {}
|
|
1412
|
+
};
|
|
1413
|
+
}
|
|
1414
|
+
function mapCollabAgentEvent(input) {
|
|
1415
|
+
if (!isRecord3(input.params)) return [];
|
|
1416
|
+
const agentThreadId = asString(input.params["agentThreadId"]) ?? asString(input.params["threadId"]);
|
|
1417
|
+
if (!agentThreadId) return [];
|
|
1418
|
+
const agentPath = asString(input.params["agentPath"]) ?? asString(input.params["path"]) ?? void 0;
|
|
1419
|
+
const nickname = asString(input.params["agentNickname"]) ?? asString(input.params["nickname"]);
|
|
1420
|
+
const role = asString(input.params["agentRole"]) ?? asString(input.params["role"]) ?? pathLeaf(agentPath) ?? "general-purpose";
|
|
1421
|
+
const title = nickname ?? pathLeaf(agentPath) ?? agentThreadId;
|
|
1422
|
+
const linkage = {
|
|
1423
|
+
role,
|
|
1424
|
+
title,
|
|
1425
|
+
...agentPath ? { agentPath } : {},
|
|
1426
|
+
timelineBypass: true
|
|
1427
|
+
};
|
|
1428
|
+
const threadId = input.rootThreadId;
|
|
1429
|
+
const method = input.method;
|
|
1430
|
+
if (method === "collabAgent/started" || method === "task.started") {
|
|
1431
|
+
return [
|
|
1432
|
+
baseEvent({
|
|
1433
|
+
type: "task.started",
|
|
1434
|
+
threadId,
|
|
1435
|
+
payload: { taskId: agentThreadId, ...linkage },
|
|
1436
|
+
method,
|
|
1437
|
+
providerInstanceId: input.providerInstanceId
|
|
1438
|
+
})
|
|
1439
|
+
];
|
|
1440
|
+
}
|
|
1441
|
+
if (method === "collabAgent/progress" || method === "task.progress") {
|
|
1442
|
+
return [
|
|
1443
|
+
baseEvent({
|
|
1444
|
+
type: "task.progress",
|
|
1445
|
+
threadId,
|
|
1446
|
+
payload: {
|
|
1447
|
+
taskId: agentThreadId,
|
|
1448
|
+
...linkage,
|
|
1449
|
+
detail: trimText(input.params["detail"]) ?? trimText(input.params["text"])
|
|
1450
|
+
},
|
|
1451
|
+
method,
|
|
1452
|
+
providerInstanceId: input.providerInstanceId
|
|
1453
|
+
})
|
|
1454
|
+
];
|
|
1455
|
+
}
|
|
1456
|
+
if (method === "collabAgent/completed" || method === "task.completed") {
|
|
1457
|
+
return [
|
|
1458
|
+
baseEvent({
|
|
1459
|
+
type: "task.completed",
|
|
1460
|
+
threadId,
|
|
1461
|
+
payload: { taskId: agentThreadId, ...linkage },
|
|
1462
|
+
method,
|
|
1463
|
+
providerInstanceId: input.providerInstanceId
|
|
1464
|
+
})
|
|
1465
|
+
];
|
|
1466
|
+
}
|
|
1467
|
+
if (method === "collabAgent/statusChanged") {
|
|
1468
|
+
return [
|
|
1469
|
+
baseEvent({
|
|
1470
|
+
type: "task.updated",
|
|
1471
|
+
threadId,
|
|
1472
|
+
payload: {
|
|
1473
|
+
taskId: agentThreadId,
|
|
1474
|
+
...linkage,
|
|
1475
|
+
status: asString(input.params["status"]) ?? "running"
|
|
1476
|
+
},
|
|
1477
|
+
method,
|
|
1478
|
+
providerInstanceId: input.providerInstanceId
|
|
1479
|
+
})
|
|
1480
|
+
];
|
|
1481
|
+
}
|
|
1482
|
+
if (method === "collabAgent/tokenUsage") {
|
|
1483
|
+
const usage = normalizeCodexTokenUsage(input.params);
|
|
1484
|
+
if (!usage) return [];
|
|
1485
|
+
return [
|
|
1486
|
+
baseEvent({
|
|
1487
|
+
type: "thread.token-usage.updated",
|
|
1488
|
+
threadId: agentThreadId,
|
|
1489
|
+
payload: usage,
|
|
1490
|
+
method,
|
|
1491
|
+
providerInstanceId: input.providerInstanceId
|
|
1492
|
+
})
|
|
1493
|
+
];
|
|
1494
|
+
}
|
|
1495
|
+
return [];
|
|
1496
|
+
}
|
|
1497
|
+
function describeCommandApproval(params) {
|
|
1498
|
+
const record = isRecord3(params) ? params : {};
|
|
1499
|
+
const commandValue = record["command"];
|
|
1500
|
+
const command = Array.isArray(commandValue) ? commandValue.filter((entry) => typeof entry === "string").join(" ") : asString(commandValue) ?? "";
|
|
1501
|
+
const cwd = asString(record["cwd"]);
|
|
1502
|
+
const reason = trimText(record["reason"]);
|
|
1503
|
+
const callId = asString(record["callId"]);
|
|
1504
|
+
return {
|
|
1505
|
+
command,
|
|
1506
|
+
...cwd ? { cwd } : {},
|
|
1507
|
+
...reason ? { reason } : {},
|
|
1508
|
+
...callId ? { callId } : {}
|
|
1509
|
+
};
|
|
1510
|
+
}
|
|
1511
|
+
function describeFileChangeApproval(params) {
|
|
1512
|
+
const record = isRecord3(params) ? params : {};
|
|
1513
|
+
const reason = trimText(record["reason"]);
|
|
1514
|
+
const grantRoot = asString(record["grantRoot"]);
|
|
1515
|
+
const callId = asString(record["callId"]);
|
|
1516
|
+
return {
|
|
1517
|
+
...reason ? { reason } : {},
|
|
1518
|
+
...grantRoot ? { grantRoot } : {},
|
|
1519
|
+
...callId ? { callId } : {},
|
|
1520
|
+
fileChanges: toApplyPatchFileChanges(record)
|
|
1521
|
+
};
|
|
1522
|
+
}
|
|
1523
|
+
function describeMcpElicitation(params) {
|
|
1524
|
+
const record = isRecord3(params) ? params : {};
|
|
1525
|
+
const appName = asString(record["appName"]) ?? asString(record["serverName"]) ?? void 0;
|
|
1526
|
+
const serverName = asString(record["serverName"]);
|
|
1527
|
+
const message = trimText(record["message"]);
|
|
1528
|
+
const url = asString(record["url"]) ?? asString(record["authorizationUrl"]);
|
|
1529
|
+
const identity = appName ?? url ?? "unknown";
|
|
1530
|
+
return {
|
|
1531
|
+
...appName ? { appName } : {},
|
|
1532
|
+
...serverName ? { serverName } : {},
|
|
1533
|
+
...message ? { message } : {},
|
|
1534
|
+
...url ? { url } : {},
|
|
1535
|
+
identity,
|
|
1536
|
+
mode: url ? "url" : "form",
|
|
1537
|
+
...record["requestedSchema"] !== void 0 ? { schema: record["requestedSchema"] } : {}
|
|
1538
|
+
};
|
|
1539
|
+
}
|
|
1540
|
+
function persistFromMeta(meta) {
|
|
1541
|
+
if (!isRecord3(meta)) return void 0;
|
|
1542
|
+
const persist = asString(meta["persist"]) ?? asString(meta["persistence"]);
|
|
1543
|
+
if (persist === "session" || persist === "acceptForSession") return "session";
|
|
1544
|
+
if (persist === "always" || persist === "permanent" || persist === "forever" || persist === "persistent" || persist === "acceptAlways") {
|
|
1545
|
+
return "always";
|
|
1546
|
+
}
|
|
1547
|
+
return void 0;
|
|
1548
|
+
}
|
|
1549
|
+
function toMcpElicitationResponse(decision, params) {
|
|
1550
|
+
const described = describeMcpElicitation(params);
|
|
1551
|
+
if (decision !== "allow" || described.mode === "url") {
|
|
1552
|
+
return {
|
|
1553
|
+
action: decision === "allow" ? "decline" : decision === "cancel" ? "cancel" : "decline"
|
|
1554
|
+
};
|
|
1555
|
+
}
|
|
1556
|
+
const schema = isRecord3(described.schema) ? described.schema : void 0;
|
|
1557
|
+
const properties = schema && isRecord3(schema["properties"]) ? schema["properties"] : {};
|
|
1558
|
+
const required = schema && Array.isArray(schema["required"]) ? schema["required"] : [];
|
|
1559
|
+
const content = {};
|
|
1560
|
+
for (const [key, spec] of Object.entries(properties)) {
|
|
1561
|
+
if (!isRecord3(spec)) continue;
|
|
1562
|
+
if (Array.isArray(spec["oneOf"]) || Array.isArray(spec["enum"])) {
|
|
1563
|
+
const options = Array.isArray(spec["oneOf"]) ? spec["oneOf"] : Array.isArray(spec["enum"]) ? spec["enum"] : [];
|
|
1564
|
+
const first = options[0];
|
|
1565
|
+
content[key] = isRecord3(first) ? first["const"] ?? first["value"] ?? first : first;
|
|
1566
|
+
} else if (spec["type"] === "boolean") {
|
|
1567
|
+
content[key] = true;
|
|
1568
|
+
} else if (typeof spec["default"] !== "undefined") {
|
|
1569
|
+
content[key] = spec["default"];
|
|
1570
|
+
}
|
|
1571
|
+
}
|
|
1572
|
+
for (const key of required) {
|
|
1573
|
+
if (typeof key === "string" && !(key in content)) {
|
|
1574
|
+
return { action: "decline" };
|
|
1575
|
+
}
|
|
1576
|
+
}
|
|
1577
|
+
const persist = persistFromMeta(isRecord3(params) ? params["_meta"] : void 0);
|
|
1578
|
+
return {
|
|
1579
|
+
action: "accept",
|
|
1580
|
+
content,
|
|
1581
|
+
...persist ? { _meta: { persist } } : {}
|
|
1582
|
+
};
|
|
1583
|
+
}
|
|
1584
|
+
function producedImages(params) {
|
|
1585
|
+
if (!isRecord3(params)) return [];
|
|
1586
|
+
const item = isRecord3(params["item"]) ? params["item"] : params;
|
|
1587
|
+
const type = toCanonicalItemType(asString(item["type"]) ?? void 0);
|
|
1588
|
+
const events = [];
|
|
1589
|
+
const threadId = conversationIdFromNotification(params) ?? "";
|
|
1590
|
+
const emit = (payload) => {
|
|
1591
|
+
events.push(
|
|
1592
|
+
baseEvent({
|
|
1593
|
+
type: "assistant.attachment.produced",
|
|
1594
|
+
threadId,
|
|
1595
|
+
payload,
|
|
1596
|
+
itemId: asString(item["id"]) ?? void 0,
|
|
1597
|
+
turnId: turnIdOf(params),
|
|
1598
|
+
method: "item/completed"
|
|
1599
|
+
})
|
|
1600
|
+
);
|
|
1601
|
+
};
|
|
1602
|
+
if (type === "image") {
|
|
1603
|
+
const savedPath = asString(item["savedPath"]) ?? asString(item["path"]);
|
|
1604
|
+
if (savedPath) emit({ path: savedPath, mimeType: "image/png" });
|
|
1605
|
+
}
|
|
1606
|
+
const content = Array.isArray(item["content"]) ? item["content"] : [];
|
|
1607
|
+
for (const entry of content) {
|
|
1608
|
+
if (!isRecord3(entry)) continue;
|
|
1609
|
+
if (entry["type"] === "image" && typeof entry["data"] === "string") {
|
|
1610
|
+
const mimeType = asString(entry["mimeType"]) ?? "image/png";
|
|
1611
|
+
emit({
|
|
1612
|
+
mimeType,
|
|
1613
|
+
data: entry["data"],
|
|
1614
|
+
extension: inferImageExtension({ mimeType })
|
|
1615
|
+
});
|
|
1616
|
+
}
|
|
1617
|
+
if (entry["type"] === "inputImage" && typeof entry["imageUrl"] === "string") {
|
|
1618
|
+
const parsed = parseBase64DataUrl(entry["imageUrl"]);
|
|
1619
|
+
if (parsed) {
|
|
1620
|
+
emit({
|
|
1621
|
+
mimeType: parsed.mimeType,
|
|
1622
|
+
data: parsed.base64,
|
|
1623
|
+
extension: inferImageExtension({ mimeType: parsed.mimeType })
|
|
1624
|
+
});
|
|
1625
|
+
}
|
|
1626
|
+
}
|
|
1627
|
+
}
|
|
1628
|
+
return events;
|
|
1629
|
+
}
|
|
1630
|
+
function mapNotificationToEvents(input) {
|
|
1631
|
+
const { method, params, threadId, providerInstanceId } = input;
|
|
1632
|
+
if (method.startsWith("collabAgent/")) {
|
|
1633
|
+
return mapCollabAgentEvent({ method, params, rootThreadId: threadId, providerInstanceId });
|
|
1634
|
+
}
|
|
1635
|
+
if (method === "error") {
|
|
1636
|
+
const willRetry = isRecord3(params) && params["willRetry"] === true;
|
|
1637
|
+
const nested = isRecord3(params) && isRecord3(params["error"]) ? params["error"] : void 0;
|
|
1638
|
+
const message = (nested ? trimText(nested["message"]) : void 0) ?? (isRecord3(params) ? trimText(params["message"]) : void 0) ?? "Codex error";
|
|
1639
|
+
const payload = { message };
|
|
1640
|
+
if (!willRetry) payload["class"] = "provider_error";
|
|
1641
|
+
if (params !== void 0) payload["detail"] = params;
|
|
1642
|
+
return [
|
|
1643
|
+
baseEvent({
|
|
1644
|
+
type: willRetry ? "runtime.warning" : "runtime.error",
|
|
1645
|
+
threadId,
|
|
1646
|
+
payload,
|
|
1647
|
+
turnId: turnIdOf(params),
|
|
1648
|
+
method,
|
|
1649
|
+
providerInstanceId
|
|
1650
|
+
})
|
|
1651
|
+
];
|
|
1652
|
+
}
|
|
1653
|
+
if (method === "process/stderr") {
|
|
1654
|
+
const text = isRecord3(params) ? trimText(params["text"]) ?? "" : "";
|
|
1655
|
+
if (!text) return [];
|
|
1656
|
+
if (isFatalStderr(text)) {
|
|
1657
|
+
return [
|
|
1658
|
+
baseEvent({
|
|
1659
|
+
type: "runtime.error",
|
|
1660
|
+
threadId,
|
|
1661
|
+
payload: { message: text },
|
|
1662
|
+
method,
|
|
1663
|
+
providerInstanceId
|
|
1664
|
+
})
|
|
1665
|
+
];
|
|
1666
|
+
}
|
|
1667
|
+
return [
|
|
1668
|
+
baseEvent({
|
|
1669
|
+
type: "runtime.warning",
|
|
1670
|
+
threadId,
|
|
1671
|
+
payload: { message: text },
|
|
1672
|
+
method,
|
|
1673
|
+
providerInstanceId
|
|
1674
|
+
})
|
|
1675
|
+
];
|
|
1676
|
+
}
|
|
1677
|
+
const turnId = turnIdOf(params);
|
|
1678
|
+
const itemId = itemIdOf(params);
|
|
1679
|
+
const type = itemTypeOf(params);
|
|
1680
|
+
switch (method) {
|
|
1681
|
+
case "thread/started":
|
|
1682
|
+
return [
|
|
1683
|
+
baseEvent({
|
|
1684
|
+
type: "thread.started",
|
|
1685
|
+
threadId,
|
|
1686
|
+
payload: isRecord3(params) ? params : {},
|
|
1687
|
+
method,
|
|
1688
|
+
providerInstanceId
|
|
1689
|
+
})
|
|
1690
|
+
];
|
|
1691
|
+
case "thread/status/changed": {
|
|
1692
|
+
const status = isRecord3(params) ? asString(params["status"]) : void 0;
|
|
1693
|
+
return [
|
|
1694
|
+
baseEvent({
|
|
1695
|
+
type: "thread.state.changed",
|
|
1696
|
+
threadId,
|
|
1697
|
+
payload: { state: toThreadState(status ?? void 0), status },
|
|
1698
|
+
method,
|
|
1699
|
+
providerInstanceId
|
|
1700
|
+
})
|
|
1701
|
+
];
|
|
1702
|
+
}
|
|
1703
|
+
case "thread/name/updated":
|
|
1704
|
+
return [
|
|
1705
|
+
baseEvent({
|
|
1706
|
+
type: "thread.metadata.updated",
|
|
1707
|
+
threadId,
|
|
1708
|
+
payload: { title: isRecord3(params) ? asString(params["name"]) : null },
|
|
1709
|
+
method,
|
|
1710
|
+
providerInstanceId
|
|
1711
|
+
})
|
|
1712
|
+
];
|
|
1713
|
+
case "thread/tokenUsage/updated": {
|
|
1714
|
+
const usage = normalizeCodexTokenUsage(params);
|
|
1715
|
+
if (!usage) return [];
|
|
1716
|
+
return [
|
|
1717
|
+
baseEvent({
|
|
1718
|
+
type: "thread.token-usage.updated",
|
|
1719
|
+
threadId,
|
|
1720
|
+
payload: usage,
|
|
1721
|
+
method,
|
|
1722
|
+
providerInstanceId
|
|
1723
|
+
})
|
|
1724
|
+
];
|
|
1725
|
+
}
|
|
1726
|
+
case "thread/closed":
|
|
1727
|
+
case "session/closed":
|
|
1728
|
+
return [
|
|
1729
|
+
baseEvent({
|
|
1730
|
+
type: "session.exited",
|
|
1731
|
+
threadId,
|
|
1732
|
+
payload: {},
|
|
1733
|
+
method,
|
|
1734
|
+
providerInstanceId
|
|
1735
|
+
})
|
|
1736
|
+
];
|
|
1737
|
+
case "turn/started":
|
|
1738
|
+
return [
|
|
1739
|
+
baseEvent({
|
|
1740
|
+
type: "turn.started",
|
|
1741
|
+
threadId,
|
|
1742
|
+
turnId,
|
|
1743
|
+
payload: { turnId },
|
|
1744
|
+
method,
|
|
1745
|
+
providerInstanceId
|
|
1746
|
+
})
|
|
1747
|
+
];
|
|
1748
|
+
case "turn/completed": {
|
|
1749
|
+
const flow = turnCompletedControlFlow(params);
|
|
1750
|
+
const typeName = flow.status === "cancelled" || flow.status === "interrupted" ? "turn.aborted" : "turn.completed";
|
|
1751
|
+
return [
|
|
1752
|
+
baseEvent({
|
|
1753
|
+
type: typeName,
|
|
1754
|
+
threadId,
|
|
1755
|
+
turnId,
|
|
1756
|
+
payload: flow,
|
|
1757
|
+
method,
|
|
1758
|
+
providerInstanceId
|
|
1759
|
+
})
|
|
1760
|
+
];
|
|
1761
|
+
}
|
|
1762
|
+
case "turn/plan":
|
|
1763
|
+
return [
|
|
1764
|
+
baseEvent({
|
|
1765
|
+
type: "turn.plan.updated",
|
|
1766
|
+
threadId,
|
|
1767
|
+
turnId,
|
|
1768
|
+
payload: params,
|
|
1769
|
+
method,
|
|
1770
|
+
providerInstanceId
|
|
1771
|
+
})
|
|
1772
|
+
];
|
|
1773
|
+
case "item/started": {
|
|
1774
|
+
if (type && normalizeItemType(type).includes("compact")) {
|
|
1775
|
+
return [
|
|
1776
|
+
baseEvent({
|
|
1777
|
+
type: "thread.state.changed",
|
|
1778
|
+
threadId,
|
|
1779
|
+
payload: { state: "compacting" },
|
|
1780
|
+
turnId,
|
|
1781
|
+
itemId,
|
|
1782
|
+
method,
|
|
1783
|
+
providerInstanceId
|
|
1784
|
+
})
|
|
1785
|
+
];
|
|
1786
|
+
}
|
|
1787
|
+
return [
|
|
1788
|
+
baseEvent({
|
|
1789
|
+
type: "item.started",
|
|
1790
|
+
threadId,
|
|
1791
|
+
turnId,
|
|
1792
|
+
itemId,
|
|
1793
|
+
payload: {
|
|
1794
|
+
itemType: toCanonicalItemType(type),
|
|
1795
|
+
title: itemTitle(params),
|
|
1796
|
+
detail: itemDetail(params)
|
|
1797
|
+
},
|
|
1798
|
+
method,
|
|
1799
|
+
providerInstanceId
|
|
1800
|
+
})
|
|
1801
|
+
];
|
|
1802
|
+
}
|
|
1803
|
+
case "item/completed": {
|
|
1804
|
+
if (type && normalizeItemType(type).includes("compact")) {
|
|
1805
|
+
return [
|
|
1806
|
+
baseEvent({
|
|
1807
|
+
type: "thread.state.changed",
|
|
1808
|
+
threadId,
|
|
1809
|
+
payload: { state: "compacted" },
|
|
1810
|
+
turnId,
|
|
1811
|
+
itemId,
|
|
1812
|
+
method,
|
|
1813
|
+
providerInstanceId
|
|
1814
|
+
})
|
|
1815
|
+
];
|
|
1816
|
+
}
|
|
1817
|
+
if (type && normalizeItemType(type).includes("plan")) {
|
|
1818
|
+
return [
|
|
1819
|
+
baseEvent({
|
|
1820
|
+
type: "turn.proposed.completed",
|
|
1821
|
+
threadId,
|
|
1822
|
+
turnId,
|
|
1823
|
+
itemId,
|
|
1824
|
+
payload: params,
|
|
1825
|
+
method,
|
|
1826
|
+
providerInstanceId
|
|
1827
|
+
})
|
|
1828
|
+
];
|
|
1829
|
+
}
|
|
1830
|
+
return [
|
|
1831
|
+
baseEvent({
|
|
1832
|
+
type: "item.completed",
|
|
1833
|
+
threadId,
|
|
1834
|
+
turnId,
|
|
1835
|
+
itemId,
|
|
1836
|
+
payload: {
|
|
1837
|
+
itemType: toCanonicalItemType(type),
|
|
1838
|
+
title: itemTitle(params),
|
|
1839
|
+
detail: itemDetail(params)
|
|
1840
|
+
},
|
|
1841
|
+
method,
|
|
1842
|
+
providerInstanceId
|
|
1843
|
+
}),
|
|
1844
|
+
...producedImages(params).map((event) => ({ ...event, threadId }))
|
|
1845
|
+
];
|
|
1846
|
+
}
|
|
1847
|
+
case "item/plan/delta":
|
|
1848
|
+
return [
|
|
1849
|
+
baseEvent({
|
|
1850
|
+
type: "turn.proposed.delta",
|
|
1851
|
+
threadId,
|
|
1852
|
+
turnId,
|
|
1853
|
+
itemId,
|
|
1854
|
+
payload: params,
|
|
1855
|
+
method,
|
|
1856
|
+
providerInstanceId
|
|
1857
|
+
})
|
|
1858
|
+
];
|
|
1859
|
+
case "item/agentMessage/delta":
|
|
1860
|
+
case "item/reasoning/delta":
|
|
1861
|
+
case "item/reasoning/summaryDelta": {
|
|
1862
|
+
const delta = isRecord3(params) ? asString(params["delta"]) ?? asString(params["text"]) ?? "" : "";
|
|
1863
|
+
return [
|
|
1864
|
+
baseEvent({
|
|
1865
|
+
type: "content.delta",
|
|
1866
|
+
threadId,
|
|
1867
|
+
turnId,
|
|
1868
|
+
itemId,
|
|
1869
|
+
payload: { kind: contentStreamKindFromMethod(method), delta },
|
|
1870
|
+
method,
|
|
1871
|
+
providerInstanceId
|
|
1872
|
+
})
|
|
1873
|
+
];
|
|
1874
|
+
}
|
|
1875
|
+
case "item/reasoning/summaryPartAdded":
|
|
1876
|
+
return [
|
|
1877
|
+
baseEvent({
|
|
1878
|
+
type: "content.delta",
|
|
1879
|
+
threadId,
|
|
1880
|
+
turnId,
|
|
1881
|
+
itemId,
|
|
1882
|
+
payload: {
|
|
1883
|
+
kind: "reasoning",
|
|
1884
|
+
delta: "",
|
|
1885
|
+
summaryIndex: isRecord3(params) ? asNumber(params["summaryIndex"]) : null
|
|
1886
|
+
},
|
|
1887
|
+
method,
|
|
1888
|
+
providerInstanceId
|
|
1889
|
+
})
|
|
1890
|
+
];
|
|
1891
|
+
case "account/updated":
|
|
1892
|
+
case "account/rateLimits/updated":
|
|
1893
|
+
return [
|
|
1894
|
+
baseEvent({
|
|
1895
|
+
type: method === "account/updated" ? "account.updated" : "account.rate-limits.updated",
|
|
1896
|
+
threadId,
|
|
1897
|
+
payload: params,
|
|
1898
|
+
method,
|
|
1899
|
+
providerInstanceId
|
|
1900
|
+
})
|
|
1901
|
+
];
|
|
1902
|
+
case "model/rerouted":
|
|
1903
|
+
return [
|
|
1904
|
+
baseEvent({
|
|
1905
|
+
type: "model.rerouted",
|
|
1906
|
+
threadId,
|
|
1907
|
+
payload: params,
|
|
1908
|
+
method,
|
|
1909
|
+
providerInstanceId
|
|
1910
|
+
})
|
|
1911
|
+
];
|
|
1912
|
+
case "config/warning":
|
|
1913
|
+
return [
|
|
1914
|
+
baseEvent({
|
|
1915
|
+
type: "config.warning",
|
|
1916
|
+
threadId,
|
|
1917
|
+
payload: params,
|
|
1918
|
+
method,
|
|
1919
|
+
providerInstanceId
|
|
1920
|
+
})
|
|
1921
|
+
];
|
|
1922
|
+
case "mcp/oauth/completed":
|
|
1923
|
+
return [
|
|
1924
|
+
baseEvent({
|
|
1925
|
+
type: "mcp.oauth.completed",
|
|
1926
|
+
threadId,
|
|
1927
|
+
payload: params,
|
|
1928
|
+
method,
|
|
1929
|
+
providerInstanceId
|
|
1930
|
+
})
|
|
1931
|
+
];
|
|
1932
|
+
default:
|
|
1933
|
+
return [];
|
|
1934
|
+
}
|
|
1935
|
+
}
|
|
1936
|
+
function mapServerRequestToEvents(input) {
|
|
1937
|
+
const requestId = String(input.id);
|
|
1938
|
+
if (input.method === "item/tool/requestUserInput") {
|
|
1939
|
+
const questions = toUserInputQuestions(input.params);
|
|
1940
|
+
if (!questions) return [];
|
|
1941
|
+
return [
|
|
1942
|
+
baseEvent({
|
|
1943
|
+
type: "user-input.requested",
|
|
1944
|
+
threadId: input.threadId,
|
|
1945
|
+
requestId,
|
|
1946
|
+
payload: { questions },
|
|
1947
|
+
method: input.method,
|
|
1948
|
+
source: "codex.app-server.request",
|
|
1949
|
+
providerInstanceId: input.providerInstanceId
|
|
1950
|
+
})
|
|
1951
|
+
];
|
|
1952
|
+
}
|
|
1953
|
+
const kind = input.method === "item/commandExecution/requestApproval" ? "command" : input.method === "item/fileChange/requestApproval" ? "file-change" : input.method === "mcpServer/elicitation/request" ? "mcp-elicitation" : input.method.includes("applyPatch") ? "apply-patch" : "command";
|
|
1954
|
+
const args = kind === "command" ? describeCommandApproval(input.params) : kind === "file-change" || kind === "apply-patch" ? describeFileChangeApproval(input.params) : describeMcpElicitation(input.params);
|
|
1955
|
+
return [
|
|
1956
|
+
baseEvent({
|
|
1957
|
+
type: "request.opened",
|
|
1958
|
+
threadId: input.threadId,
|
|
1959
|
+
requestId,
|
|
1960
|
+
payload: {
|
|
1961
|
+
kind,
|
|
1962
|
+
args,
|
|
1963
|
+
declineReasonRelay: "queued"
|
|
1964
|
+
},
|
|
1965
|
+
method: input.method,
|
|
1966
|
+
source: "codex.app-server.request",
|
|
1967
|
+
providerInstanceId: input.providerInstanceId
|
|
1968
|
+
})
|
|
1969
|
+
];
|
|
1970
|
+
}
|
|
1971
|
+
|
|
1972
|
+
// src/sessionRuntime.ts
|
|
1973
|
+
var sleep = (ms) => new Promise((resolve2) => {
|
|
1974
|
+
setTimeout(resolve2, ms);
|
|
1975
|
+
});
|
|
1976
|
+
function isRootChild(agentPath, agentThreadId, rootThreadId) {
|
|
1977
|
+
if (agentThreadId === rootThreadId) return true;
|
|
1978
|
+
return agentPath === "/root" || agentPath === "/";
|
|
1979
|
+
}
|
|
1980
|
+
function pathLeaf2(value) {
|
|
1981
|
+
if (!value) return void 0;
|
|
1982
|
+
const parts = value.split("/").filter((part) => part.length > 0);
|
|
1983
|
+
return parts.at(-1);
|
|
1984
|
+
}
|
|
1985
|
+
var CodexSessionRuntime = class {
|
|
1986
|
+
threadId;
|
|
1987
|
+
cwd;
|
|
1988
|
+
session;
|
|
1989
|
+
nativeThreadId;
|
|
1990
|
+
activeTurnId;
|
|
1991
|
+
model;
|
|
1992
|
+
runtimeMode;
|
|
1993
|
+
interactionMode;
|
|
1994
|
+
closed = false;
|
|
1995
|
+
closeCompleted = false;
|
|
1996
|
+
turns = [];
|
|
1997
|
+
pendingApprovals = /* @__PURE__ */ new Map();
|
|
1998
|
+
pendingUserInputs = /* @__PURE__ */ new Map();
|
|
1999
|
+
writeAcks = /* @__PURE__ */ new Map();
|
|
2000
|
+
grants;
|
|
2001
|
+
declineRelay;
|
|
2002
|
+
collabChildren = /* @__PURE__ */ new Map();
|
|
2003
|
+
liveForeignTurns = /* @__PURE__ */ new Map();
|
|
2004
|
+
collabReceiverTurns = /* @__PURE__ */ new Map();
|
|
2005
|
+
memoryThreadIds = /* @__PURE__ */ new Set();
|
|
2006
|
+
appServerArgs;
|
|
2007
|
+
host;
|
|
2008
|
+
rpc;
|
|
2009
|
+
emit;
|
|
2010
|
+
constructor(host, rpc, emit, input, cwd, appServerArgs, grants, declineRelay) {
|
|
2011
|
+
this.host = host;
|
|
2012
|
+
this.rpc = rpc;
|
|
2013
|
+
this.emit = emit;
|
|
2014
|
+
this.threadId = input.threadId;
|
|
2015
|
+
this.cwd = cwd;
|
|
2016
|
+
this.runtimeMode = input.runtimeMode;
|
|
2017
|
+
this.appServerArgs = appServerArgs;
|
|
2018
|
+
this.grants = grants ?? createApprovalGrantStore();
|
|
2019
|
+
this.declineRelay = declineRelay ?? createDeclineReasonRelay();
|
|
2020
|
+
const createdAt = nowIso();
|
|
2021
|
+
this.session = {
|
|
2022
|
+
provider: CODEX_DRIVER_KIND,
|
|
2023
|
+
providerInstanceId: host.instanceId,
|
|
2024
|
+
status: "idle",
|
|
2025
|
+
runtimeMode: input.runtimeMode,
|
|
2026
|
+
cwd,
|
|
2027
|
+
threadId: input.threadId,
|
|
2028
|
+
createdAt,
|
|
2029
|
+
updatedAt: createdAt,
|
|
2030
|
+
...input.bindingGeneration === void 0 ? {} : { bindingGeneration: input.bindingGeneration }
|
|
2031
|
+
};
|
|
2032
|
+
}
|
|
2033
|
+
registerWriteAck(jsonRpcId) {
|
|
2034
|
+
return new Promise((resolve2) => {
|
|
2035
|
+
this.writeAcks.set(jsonRpcId, { resolve: resolve2 });
|
|
2036
|
+
});
|
|
2037
|
+
}
|
|
2038
|
+
settleNativeWrite(requestId, status) {
|
|
2039
|
+
const ack = this.writeAcks.get(requestId);
|
|
2040
|
+
if (!ack) return;
|
|
2041
|
+
this.writeAcks.delete(requestId);
|
|
2042
|
+
ack.resolve(status);
|
|
2043
|
+
}
|
|
2044
|
+
failPendingNativeWrites(reason) {
|
|
2045
|
+
for (const [, ack] of this.writeAcks) ack.resolve("closed");
|
|
2046
|
+
this.writeAcks.clear();
|
|
2047
|
+
void reason;
|
|
2048
|
+
}
|
|
2049
|
+
async confirmAfterNativeWrite(jsonRpcId) {
|
|
2050
|
+
const existing = this.writeAcks.get(jsonRpcId);
|
|
2051
|
+
if (existing) {
|
|
2052
|
+
return await new Promise((resolve2) => {
|
|
2053
|
+
const previous = existing.resolve;
|
|
2054
|
+
existing.resolve = (status) => {
|
|
2055
|
+
previous(status);
|
|
2056
|
+
resolve2(status);
|
|
2057
|
+
};
|
|
2058
|
+
});
|
|
2059
|
+
}
|
|
2060
|
+
return await this.registerWriteAck(jsonRpcId);
|
|
2061
|
+
}
|
|
2062
|
+
handleStderrLine(line) {
|
|
2063
|
+
const kind = classifyStderrLine(line);
|
|
2064
|
+
if (kind === "drop") return;
|
|
2065
|
+
if (isFatalStderr(line)) {
|
|
2066
|
+
this.emit({
|
|
2067
|
+
eventId: `codex-stderr-${Date.now()}`,
|
|
2068
|
+
provider: CODEX_DRIVER_KIND,
|
|
2069
|
+
threadId: this.threadId,
|
|
2070
|
+
createdAt: nowIso(),
|
|
2071
|
+
type: "runtime.error",
|
|
2072
|
+
payload: { message: line }
|
|
2073
|
+
});
|
|
2074
|
+
return;
|
|
2075
|
+
}
|
|
2076
|
+
if (kind === "error") {
|
|
2077
|
+
this.host.log.error(line, { threadId: this.threadId });
|
|
2078
|
+
return;
|
|
2079
|
+
}
|
|
2080
|
+
this.host.log.warn(line, { threadId: this.threadId });
|
|
2081
|
+
}
|
|
2082
|
+
handleNotification(method, params) {
|
|
2083
|
+
if (this.closed) return;
|
|
2084
|
+
const conversationId = conversationIdFromNotification(params);
|
|
2085
|
+
if (conversationId && conversationId !== this.nativeThreadId && conversationId !== this.threadId) {
|
|
2086
|
+
this.interceptCollab(method, params, conversationId);
|
|
2087
|
+
return;
|
|
2088
|
+
}
|
|
2089
|
+
if (conversationId && this.memoryThreadIds.has(conversationId)) {
|
|
2090
|
+
if (method === "serverRequest" || method === "serverRequest/resolved") {
|
|
2091
|
+
this.dispatchMapped(method, params);
|
|
2092
|
+
}
|
|
2093
|
+
return;
|
|
2094
|
+
}
|
|
2095
|
+
if (method === "thread/started") {
|
|
2096
|
+
const name = isRecord3(params) ? asString(params["name"]) ?? (isRecord3(params["thread"]) ? asString(params["thread"]["name"]) : null) : null;
|
|
2097
|
+
if (isMemoryConsolidationThread(name ?? void 0) && conversationId) {
|
|
2098
|
+
this.memoryThreadIds.add(conversationId);
|
|
2099
|
+
return;
|
|
2100
|
+
}
|
|
2101
|
+
}
|
|
2102
|
+
this.dispatchMapped(method, params);
|
|
2103
|
+
}
|
|
2104
|
+
dispatchMapped(method, params) {
|
|
2105
|
+
for (const event of mapNotificationToEvents({
|
|
2106
|
+
method,
|
|
2107
|
+
params,
|
|
2108
|
+
threadId: this.threadId,
|
|
2109
|
+
providerInstanceId: this.host.instanceId
|
|
2110
|
+
})) {
|
|
2111
|
+
this.emit(event);
|
|
2112
|
+
}
|
|
2113
|
+
if (method === "turn/started") {
|
|
2114
|
+
const turnId = isRecord3(params) ? asString(params["turnId"]) ?? (isRecord3(params["turn"]) ? asString(params["turn"]["id"]) : null) : null;
|
|
2115
|
+
if (turnId) this.activeTurnId = turnId;
|
|
2116
|
+
}
|
|
2117
|
+
if (method === "turn/completed") {
|
|
2118
|
+
this.activeTurnId = void 0;
|
|
2119
|
+
const { activeTurnId: _completedTurn, ...idleSession } = this.session;
|
|
2120
|
+
this.session = { ...idleSession, status: "idle", updatedAt: nowIso() };
|
|
2121
|
+
}
|
|
2122
|
+
}
|
|
2123
|
+
interceptCollab(method, params, conversationId) {
|
|
2124
|
+
const source = isRecord3(params) ? params["source"] : void 0;
|
|
2125
|
+
const subAgent = isRecord3(source) ? source["subAgent"] : void 0;
|
|
2126
|
+
const spawn = isRecord3(subAgent) ? subAgent["thread_spawn"] : void 0;
|
|
2127
|
+
if (method === "thread/started" && isRecord3(spawn)) {
|
|
2128
|
+
const agentPath = asString(spawn["agent_path"]) ?? void 0;
|
|
2129
|
+
if (isRootChild(agentPath, conversationId, this.nativeThreadId ?? this.threadId)) return;
|
|
2130
|
+
const existing = this.collabChildren.get(conversationId);
|
|
2131
|
+
const spawnTurnId = existing?.spawnTurnId ?? this.activeTurnId;
|
|
2132
|
+
const child2 = {
|
|
2133
|
+
agentThreadId: conversationId,
|
|
2134
|
+
...agentPath ? { agentPath } : {},
|
|
2135
|
+
...asString(spawn["agent_role"]) ? { role: asString(spawn["agent_role"]) } : {},
|
|
2136
|
+
...asString(spawn["agent_nickname"]) ? { nickname: asString(spawn["agent_nickname"]) } : {},
|
|
2137
|
+
...spawnTurnId ? { spawnTurnId } : {}
|
|
2138
|
+
};
|
|
2139
|
+
this.collabChildren.set(conversationId, child2);
|
|
2140
|
+
this.emitCollab("collabAgent/started", child2);
|
|
2141
|
+
return;
|
|
2142
|
+
}
|
|
2143
|
+
const itemType = itemTypeOf(params);
|
|
2144
|
+
if ((method === "item/started" || method === "item/completed") && itemType === "collabAgentToolCall") {
|
|
2145
|
+
const item = isRecord3(params) ? isRecord3(params["item"]) ? params["item"] : params : {};
|
|
2146
|
+
const receiver = asString(item["receiverThreadId"]) ?? asString(item["agentThreadId"]);
|
|
2147
|
+
if (receiver && this.activeTurnId) this.collabReceiverTurns.set(receiver, this.activeTurnId);
|
|
2148
|
+
return;
|
|
2149
|
+
}
|
|
2150
|
+
if ((method === "item/started" || method === "item/completed") && itemType === "subAgentActivity") {
|
|
2151
|
+
const item = isRecord3(params) ? isRecord3(params["item"]) ? params["item"] : params : {};
|
|
2152
|
+
const agentThreadId = asString(item["agentThreadId"]) ?? conversationId;
|
|
2153
|
+
const agentPath = asString(item["agentPath"]) ?? void 0;
|
|
2154
|
+
if (isRootChild(agentPath, agentThreadId, this.nativeThreadId ?? this.threadId)) return;
|
|
2155
|
+
if (!this.collabChildren.has(agentThreadId)) {
|
|
2156
|
+
const spawnTurnId = this.activeTurnId;
|
|
2157
|
+
const child2 = {
|
|
2158
|
+
agentThreadId,
|
|
2159
|
+
...agentPath ? { agentPath } : {},
|
|
2160
|
+
...spawnTurnId ? { spawnTurnId } : {}
|
|
2161
|
+
};
|
|
2162
|
+
this.collabChildren.set(agentThreadId, child2);
|
|
2163
|
+
this.emitCollab("collabAgent/started", child2);
|
|
2164
|
+
}
|
|
2165
|
+
}
|
|
2166
|
+
const child = this.collabChildren.get(conversationId);
|
|
2167
|
+
if (method === "turn/started") {
|
|
2168
|
+
const turnId = isRecord3(params) ? asString(params["turnId"]) ?? (isRecord3(params["turn"]) ? asString(params["turn"]["id"]) : null) : null;
|
|
2169
|
+
if (turnId) this.liveForeignTurns.set(conversationId, turnId);
|
|
2170
|
+
}
|
|
2171
|
+
if (method === "turn/completed" || method === "thread/closed") {
|
|
2172
|
+
this.liveForeignTurns.delete(conversationId);
|
|
2173
|
+
}
|
|
2174
|
+
if (!child) {
|
|
2175
|
+
if (CHILD_CHATTER_METHODS.has(method) || !CHILD_AGENT_EVENT_METHODS.has(method)) return;
|
|
2176
|
+
return;
|
|
2177
|
+
}
|
|
2178
|
+
if (method === "turn/completed") {
|
|
2179
|
+
this.emitCollab("collabAgent/completed", child);
|
|
2180
|
+
return;
|
|
2181
|
+
}
|
|
2182
|
+
if (method === "error") {
|
|
2183
|
+
const willRetry = isRecord3(params) ? params["willRetry"] === true : false;
|
|
2184
|
+
if (willRetry) return;
|
|
2185
|
+
this.emitCollab("collabAgent/statusChanged", child, { status: "systemError" });
|
|
2186
|
+
return;
|
|
2187
|
+
}
|
|
2188
|
+
if (method === "thread/tokenUsage/updated") {
|
|
2189
|
+
this.emitCollab("collabAgent/tokenUsage", child, params);
|
|
2190
|
+
return;
|
|
2191
|
+
}
|
|
2192
|
+
if (CHILD_CHATTER_METHODS.has(method)) return;
|
|
2193
|
+
if (CHILD_AGENT_EVENT_METHODS.has(method)) {
|
|
2194
|
+
this.emitCollab("collabAgent/progress", child, params);
|
|
2195
|
+
}
|
|
2196
|
+
}
|
|
2197
|
+
emitCollab(method, child, extra) {
|
|
2198
|
+
const params = {
|
|
2199
|
+
agentThreadId: child.agentThreadId,
|
|
2200
|
+
agentPath: child.agentPath,
|
|
2201
|
+
agentRole: child.role ?? pathLeaf2(child.agentPath) ?? "general-purpose",
|
|
2202
|
+
agentNickname: child.nickname,
|
|
2203
|
+
...isRecord3(extra) ? extra : {}
|
|
2204
|
+
};
|
|
2205
|
+
for (const event of mapNotificationToEvents({
|
|
2206
|
+
method,
|
|
2207
|
+
params,
|
|
2208
|
+
threadId: this.threadId,
|
|
2209
|
+
providerInstanceId: this.host.instanceId
|
|
2210
|
+
})) {
|
|
2211
|
+
this.emit(event);
|
|
2212
|
+
}
|
|
2213
|
+
}
|
|
2214
|
+
async handleServerRequest(id, method, params) {
|
|
2215
|
+
if (method === "item/commandExecution/requestApproval") {
|
|
2216
|
+
await this.handleApproval(
|
|
2217
|
+
id,
|
|
2218
|
+
method,
|
|
2219
|
+
params,
|
|
2220
|
+
"command",
|
|
2221
|
+
describeCommandApproval(params).command
|
|
2222
|
+
);
|
|
2223
|
+
return;
|
|
2224
|
+
}
|
|
2225
|
+
if (method === "item/fileChange/requestApproval") {
|
|
2226
|
+
const described = describeFileChangeApproval(params);
|
|
2227
|
+
await this.handleApproval(
|
|
2228
|
+
id,
|
|
2229
|
+
method,
|
|
2230
|
+
params,
|
|
2231
|
+
"file-change",
|
|
2232
|
+
described.callId ?? "file-change"
|
|
2233
|
+
);
|
|
2234
|
+
return;
|
|
2235
|
+
}
|
|
2236
|
+
if (method === "mcpServer/elicitation/request") {
|
|
2237
|
+
const described = describeMcpElicitation(params);
|
|
2238
|
+
await this.handleApproval(id, method, params, "mcp-elicitation", described.identity);
|
|
2239
|
+
return;
|
|
2240
|
+
}
|
|
2241
|
+
if (method === "item/tool/requestUserInput") {
|
|
2242
|
+
const requestId = String(id);
|
|
2243
|
+
const events = mapServerRequestToEvents({
|
|
2244
|
+
id,
|
|
2245
|
+
method,
|
|
2246
|
+
params,
|
|
2247
|
+
threadId: this.threadId,
|
|
2248
|
+
providerInstanceId: this.host.instanceId
|
|
2249
|
+
});
|
|
2250
|
+
if (events.length === 0) {
|
|
2251
|
+
await this.rpc.respond(id, {});
|
|
2252
|
+
return;
|
|
2253
|
+
}
|
|
2254
|
+
await new Promise((resolve2) => {
|
|
2255
|
+
this.pendingUserInputs.set(requestId, {
|
|
2256
|
+
jsonRpcId: id,
|
|
2257
|
+
settle: (result) => {
|
|
2258
|
+
this.pendingUserInputs.delete(requestId);
|
|
2259
|
+
void this.rpc.respond(id, result).finally(resolve2);
|
|
2260
|
+
}
|
|
2261
|
+
});
|
|
2262
|
+
for (const event of events) this.emit(event);
|
|
2263
|
+
});
|
|
2264
|
+
return;
|
|
2265
|
+
}
|
|
2266
|
+
await this.rpc.respondError(id, -32601, `Method not found: ${method}`);
|
|
2267
|
+
}
|
|
2268
|
+
async handleApproval(id, method, params, kind, identity) {
|
|
2269
|
+
const grant = this.grants.lookup(codexApprovalGrantKey(kind, identity));
|
|
2270
|
+
if (grant) {
|
|
2271
|
+
const decision = wireDecisionFromPersist(kind, grant);
|
|
2272
|
+
await this.rpc.respond(id, { decision });
|
|
2273
|
+
return;
|
|
2274
|
+
}
|
|
2275
|
+
const requestId = String(id);
|
|
2276
|
+
const events = mapServerRequestToEvents({
|
|
2277
|
+
id,
|
|
2278
|
+
method,
|
|
2279
|
+
params,
|
|
2280
|
+
threadId: this.threadId,
|
|
2281
|
+
providerInstanceId: this.host.instanceId
|
|
2282
|
+
});
|
|
2283
|
+
await new Promise((resolve2) => {
|
|
2284
|
+
this.pendingApprovals.set(requestId, {
|
|
2285
|
+
jsonRpcId: id,
|
|
2286
|
+
method,
|
|
2287
|
+
params,
|
|
2288
|
+
kind,
|
|
2289
|
+
identity,
|
|
2290
|
+
settle: (decision) => {
|
|
2291
|
+
this.pendingApprovals.delete(requestId);
|
|
2292
|
+
void this.rpc.respond(id, decision).finally(resolve2);
|
|
2293
|
+
}
|
|
2294
|
+
});
|
|
2295
|
+
for (const event of events) this.emit(event);
|
|
2296
|
+
});
|
|
2297
|
+
}
|
|
2298
|
+
async respondToRequest(requestId, decision, reason, persist) {
|
|
2299
|
+
const pending = this.pendingApprovals.get(requestId);
|
|
2300
|
+
if (!pending) throw new Error(`unknown request ${requestId}`);
|
|
2301
|
+
if (decision === "deny" && reason) this.declineRelay.queue(this.threadId, reason);
|
|
2302
|
+
let wire = { decision: decision === "allow" ? "accept" : "decline" };
|
|
2303
|
+
if (decision === "allow" && pending.kind === "mcp-elicitation") {
|
|
2304
|
+
wire = toMcpElicitationResponse("allow", pending.params);
|
|
2305
|
+
}
|
|
2306
|
+
if (decision === "allow" && persist) {
|
|
2307
|
+
this.grants.remember(codexApprovalGrantKey(pending.kind, pending.identity), persist);
|
|
2308
|
+
if (pending.kind !== "mcp-elicitation") {
|
|
2309
|
+
wire = { decision: wireDecisionFromPersist(pending.kind, persist) };
|
|
2310
|
+
}
|
|
2311
|
+
}
|
|
2312
|
+
const ack = this.registerWriteAck(pending.jsonRpcId);
|
|
2313
|
+
pending.settle(wire);
|
|
2314
|
+
await ack.catch(() => "closed");
|
|
2315
|
+
this.emit({
|
|
2316
|
+
eventId: `codex-req-${requestId}`,
|
|
2317
|
+
provider: CODEX_DRIVER_KIND,
|
|
2318
|
+
threadId: this.threadId,
|
|
2319
|
+
createdAt: nowIso(),
|
|
2320
|
+
type: "request.resolved",
|
|
2321
|
+
requestId,
|
|
2322
|
+
payload: { decision }
|
|
2323
|
+
});
|
|
2324
|
+
}
|
|
2325
|
+
async respondToUserInput(requestId, answers) {
|
|
2326
|
+
const pending = this.pendingUserInputs.get(requestId);
|
|
2327
|
+
if (!pending) throw new Error(`unknown user-input ${requestId}`);
|
|
2328
|
+
const mapped = toCodexUserInputAnswers(answers);
|
|
2329
|
+
pending.settle(mapped === void 0 ? {} : { answers: mapped });
|
|
2330
|
+
this.emit({
|
|
2331
|
+
eventId: `codex-ui-${requestId}`,
|
|
2332
|
+
provider: CODEX_DRIVER_KIND,
|
|
2333
|
+
threadId: this.threadId,
|
|
2334
|
+
createdAt: nowIso(),
|
|
2335
|
+
type: "user-input.resolved",
|
|
2336
|
+
requestId,
|
|
2337
|
+
payload: { answers: mapped ?? [] }
|
|
2338
|
+
});
|
|
2339
|
+
}
|
|
2340
|
+
async withActiveWriterRecovery(run, allowUnsubscribe) {
|
|
2341
|
+
let lastError;
|
|
2342
|
+
for (let attempt = 0; attempt < ACTIVE_WRITER_ATTEMPTS; attempt += 1) {
|
|
2343
|
+
try {
|
|
2344
|
+
return await run();
|
|
2345
|
+
} catch (error) {
|
|
2346
|
+
lastError = error;
|
|
2347
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
2348
|
+
if (!isActiveWriterConflict(message) || !this.nativeThreadId) throw error;
|
|
2349
|
+
await sleep(ACTIVE_WRITER_DELAY_MS);
|
|
2350
|
+
let probe;
|
|
2351
|
+
try {
|
|
2352
|
+
probe = await this.rpc.request("thread/read", { threadId: this.nativeThreadId }, 0);
|
|
2353
|
+
} catch {
|
|
2354
|
+
continue;
|
|
2355
|
+
}
|
|
2356
|
+
const liveTurn = inProgressTurnIdFromThreadRead(probe);
|
|
2357
|
+
if (liveTurn && allowUnsubscribe) {
|
|
2358
|
+
try {
|
|
2359
|
+
await this.rpc.request("turn/interrupt", {
|
|
2360
|
+
threadId: this.nativeThreadId,
|
|
2361
|
+
turnId: liveTurn
|
|
2362
|
+
});
|
|
2363
|
+
} catch {
|
|
2364
|
+
}
|
|
2365
|
+
}
|
|
2366
|
+
}
|
|
2367
|
+
}
|
|
2368
|
+
throw lastError instanceof Error ? lastError : new Error(String(lastError));
|
|
2369
|
+
}
|
|
2370
|
+
threadConfigParams() {
|
|
2371
|
+
const config = runtimeModeToThreadConfig(this.runtimeMode);
|
|
2372
|
+
return {
|
|
2373
|
+
approvalPolicy: config.approvalPolicy,
|
|
2374
|
+
sandbox: config.sandbox,
|
|
2375
|
+
approvalsReviewer: config.approvalsReviewer,
|
|
2376
|
+
cwd: this.cwd
|
|
2377
|
+
};
|
|
2378
|
+
}
|
|
2379
|
+
async openThread(input) {
|
|
2380
|
+
const resume = input.discardResumeCursor === true ? void 0 : parseCodexResumeCursor(input.resumeCursor);
|
|
2381
|
+
const requestedModel = input.launch.requestedModelId?.trim() || modelIdFromSelection(input.modelSelection);
|
|
2382
|
+
const startParams = {
|
|
2383
|
+
...this.threadConfigParams(),
|
|
2384
|
+
...requestedModel ? { model: requestedModel } : {}
|
|
2385
|
+
};
|
|
2386
|
+
if (resume) {
|
|
2387
|
+
try {
|
|
2388
|
+
const result = await this.withActiveWriterRecovery(
|
|
2389
|
+
() => this.rpc.request("thread/resume", { threadId: resume.threadId, ...startParams }, 0),
|
|
2390
|
+
true
|
|
2391
|
+
);
|
|
2392
|
+
const opened = decodeThreadOpen(result);
|
|
2393
|
+
this.nativeThreadId = opened.threadId;
|
|
2394
|
+
this.model = opened.model ?? requestedModel;
|
|
2395
|
+
} catch (error) {
|
|
2396
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
2397
|
+
if (!isRecoverableThreadResumeError(message)) throw error;
|
|
2398
|
+
const result = await this.rpc.request("thread/start", startParams, 0);
|
|
2399
|
+
const opened = decodeThreadOpen(result);
|
|
2400
|
+
this.nativeThreadId = opened.threadId;
|
|
2401
|
+
this.model = opened.model ?? requestedModel;
|
|
2402
|
+
}
|
|
2403
|
+
} else {
|
|
2404
|
+
const result = await this.rpc.request("thread/start", startParams, 0);
|
|
2405
|
+
const opened = decodeThreadOpen(result);
|
|
2406
|
+
this.nativeThreadId = opened.threadId;
|
|
2407
|
+
this.model = opened.model ?? requestedModel;
|
|
2408
|
+
}
|
|
2409
|
+
this.session = {
|
|
2410
|
+
...this.session,
|
|
2411
|
+
status: "idle",
|
|
2412
|
+
resumeCursor: { threadId: this.nativeThreadId },
|
|
2413
|
+
updatedAt: nowIso(),
|
|
2414
|
+
...this.model ? { model: this.model } : {}
|
|
2415
|
+
};
|
|
2416
|
+
this.emit({
|
|
2417
|
+
eventId: `codex-session-${this.threadId}`,
|
|
2418
|
+
provider: CODEX_DRIVER_KIND,
|
|
2419
|
+
threadId: this.threadId,
|
|
2420
|
+
createdAt: nowIso(),
|
|
2421
|
+
type: "session.started",
|
|
2422
|
+
payload: { resumeCursor: this.session.resumeCursor }
|
|
2423
|
+
});
|
|
2424
|
+
return this.session;
|
|
2425
|
+
}
|
|
2426
|
+
developerInstructions(input) {
|
|
2427
|
+
const mode = typeof input.interactionMode === "string" ? input.interactionMode : void 0;
|
|
2428
|
+
this.interactionMode = mode;
|
|
2429
|
+
if (mode === void 0) return void 0;
|
|
2430
|
+
const tools = hasConfiguredMcpServer(this.appServerArgs);
|
|
2431
|
+
const model = this.model ?? DEFAULT_MODEL;
|
|
2432
|
+
const effort = modelSelectionStringOption(input.modelSelection, "reasoningEffort") ?? DEFAULT_TEXT_GENERATION_REASONING_EFFORT;
|
|
2433
|
+
return buildCodexDeveloperInstructions(mode, { model, reasoningEffort: effort }, tools, tools);
|
|
2434
|
+
}
|
|
2435
|
+
turnInput(input) {
|
|
2436
|
+
const items = [];
|
|
2437
|
+
const declined = this.declineRelay.take(this.threadId);
|
|
2438
|
+
const text = [declined, input.input ?? ""].filter((part) => part && part.length > 0).join("\n\n");
|
|
2439
|
+
if (text.length > 0) items.push({ type: "text", text });
|
|
2440
|
+
if (Array.isArray(input.attachments)) {
|
|
2441
|
+
for (const attachment of input.attachments) {
|
|
2442
|
+
if (!isRecord3(attachment)) continue;
|
|
2443
|
+
const url = asString(attachment["dataUrl"]) ?? asString(attachment["url"]);
|
|
2444
|
+
if (url) items.push({ type: "image", url });
|
|
2445
|
+
}
|
|
2446
|
+
}
|
|
2447
|
+
return items;
|
|
2448
|
+
}
|
|
2449
|
+
async sendTurn(input) {
|
|
2450
|
+
if (!this.nativeThreadId) throw new Error("codex session has no native thread");
|
|
2451
|
+
if (hasConfiguredMcpServer(this.appServerArgs)) {
|
|
2452
|
+
await this.rpc.request("mcpServer/reload", {}, 0).catch(() => void 0);
|
|
2453
|
+
}
|
|
2454
|
+
const sandboxPolicy = runtimeModeToTurnSandboxPolicy(this.runtimeMode);
|
|
2455
|
+
const config = runtimeModeToThreadConfig(this.runtimeMode);
|
|
2456
|
+
const developerInstructions = this.developerInstructions(input);
|
|
2457
|
+
const requestedModel = modelIdFromSelection(input.modelSelection) ?? this.model;
|
|
2458
|
+
const params = {
|
|
2459
|
+
threadId: this.nativeThreadId,
|
|
2460
|
+
input: this.turnInput(input),
|
|
2461
|
+
approvalPolicy: config.approvalPolicy,
|
|
2462
|
+
approvalsReviewer: config.approvalsReviewer,
|
|
2463
|
+
sandboxPolicy,
|
|
2464
|
+
...requestedModel ? { model: requestedModel } : {},
|
|
2465
|
+
...developerInstructions ? { developerInstructions } : {}
|
|
2466
|
+
};
|
|
2467
|
+
if (input.interactionMode !== void 0) {
|
|
2468
|
+
params["collaborationMode"] = input.interactionMode === "plan" ? "plan" : "default";
|
|
2469
|
+
}
|
|
2470
|
+
const isSteer = input.expectedTurnId !== void 0;
|
|
2471
|
+
const result = isSteer ? await this.rpc.request(
|
|
2472
|
+
"turn/steer",
|
|
2473
|
+
{
|
|
2474
|
+
...params,
|
|
2475
|
+
turnId: input.expectedTurnId
|
|
2476
|
+
},
|
|
2477
|
+
0
|
|
2478
|
+
) : await this.withActiveWriterRecovery(() => this.rpc.request("turn/start", params, 0), false);
|
|
2479
|
+
const started = decodeTurnStart(result);
|
|
2480
|
+
this.activeTurnId = started.turnId;
|
|
2481
|
+
this.turns.push({ id: started.turnId, items: [] });
|
|
2482
|
+
this.session = {
|
|
2483
|
+
...this.session,
|
|
2484
|
+
status: "running",
|
|
2485
|
+
activeTurnId: started.turnId,
|
|
2486
|
+
updatedAt: nowIso(),
|
|
2487
|
+
...requestedModel ? { model: requestedModel } : {}
|
|
2488
|
+
};
|
|
2489
|
+
return {
|
|
2490
|
+
threadId: this.threadId,
|
|
2491
|
+
turnId: started.turnId,
|
|
2492
|
+
resumeCursor: { threadId: this.nativeThreadId }
|
|
2493
|
+
};
|
|
2494
|
+
}
|
|
2495
|
+
async interruptTurn(turnId) {
|
|
2496
|
+
const childTurns = [...this.liveForeignTurns.entries()];
|
|
2497
|
+
const started = Date.now();
|
|
2498
|
+
await Promise.all(
|
|
2499
|
+
childTurns.slice(0, 8).map(async ([thread, childTurn]) => {
|
|
2500
|
+
if (Date.now() - started > 1e4) return;
|
|
2501
|
+
try {
|
|
2502
|
+
await Promise.race([
|
|
2503
|
+
this.rpc.request("turn/interrupt", { threadId: thread, turnId: childTurn }, 0),
|
|
2504
|
+
sleep(3e3)
|
|
2505
|
+
]);
|
|
2506
|
+
} catch {
|
|
2507
|
+
}
|
|
2508
|
+
})
|
|
2509
|
+
);
|
|
2510
|
+
if (!this.nativeThreadId) return;
|
|
2511
|
+
const target = turnId ?? this.activeTurnId;
|
|
2512
|
+
if (!target) return;
|
|
2513
|
+
await this.rpc.request("turn/interrupt", { threadId: this.nativeThreadId, turnId: target }, 0).catch(() => void 0);
|
|
2514
|
+
}
|
|
2515
|
+
async compactContext() {
|
|
2516
|
+
if (!this.nativeThreadId) return;
|
|
2517
|
+
await this.rpc.request("thread/compact", { threadId: this.nativeThreadId }, 0).catch(() => void 0);
|
|
2518
|
+
}
|
|
2519
|
+
async uploadFeedback(reason) {
|
|
2520
|
+
const result = await this.rpc.request(
|
|
2521
|
+
"feedback/upload",
|
|
2522
|
+
{
|
|
2523
|
+
threadId: this.nativeThreadId,
|
|
2524
|
+
...reason ? { reason } : {}
|
|
2525
|
+
},
|
|
2526
|
+
0
|
|
2527
|
+
);
|
|
2528
|
+
const id = (isRecord3(result) ? asString(result["feedbackId"]) : null) ?? `codex-feedback-${Date.now()}`;
|
|
2529
|
+
return { feedbackId: id };
|
|
2530
|
+
}
|
|
2531
|
+
async readThread() {
|
|
2532
|
+
if (!this.nativeThreadId) return { threadId: this.threadId, turns: this.turns };
|
|
2533
|
+
try {
|
|
2534
|
+
const result = await this.rpc.request(
|
|
2535
|
+
"thread/read",
|
|
2536
|
+
{
|
|
2537
|
+
threadId: this.nativeThreadId,
|
|
2538
|
+
includeTurns: true
|
|
2539
|
+
},
|
|
2540
|
+
0
|
|
2541
|
+
);
|
|
2542
|
+
const thread = isRecord3(result) && isRecord3(result["thread"]) ? result["thread"] : null;
|
|
2543
|
+
const turnsRaw = thread && Array.isArray(thread["turns"]) ? thread["turns"] : [];
|
|
2544
|
+
const turns = turnsRaw.flatMap((turn) => {
|
|
2545
|
+
if (!isRecord3(turn)) return [];
|
|
2546
|
+
const id = asString(turn["id"]);
|
|
2547
|
+
if (!id) return [];
|
|
2548
|
+
return [{ id, items: Array.isArray(turn["items"]) ? turn["items"] : [] }];
|
|
2549
|
+
});
|
|
2550
|
+
this.turns.splice(0, this.turns.length, ...turns);
|
|
2551
|
+
} catch (error) {
|
|
2552
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
2553
|
+
if (!isBenignErrorLog(message)) this.host.log.warn(message, { threadId: this.threadId });
|
|
2554
|
+
}
|
|
2555
|
+
return { threadId: this.threadId, turns: this.turns };
|
|
2556
|
+
}
|
|
2557
|
+
settlePendingApprovals(decision) {
|
|
2558
|
+
for (const [requestId, pending] of this.pendingApprovals) {
|
|
2559
|
+
pending.settle({ decision });
|
|
2560
|
+
this.pendingApprovals.delete(requestId);
|
|
2561
|
+
}
|
|
2562
|
+
}
|
|
2563
|
+
settlePendingUserInputs(result) {
|
|
2564
|
+
for (const [requestId, pending] of this.pendingUserInputs) {
|
|
2565
|
+
pending.settle(result);
|
|
2566
|
+
this.pendingUserInputs.delete(requestId);
|
|
2567
|
+
}
|
|
2568
|
+
}
|
|
2569
|
+
async close() {
|
|
2570
|
+
if (this.closeCompleted) return;
|
|
2571
|
+
this.closed = true;
|
|
2572
|
+
this.failPendingNativeWrites("session closed before write confirmation");
|
|
2573
|
+
this.settlePendingApprovals("cancel");
|
|
2574
|
+
this.settlePendingUserInputs({});
|
|
2575
|
+
this.declineRelay.clear(this.threadId);
|
|
2576
|
+
this.grants.clear();
|
|
2577
|
+
try {
|
|
2578
|
+
await this.rpc.close();
|
|
2579
|
+
} catch {
|
|
2580
|
+
}
|
|
2581
|
+
this.closeCompleted = true;
|
|
2582
|
+
this.emit({
|
|
2583
|
+
eventId: `codex-close-${this.threadId}`,
|
|
2584
|
+
provider: CODEX_DRIVER_KIND,
|
|
2585
|
+
threadId: this.threadId,
|
|
2586
|
+
createdAt: nowIso(),
|
|
2587
|
+
type: "session.exited",
|
|
2588
|
+
payload: {}
|
|
2589
|
+
});
|
|
2590
|
+
}
|
|
2591
|
+
};
|
|
2592
|
+
|
|
2593
|
+
// src/sessions.ts
|
|
2594
|
+
import * as NodeFS2 from "node:fs";
|
|
2595
|
+
import * as NodePath4 from "node:path";
|
|
2596
|
+
import * as NodeReadline from "node:readline";
|
|
2597
|
+
var EXTENDED_LENGTH_UNC = "//?/UNC/";
|
|
2598
|
+
var EXTENDED_LENGTH = "//?/";
|
|
2599
|
+
function normalize(value, caseInsensitive) {
|
|
2600
|
+
let unified = value.trim().replace(/\\/gu, "/");
|
|
2601
|
+
const upper = unified.toUpperCase();
|
|
2602
|
+
if (upper.startsWith(EXTENDED_LENGTH_UNC)) {
|
|
2603
|
+
unified = `//${unified.slice(EXTENDED_LENGTH_UNC.length)}`;
|
|
2604
|
+
} else if (upper.startsWith(EXTENDED_LENGTH)) {
|
|
2605
|
+
unified = unified.slice(EXTENDED_LENGTH.length);
|
|
2606
|
+
}
|
|
2607
|
+
const isUnc = unified.startsWith("//");
|
|
2608
|
+
unified = unified.replace(/\/+/gu, "/").replace(/\/+$/u, "");
|
|
2609
|
+
if (isUnc) unified = `/${unified}`;
|
|
2610
|
+
return caseInsensitive ? unified.toLowerCase() : unified;
|
|
2611
|
+
}
|
|
2612
|
+
function isWithinDirectory(candidate, root, caseInsensitive) {
|
|
2613
|
+
const normalizedRoot = normalize(root, caseInsensitive);
|
|
2614
|
+
if (normalizedRoot.length === 0 || normalizedRoot === "/") return false;
|
|
2615
|
+
const normalizedCandidate = normalize(candidate, caseInsensitive);
|
|
2616
|
+
return normalizedCandidate === normalizedRoot || normalizedCandidate.startsWith(`${normalizedRoot}/`);
|
|
2617
|
+
}
|
|
2618
|
+
var PAGE_LIMIT = 50;
|
|
2619
|
+
var MAX_PAGES = 10;
|
|
2620
|
+
var MAX_ROWS = 50;
|
|
2621
|
+
var PREVIEW_LIMIT = 200;
|
|
2622
|
+
function truncate(value) {
|
|
2623
|
+
const trimmed = value?.trim() ?? "";
|
|
2624
|
+
if (trimmed.length === 0) return null;
|
|
2625
|
+
return trimmed.length <= PREVIEW_LIMIT ? trimmed : `${trimmed.slice(0, PREVIEW_LIMIT)}\u2026`;
|
|
2626
|
+
}
|
|
2627
|
+
var nativeModel = (value) => {
|
|
2628
|
+
const model = asString(value)?.trim();
|
|
2629
|
+
return model && model !== "<synthetic>" ? model : null;
|
|
2630
|
+
};
|
|
2631
|
+
function readListPage(response) {
|
|
2632
|
+
if (!response || typeof response !== "object") return { threads: [], nextCursor: null };
|
|
2633
|
+
const typed = response;
|
|
2634
|
+
const rows = Array.isArray(typed.data) ? typed.data : [];
|
|
2635
|
+
const threads = [];
|
|
2636
|
+
for (const row of rows) {
|
|
2637
|
+
if (!isRecord3(row)) continue;
|
|
2638
|
+
if (typeof row.id !== "string" || typeof row.cwd !== "string") continue;
|
|
2639
|
+
threads.push({
|
|
2640
|
+
id: row.id,
|
|
2641
|
+
cwd: row.cwd,
|
|
2642
|
+
name: asString(row.name),
|
|
2643
|
+
preview: asString(row.preview),
|
|
2644
|
+
model: asString(row.model),
|
|
2645
|
+
updatedAt: typeof row.updatedAt === "number" ? row.updatedAt : 0
|
|
2646
|
+
});
|
|
2647
|
+
}
|
|
2648
|
+
return { threads, nextCursor: asString(typed.nextCursor) };
|
|
2649
|
+
}
|
|
2650
|
+
function readThreadTurns(response) {
|
|
2651
|
+
if (!isRecord3(response)) return [];
|
|
2652
|
+
const thread = isRecord3(response.thread) ? response.thread : null;
|
|
2653
|
+
if (!thread) return [];
|
|
2654
|
+
const turns = Array.isArray(thread.turns) ? thread.turns : [];
|
|
2655
|
+
return turns.flatMap((turn) => {
|
|
2656
|
+
if (!isRecord3(turn)) return [];
|
|
2657
|
+
return [
|
|
2658
|
+
{
|
|
2659
|
+
...typeof turn.id === "string" && turn.id.length > 0 ? { id: turn.id } : {},
|
|
2660
|
+
...typeof turn.model === "string" && turn.model.length > 0 ? { model: turn.model } : {},
|
|
2661
|
+
startedAt: typeof turn.startedAt === "number" ? turn.startedAt : null,
|
|
2662
|
+
items: Array.isArray(turn.items) ? turn.items : []
|
|
2663
|
+
}
|
|
2664
|
+
];
|
|
2665
|
+
});
|
|
2666
|
+
}
|
|
2667
|
+
function itemText(item) {
|
|
2668
|
+
if (!isRecord3(item)) return void 0;
|
|
2669
|
+
const type = (asString(item.type) ?? "").toLowerCase();
|
|
2670
|
+
const text = asString(item.text) ?? asString(item.content) ?? (Array.isArray(item.content) ? item.content.map((entry) => isRecord3(entry) ? asString(entry.text) : null).filter((entry) => typeof entry === "string").join("\n") : null);
|
|
2671
|
+
if (!text || text.trim().length === 0) return void 0;
|
|
2672
|
+
if (type.includes("user")) return { role: "user", text };
|
|
2673
|
+
if (type.includes("agent") || type.includes("assistant")) return { role: "assistant", text };
|
|
2674
|
+
if (type.includes("system")) return { role: "system", text };
|
|
2675
|
+
return void 0;
|
|
2676
|
+
}
|
|
2677
|
+
async function scanCodexSessions(input) {
|
|
2678
|
+
const threads = [];
|
|
2679
|
+
let cursor = null;
|
|
2680
|
+
for (let page = 0; page < MAX_PAGES && threads.length < MAX_ROWS; page += 1) {
|
|
2681
|
+
const listed = readListPage(
|
|
2682
|
+
await input.client.request("thread/list", {
|
|
2683
|
+
limit: PAGE_LIMIT,
|
|
2684
|
+
sortKey: "updated_at",
|
|
2685
|
+
sortDirection: "desc",
|
|
2686
|
+
...cursor === null ? {} : { cursor }
|
|
2687
|
+
})
|
|
2688
|
+
);
|
|
2689
|
+
for (const thread of listed.threads) {
|
|
2690
|
+
if (!isWithinDirectory(thread.cwd, input.directory, input.caseInsensitive)) continue;
|
|
2691
|
+
threads.push(thread);
|
|
2692
|
+
if (threads.length >= MAX_ROWS) break;
|
|
2693
|
+
}
|
|
2694
|
+
if (listed.nextCursor === null || listed.nextCursor.length === 0) break;
|
|
2695
|
+
cursor = listed.nextCursor;
|
|
2696
|
+
}
|
|
2697
|
+
const rows = [];
|
|
2698
|
+
for (const thread of threads) {
|
|
2699
|
+
const turnCount = await input.client.request("thread/read", { threadId: thread.id, includeTurns: true }).then((response) => readThreadTurns(response).length).catch(() => 0);
|
|
2700
|
+
rows.push({
|
|
2701
|
+
nativeSessionId: thread.id,
|
|
2702
|
+
cwd: thread.cwd,
|
|
2703
|
+
title: truncate(thread.name),
|
|
2704
|
+
preview: truncate(thread.preview),
|
|
2705
|
+
model: truncate(thread.model),
|
|
2706
|
+
turnCount,
|
|
2707
|
+
updatedAt: thread.updatedAt * 1e3,
|
|
2708
|
+
availability: "importable",
|
|
2709
|
+
openElsewhere: false
|
|
2710
|
+
});
|
|
2711
|
+
}
|
|
2712
|
+
return rows;
|
|
2713
|
+
}
|
|
2714
|
+
async function readCodexSession(input) {
|
|
2715
|
+
const response = await input.client.request("thread/read", {
|
|
2716
|
+
threadId: input.threadId,
|
|
2717
|
+
includeTurns: true
|
|
2718
|
+
});
|
|
2719
|
+
const record = isRecord3(response) ? response : null;
|
|
2720
|
+
const thread = record && isRecord3(record.thread) ? record.thread : null;
|
|
2721
|
+
const path = input.caseInsensitive ? NodePath4.win32 : NodePath4;
|
|
2722
|
+
const sameDirectory = (left, right) => isWithinDirectory(path.resolve(left), path.resolve(right), input.caseInsensitive) && isWithinDirectory(path.resolve(right), path.resolve(left), input.caseInsensitive);
|
|
2723
|
+
if (!isRecord3(thread) || thread.id !== input.threadId || typeof thread.cwd !== "string" || !path.isAbsolute(thread.cwd) || !path.isAbsolute(input.cwd) || !sameDirectory(thread.cwd, input.cwd)) {
|
|
2724
|
+
throw new Error("The native Codex session does not match the requested session and directory.");
|
|
2725
|
+
}
|
|
2726
|
+
const model = nativeModel(thread.model) ?? await readRolloutModel(
|
|
2727
|
+
{
|
|
2728
|
+
threadId: input.threadId,
|
|
2729
|
+
cwd: input.cwd,
|
|
2730
|
+
homePath: input.homePath,
|
|
2731
|
+
caseInsensitive: input.caseInsensitive
|
|
2732
|
+
},
|
|
2733
|
+
thread.path,
|
|
2734
|
+
sameDirectory
|
|
2735
|
+
);
|
|
2736
|
+
const messages = [];
|
|
2737
|
+
for (const turn of readThreadTurns(response)) {
|
|
2738
|
+
for (const item of turn.items) {
|
|
2739
|
+
const mapped = itemText(item);
|
|
2740
|
+
if (mapped) {
|
|
2741
|
+
messages.push({
|
|
2742
|
+
...mapped,
|
|
2743
|
+
...typeof turn.startedAt === "number" ? { createdAt: turn.startedAt * 1e3 } : {}
|
|
2744
|
+
});
|
|
2745
|
+
}
|
|
2746
|
+
}
|
|
2747
|
+
}
|
|
2748
|
+
return {
|
|
2749
|
+
nativeSessionId: input.threadId,
|
|
2750
|
+
messages,
|
|
2751
|
+
resumeCursor: { threadId: input.threadId },
|
|
2752
|
+
model
|
|
2753
|
+
};
|
|
2754
|
+
}
|
|
2755
|
+
async function readRolloutModel(input, rolloutPath, sameDirectory) {
|
|
2756
|
+
const path = input.caseInsensitive ? NodePath4.win32 : NodePath4;
|
|
2757
|
+
if (typeof rolloutPath !== "string" || !path.isAbsolute(rolloutPath) || path.extname(rolloutPath).toLowerCase() !== ".jsonl") {
|
|
2758
|
+
return null;
|
|
2759
|
+
}
|
|
2760
|
+
const resolved = path.resolve(rolloutPath);
|
|
2761
|
+
const roots = input.homePath ? [path.join(input.homePath, "sessions"), path.join(input.homePath, "archived_sessions")] : [];
|
|
2762
|
+
if (!roots.some((root) => isWithinDirectory(resolved, path.resolve(root), input.caseInsensitive))) {
|
|
2763
|
+
return null;
|
|
2764
|
+
}
|
|
2765
|
+
let verified = false;
|
|
2766
|
+
let model = null;
|
|
2767
|
+
try {
|
|
2768
|
+
const stream = NodeFS2.createReadStream(resolved, { encoding: "utf8" });
|
|
2769
|
+
const rl = NodeReadline.createInterface({ input: stream, crlfDelay: Infinity });
|
|
2770
|
+
for await (const line of rl) {
|
|
2771
|
+
if (!line.includes('"session_meta"') && !line.includes('"turn_context"')) continue;
|
|
2772
|
+
let record;
|
|
2773
|
+
try {
|
|
2774
|
+
record = isRecord3(JSON.parse(line)) ? JSON.parse(line) : null;
|
|
2775
|
+
} catch {
|
|
2776
|
+
continue;
|
|
2777
|
+
}
|
|
2778
|
+
const payload = isRecord3(record?.payload) ? record.payload : null;
|
|
2779
|
+
if (!record || !payload) continue;
|
|
2780
|
+
if (record.type === "session_meta" && !verified) {
|
|
2781
|
+
if ((payload.id ?? payload.session_id) !== input.threadId || typeof payload.cwd !== "string" || !path.isAbsolute(payload.cwd) || !sameDirectory(payload.cwd, input.cwd)) {
|
|
2782
|
+
return null;
|
|
2783
|
+
}
|
|
2784
|
+
verified = true;
|
|
2785
|
+
} else if (record.type === "turn_context" && verified) {
|
|
2786
|
+
model = nativeModel(payload.model) ?? model;
|
|
2787
|
+
}
|
|
2788
|
+
}
|
|
2789
|
+
} catch {
|
|
2790
|
+
return null;
|
|
2791
|
+
}
|
|
2792
|
+
return model;
|
|
2793
|
+
}
|
|
2794
|
+
|
|
2795
|
+
// src/signIn.ts
|
|
2796
|
+
var DEVICE_CODE_URL = /https?:\/\/\S+/;
|
|
2797
|
+
var DEVICE_CODE_VALUE = /\b([A-Z0-9]{4,}(?:-[A-Z0-9]{4,})+)\b/;
|
|
2798
|
+
var deviceCodeUrl = (output) => DEVICE_CODE_URL.exec(output)?.[0];
|
|
2799
|
+
var deviceCodeValue = (output) => DEVICE_CODE_VALUE.exec(output)?.[1];
|
|
2800
|
+
async function collectText(stream, into) {
|
|
2801
|
+
const decoder = new TextDecoder();
|
|
2802
|
+
for await (const chunk of stream) {
|
|
2803
|
+
into.text += decoder.decode(chunk, { stream: true });
|
|
2804
|
+
}
|
|
2805
|
+
}
|
|
2806
|
+
function accountToAuth(account) {
|
|
2807
|
+
switch (account.state) {
|
|
2808
|
+
case "signed-in":
|
|
2809
|
+
case "expired":
|
|
2810
|
+
return {
|
|
2811
|
+
status: "authenticated",
|
|
2812
|
+
...account.email === void 0 ? {} : { email: account.email }
|
|
2813
|
+
};
|
|
2814
|
+
case "signed-out":
|
|
2815
|
+
return { status: "unauthenticated" };
|
|
2816
|
+
case "unreadable":
|
|
2817
|
+
return { status: "unknown" };
|
|
2818
|
+
}
|
|
2819
|
+
}
|
|
2820
|
+
async function startCodexSignIn(input) {
|
|
2821
|
+
if (input.recipe.binaryPath.length === 0) {
|
|
2822
|
+
throw new Error("Codex program path is not configured");
|
|
2823
|
+
}
|
|
2824
|
+
const isolatedHome = accountHomeFromLayout(input.layout);
|
|
2825
|
+
const env = nativeLoginSpawnEnv(input.processEnv, isolatedHome);
|
|
2826
|
+
const owner = { bindingId: input.host.bindingId };
|
|
2827
|
+
const tryAppServer = async () => {
|
|
2828
|
+
const handle2 = await input.host.process.spawn({
|
|
2829
|
+
program: input.recipe.binaryPath,
|
|
2830
|
+
args: tokenizeLaunchArgs(input.recipe.launchArgs),
|
|
2831
|
+
owner,
|
|
2832
|
+
env
|
|
2833
|
+
});
|
|
2834
|
+
let completed;
|
|
2835
|
+
const rpc = await createCodexRpcClient(handle2, {
|
|
2836
|
+
onNotification: (notification) => {
|
|
2837
|
+
if (notification.method !== "account/login/completed") return;
|
|
2838
|
+
const params = isRecord3(notification.params) ? notification.params : {};
|
|
2839
|
+
const loginId = asString(params["loginId"]);
|
|
2840
|
+
completed = loginId ? { loginId } : {};
|
|
2841
|
+
},
|
|
2842
|
+
onServerRequest: () => void 0,
|
|
2843
|
+
onStderrLine: () => void 0
|
|
2844
|
+
});
|
|
2845
|
+
try {
|
|
2846
|
+
await rpc.request("initialize", CODEX_INITIALIZE_PARAMS);
|
|
2847
|
+
await rpc.notify("initialized");
|
|
2848
|
+
const started = await rpc.request("account/login/start", { type: "chatgptDeviceCode" });
|
|
2849
|
+
if (!isRecord3(started) || asString(started["type"]) !== "chatgptDeviceCode") {
|
|
2850
|
+
await rpc.close();
|
|
2851
|
+
return void 0;
|
|
2852
|
+
}
|
|
2853
|
+
const userCode2 = asString(started["userCode"]) ?? asString(started["user_code"]) ?? void 0;
|
|
2854
|
+
const verificationUrl2 = asString(started["verificationUrl"]) ?? asString(started["verification_uri"]) ?? asString(started["url"]) ?? void 0;
|
|
2855
|
+
const operationId2 = `codex-login-${Date.now()}`;
|
|
2856
|
+
return {
|
|
2857
|
+
operation: {
|
|
2858
|
+
operationId: operationId2,
|
|
2859
|
+
handle: handle2,
|
|
2860
|
+
close: async () => {
|
|
2861
|
+
void completed;
|
|
2862
|
+
await rpc.close();
|
|
2863
|
+
}
|
|
2864
|
+
},
|
|
2865
|
+
...userCode2 ? { userCode: userCode2 } : {},
|
|
2866
|
+
...verificationUrl2 ? { verificationUrl: verificationUrl2 } : {}
|
|
2867
|
+
};
|
|
2868
|
+
} catch {
|
|
2869
|
+
await rpc.close().catch(() => void 0);
|
|
2870
|
+
return void 0;
|
|
2871
|
+
}
|
|
2872
|
+
};
|
|
2873
|
+
const appServer = await tryAppServer();
|
|
2874
|
+
if (appServer) return appServer;
|
|
2875
|
+
const handle = await input.host.process.spawn({
|
|
2876
|
+
program: input.recipe.binaryPath,
|
|
2877
|
+
args: [...codexExecLaunchArgs(input.recipe.launchArgs), ...CODEX_CARD.deviceCodeSignInArgs],
|
|
2878
|
+
owner,
|
|
2879
|
+
env
|
|
2880
|
+
});
|
|
2881
|
+
const stdout = { text: "" };
|
|
2882
|
+
const stderr = { text: "" };
|
|
2883
|
+
void collectText(handle.stdout, stdout);
|
|
2884
|
+
void collectText(handle.stderr, stderr);
|
|
2885
|
+
const deadline = Date.now() + 3e3;
|
|
2886
|
+
while (Date.now() < deadline) {
|
|
2887
|
+
const combined2 = `${stdout.text}
|
|
2888
|
+
${stderr.text}`;
|
|
2889
|
+
if (deviceCodeUrl(combined2) && deviceCodeValue(combined2)) break;
|
|
2890
|
+
await new Promise((resolve2) => setTimeout(resolve2, 50));
|
|
2891
|
+
}
|
|
2892
|
+
const combined = `${stdout.text}
|
|
2893
|
+
${stderr.text}`;
|
|
2894
|
+
const operationId = `codex-login-${Date.now()}`;
|
|
2895
|
+
const userCode = deviceCodeValue(combined);
|
|
2896
|
+
const verificationUrl = deviceCodeUrl(combined);
|
|
2897
|
+
return {
|
|
2898
|
+
operation: {
|
|
2899
|
+
operationId,
|
|
2900
|
+
handle,
|
|
2901
|
+
close: async () => {
|
|
2902
|
+
try {
|
|
2903
|
+
await handle.closeStdin();
|
|
2904
|
+
} catch {
|
|
2905
|
+
}
|
|
2906
|
+
try {
|
|
2907
|
+
await handle.kill("SIGTERM");
|
|
2908
|
+
} catch {
|
|
2909
|
+
}
|
|
2910
|
+
}
|
|
2911
|
+
},
|
|
2912
|
+
...userCode ? { userCode } : {},
|
|
2913
|
+
...verificationUrl ? { verificationUrl } : {}
|
|
2914
|
+
};
|
|
2915
|
+
}
|
|
2916
|
+
async function verifyCodexSignIn(input) {
|
|
2917
|
+
const home = accountHomeFromLayout(input.layout);
|
|
2918
|
+
const auth = accountToAuth(readCodexAccount(home));
|
|
2919
|
+
if (input.recipe.binaryPath.length === 0) {
|
|
2920
|
+
return {
|
|
2921
|
+
installed: false,
|
|
2922
|
+
version: null,
|
|
2923
|
+
readiness: "error",
|
|
2924
|
+
auth: { status: "unknown" },
|
|
2925
|
+
detail: "Codex program path is not configured"
|
|
2926
|
+
};
|
|
2927
|
+
}
|
|
2928
|
+
const env = nativeLoginSpawnEnv(input.processEnv, home);
|
|
2929
|
+
const handle = await input.host.process.spawn({
|
|
2930
|
+
program: input.recipe.binaryPath,
|
|
2931
|
+
args: tokenizeLaunchArgs(input.recipe.launchArgs),
|
|
2932
|
+
owner: { bindingId: input.host.bindingId },
|
|
2933
|
+
env
|
|
2934
|
+
});
|
|
2935
|
+
const rpc = await createCodexRpcClient(handle, {
|
|
2936
|
+
onNotification: () => void 0,
|
|
2937
|
+
onServerRequest: () => void 0,
|
|
2938
|
+
onStderrLine: () => void 0
|
|
2939
|
+
});
|
|
2940
|
+
try {
|
|
2941
|
+
await rpc.request("initialize", CODEX_INITIALIZE_PARAMS, 15e3);
|
|
2942
|
+
await rpc.notify("initialized");
|
|
2943
|
+
const account = await rpc.request("account/read", { refreshToken: true }, 15e3);
|
|
2944
|
+
const type = isRecord3(account) ? asString(account["type"]) : null;
|
|
2945
|
+
if (type !== "chatgpt") {
|
|
2946
|
+
return {
|
|
2947
|
+
installed: true,
|
|
2948
|
+
version: null,
|
|
2949
|
+
readiness: "unchecked",
|
|
2950
|
+
auth: { status: "unauthenticated" }
|
|
2951
|
+
};
|
|
2952
|
+
}
|
|
2953
|
+
await rpc.request("model/list", {}, 15e3).catch(() => void 0);
|
|
2954
|
+
return {
|
|
2955
|
+
installed: true,
|
|
2956
|
+
version: null,
|
|
2957
|
+
readiness: "ready",
|
|
2958
|
+
auth
|
|
2959
|
+
};
|
|
2960
|
+
} catch {
|
|
2961
|
+
return {
|
|
2962
|
+
installed: true,
|
|
2963
|
+
version: null,
|
|
2964
|
+
readiness: "unchecked",
|
|
2965
|
+
auth: { status: "unknown" }
|
|
2966
|
+
};
|
|
2967
|
+
} finally {
|
|
2968
|
+
await rpc.close().catch(() => void 0);
|
|
2969
|
+
}
|
|
2970
|
+
}
|
|
2971
|
+
|
|
2972
|
+
// src/textGeneration.ts
|
|
2973
|
+
import * as NodeFSP2 from "node:fs/promises";
|
|
2974
|
+
import * as NodeOS3 from "node:os";
|
|
2975
|
+
import * as NodePath5 from "node:path";
|
|
2976
|
+
var CODEX_TIMEOUT_MS = 18e4;
|
|
2977
|
+
var EARLIER_CONTENT_TRUNCATION_MARKER = "[Earlier content truncated]\n\n";
|
|
2978
|
+
function limitSection(value, maxChars) {
|
|
2979
|
+
if (value.length <= maxChars) return value;
|
|
2980
|
+
return `${value.slice(0, maxChars)}
|
|
2981
|
+
|
|
2982
|
+
[truncated]`;
|
|
2983
|
+
}
|
|
2984
|
+
function sanitizeCommitSubject(raw) {
|
|
2985
|
+
const singleLine = raw.trim().split(/\r?\n/g)[0]?.trim() ?? "";
|
|
2986
|
+
const withoutTrailingPeriod = singleLine.replace(/[.]+$/g, "").trim();
|
|
2987
|
+
if (withoutTrailingPeriod.length === 0) return "Update project files";
|
|
2988
|
+
return withoutTrailingPeriod.length <= 72 ? withoutTrailingPeriod : withoutTrailingPeriod.slice(0, 72).trimEnd();
|
|
2989
|
+
}
|
|
2990
|
+
function sanitizePrTitle(raw) {
|
|
2991
|
+
const singleLine = raw.trim().split(/\r?\n/g)[0]?.trim() ?? "";
|
|
2992
|
+
return singleLine.length > 0 ? singleLine : "Update project changes";
|
|
2993
|
+
}
|
|
2994
|
+
function unwrapNestedTitleObject(raw) {
|
|
2995
|
+
const trimmed = raw.trim();
|
|
2996
|
+
if (!trimmed.startsWith("{") || !trimmed.endsWith("}")) return raw;
|
|
2997
|
+
try {
|
|
2998
|
+
const parsed = JSON.parse(trimmed);
|
|
2999
|
+
if (isRecord3(parsed) && typeof parsed.title === "string") return parsed.title;
|
|
3000
|
+
} catch {
|
|
3001
|
+
}
|
|
3002
|
+
return raw;
|
|
3003
|
+
}
|
|
3004
|
+
function sanitizeThreadTitle(raw) {
|
|
3005
|
+
const normalized = unwrapNestedTitleObject(raw).trim().split(/\r?\n/g)[0]?.trim().replace(/^['"`]+|['"`]+$/g, "").trim().replace(/\s+/g, " ");
|
|
3006
|
+
if (!normalized || normalized.trim().length === 0) return "New thread";
|
|
3007
|
+
if (normalized.length <= 50) return normalized;
|
|
3008
|
+
return `${normalized.slice(0, 47).trimEnd()}...`;
|
|
3009
|
+
}
|
|
3010
|
+
function sanitizeBranchFragment(raw) {
|
|
3011
|
+
const normalized = raw.trim().toLowerCase().replace(/['"`]/g, "").replace(/^[./\s_-]+|[./\s_-]+$/g, "");
|
|
3012
|
+
const branchFragment = normalized.replace(/[^a-z0-9/_-]+/g, "-").replace(/\/+/g, "/").replace(/-+/g, "-").replace(/^[./_-]+|[./_-]+$/g, "").slice(0, 64).replace(/[./_-]+$/g, "");
|
|
3013
|
+
return branchFragment.length > 0 ? branchFragment : "update";
|
|
3014
|
+
}
|
|
3015
|
+
function sanitizeFeatureBranchName(raw) {
|
|
3016
|
+
const sanitized = sanitizeBranchFragment(raw);
|
|
3017
|
+
if (sanitized.includes("/")) {
|
|
3018
|
+
return sanitized.startsWith("feature/") ? sanitized : `feature/${sanitized}`;
|
|
3019
|
+
}
|
|
3020
|
+
return `feature/${sanitized}`;
|
|
3021
|
+
}
|
|
3022
|
+
function policyInstruction(instruction) {
|
|
3023
|
+
const trimmed = instruction?.trim();
|
|
3024
|
+
return trimmed ? ["", "Additional instructions:", limitSection(trimmed, 4e3)] : [];
|
|
3025
|
+
}
|
|
3026
|
+
function readStringField(record, key) {
|
|
3027
|
+
return asString(record[key]) ?? "";
|
|
3028
|
+
}
|
|
3029
|
+
function schemaObject(properties, required) {
|
|
3030
|
+
return { type: "object", properties, required, additionalProperties: false };
|
|
3031
|
+
}
|
|
3032
|
+
var COMMIT_SCHEMA = schemaObject({ subject: { type: "string" }, body: { type: "string" } }, [
|
|
3033
|
+
"subject",
|
|
3034
|
+
"body"
|
|
3035
|
+
]);
|
|
3036
|
+
var COMMIT_WITH_BRANCH_SCHEMA = schemaObject(
|
|
3037
|
+
{ subject: { type: "string" }, body: { type: "string" }, branch: { type: "string" } },
|
|
3038
|
+
["subject", "body", "branch"]
|
|
3039
|
+
);
|
|
3040
|
+
var PR_SCHEMA = schemaObject({ title: { type: "string" }, body: { type: "string" } }, [
|
|
3041
|
+
"title",
|
|
3042
|
+
"body"
|
|
3043
|
+
]);
|
|
3044
|
+
var BRANCH_SCHEMA = schemaObject({ branch: { type: "string" } }, ["branch"]);
|
|
3045
|
+
var TITLE_SCHEMA = schemaObject({ title: { type: "string" } }, ["title"]);
|
|
3046
|
+
var HANDOVER_SCHEMA = schemaObject({ summary: { type: "string" } }, ["summary"]);
|
|
3047
|
+
var INITIAL_THREAD_TITLE_PROMPT = `Generate a title that will help the user recognize this Ycodium thread weeks later.
|
|
3048
|
+
Return JSON with exactly one key: title.
|
|
3049
|
+
|
|
3050
|
+
Before answering, silently reduce the request to:
|
|
3051
|
+
- Subject: What system, feature, or problem is this really about?
|
|
3052
|
+
- Outcome: What does the user ultimately want to understand or change?
|
|
3053
|
+
- Incidental instructions: What only describes how the agent should do the work?
|
|
3054
|
+
|
|
3055
|
+
Title the subject and outcome. Discard incidental instructions.
|
|
3056
|
+
|
|
3057
|
+
Editorial rules:
|
|
3058
|
+
- 3-8 words, fewer than 40 characters.
|
|
3059
|
+
- Use a compact noun phrase or clear action phrase.
|
|
3060
|
+
- Capture the umbrella goal when the request lists several symptoms or steps.
|
|
3061
|
+
- Name the product change, not the mock, plan, report, branch, or PR used to produce it.
|
|
3062
|
+
- Models, subagents, tools, output formats, and monitoring instructions do not belong in the title unless they are themselves the topic.
|
|
3063
|
+
- For reviews, name what is being reviewed and the relevant concern. Avoid generic titles such as "Review PR 123" when linked or attached context reveals the subject.
|
|
3064
|
+
- For research, name the question domain rather than the requested research process.
|
|
3065
|
+
- Do not claim the work is complete.
|
|
3066
|
+
- Do not copy and truncate the user's message.
|
|
3067
|
+
- Avoid project names already visible in the UI, quotes, labels, filler, and trailing punctuation.
|
|
3068
|
+
- Use attached images as primary context for UI issues.
|
|
3069
|
+
- When a URL or attachment is the only source of the subject, use available tools to inspect it. If it cannot be resolved, remain accurate rather than guessing.`;
|
|
3070
|
+
function regenerateThreadTitlePrompt(previousTitle) {
|
|
3071
|
+
return `Regenerate the title for an existing Ycodium thread so the user can recognize it weeks later.
|
|
3072
|
+
The previous title was ${JSON.stringify(previousTitle)}.
|
|
3073
|
+
Return JSON with exactly one key: title.
|
|
3074
|
+
|
|
3075
|
+
Determine the title in this order:
|
|
3076
|
+
1. Read the USER messages first. Identify the latest explicit durable goal. The original subject remains the subject until the user clearly changes what the thread is about.
|
|
3077
|
+
2. Use ASSISTANT messages to resolve vague links, unnamed code, and discovered product nouns. Do not promote one assistant finding into the thread subject unless the user adopts it as a new goal.
|
|
3078
|
+
3. Compare that subject with the previous title. Preserve accurate scope words, especially when earlier content is truncated. Replace the previous title when it is generic, artifact-based, a completion update, or contradicted by the thread.
|
|
3079
|
+
4. Title the durable subject and desired outcome, not the current workflow state.
|
|
3080
|
+
|
|
3081
|
+
Editorial rules:
|
|
3082
|
+
- 3-8 words, fewer than 40 characters.
|
|
3083
|
+
- Use a compact noun phrase or clear action phrase.
|
|
3084
|
+
- Preserve the umbrella subject when later messages focus on one finding, provider, platform, or implementation detail.
|
|
3085
|
+
- A thread progressing through research, planning, implementation, review, CI, merge, and monitoring has usually not changed subjects.
|
|
3086
|
+
- Ignore deliverables and operations such as mocks, plans, HTML, branches, PRs, tests, CI, commits, merging, and monitoring unless they are the actual topic.
|
|
3087
|
+
- Models, subagents, tools, output formats, and monitoring instructions do not belong in the title unless they are themselves the topic.
|
|
3088
|
+
- Treat final operational follow-ups and assistant completion summaries as weak evidence of subject.
|
|
3089
|
+
- For reviews, name the reviewed feature or system and its durable concern, not one finding from the review.
|
|
3090
|
+
- For research, name the question domain rather than the research process.
|
|
3091
|
+
- Do not claim the work is complete.
|
|
3092
|
+
- Do not copy and truncate a thread message.
|
|
3093
|
+
- Avoid project names already visible in the UI, PR numbers, quotes, labels, filler, and trailing punctuation.
|
|
3094
|
+
- Use attached images as primary context for UI issues.
|
|
3095
|
+
- When a URL or attachment is the only source of the subject, use available tools to inspect it. If it cannot be resolved, remain accurate rather than guessing.
|
|
3096
|
+
- Return a meaningfully improved title, not a cosmetic paraphrase of the previous title.`;
|
|
3097
|
+
}
|
|
3098
|
+
function preserveMessageEnd(message) {
|
|
3099
|
+
const alreadyTruncated = message.startsWith(EARLIER_CONTENT_TRUNCATION_MARKER);
|
|
3100
|
+
const contents = alreadyTruncated ? message.slice(EARLIER_CONTENT_TRUNCATION_MARKER.length) : message;
|
|
3101
|
+
if (!alreadyTruncated && contents.length <= 8e3) return contents;
|
|
3102
|
+
return `${EARLIER_CONTENT_TRUNCATION_MARKER}${contents.slice(-8e3)}`;
|
|
3103
|
+
}
|
|
3104
|
+
function absoluteImagePaths(attachments) {
|
|
3105
|
+
if (!Array.isArray(attachments)) return [];
|
|
3106
|
+
const paths = [];
|
|
3107
|
+
for (const attachment of attachments) {
|
|
3108
|
+
if (!isRecord3(attachment) || attachment.type !== "image") continue;
|
|
3109
|
+
const candidate = asString(attachment.path) ?? asString(attachment.filePath) ?? asString(attachment.savedPath);
|
|
3110
|
+
if (candidate && NodePath5.isAbsolute(candidate)) paths.push(candidate);
|
|
3111
|
+
}
|
|
3112
|
+
return paths;
|
|
3113
|
+
}
|
|
3114
|
+
async function collectStream(stream) {
|
|
3115
|
+
const decoder = new TextDecoder();
|
|
3116
|
+
let out = "";
|
|
3117
|
+
for await (const chunk of stream) out += decoder.decode(chunk, { stream: true });
|
|
3118
|
+
return out;
|
|
3119
|
+
}
|
|
3120
|
+
function createCodexTextGeneration(input) {
|
|
3121
|
+
const run = async (operation, cwd, prompt, schema, modelSelection, launch, imagePaths, signal) => {
|
|
3122
|
+
if (input.recipe.binaryPath.length === 0) {
|
|
3123
|
+
throw new Error("Codex CLI is required but not available on PATH.");
|
|
3124
|
+
}
|
|
3125
|
+
const tmp = await NodeFSP2.mkdtemp(NodePath5.join(NodeOS3.tmpdir(), "ycodium-codex-tg-"));
|
|
3126
|
+
const schemaPath = NodePath5.join(tmp, "schema.json");
|
|
3127
|
+
const outputPath = NodePath5.join(tmp, "output.json");
|
|
3128
|
+
await NodeFSP2.writeFile(schemaPath, JSON.stringify(schema));
|
|
3129
|
+
await NodeFSP2.writeFile(outputPath, "");
|
|
3130
|
+
const launchRecord = isRecord3(launch) ? launch : {};
|
|
3131
|
+
const processEnv = isRecord3(launchRecord.processEnv) ? Object.fromEntries(
|
|
3132
|
+
Object.entries(launchRecord.processEnv).filter(
|
|
3133
|
+
(entry) => typeof entry[1] === "string"
|
|
3134
|
+
)
|
|
3135
|
+
) : {};
|
|
3136
|
+
const overlay = Array.isArray(launchRecord.launchArgOverrides) && launchRecord.launchArgOverrides.length > 0 ? encodeCodexConfigOverrides(
|
|
3137
|
+
launchRecord.launchArgOverrides.flatMap((entry) => {
|
|
3138
|
+
if (!isRecord3(entry) || typeof entry.key !== "string") return [];
|
|
3139
|
+
const value = entry.value;
|
|
3140
|
+
if (typeof value !== "string" && typeof value !== "boolean") return [];
|
|
3141
|
+
return [{ key: entry.key, value }];
|
|
3142
|
+
})
|
|
3143
|
+
) : "";
|
|
3144
|
+
const launchArgs = [input.recipe.launchArgs, overlay].filter((value) => value.length > 0).join(" ");
|
|
3145
|
+
const reasoningEffort = modelSelectionStringOption(modelSelection, "reasoningEffort") ?? DEFAULT_TEXT_GENERATION_REASONING_EFFORT;
|
|
3146
|
+
const serviceTier = modelSelectionStringOption(modelSelection, "serviceTier");
|
|
3147
|
+
const model = asString(launchRecord.requestedModelId) ?? modelIdFromSelection(modelSelection) ?? "gpt-5.6-luna";
|
|
3148
|
+
const env = {
|
|
3149
|
+
...processEnv,
|
|
3150
|
+
...input.recipe.environment,
|
|
3151
|
+
...input.layout.effectiveHomePath ? { CODEX_HOME: input.layout.effectiveHomePath } : input.recipe.homePath.trim() ? { CODEX_HOME: expandHomePath(input.recipe.homePath) } : {}
|
|
3152
|
+
};
|
|
3153
|
+
if (input.recipe.environment[CODEX_INSTANCE_API_KEY_ENV]) {
|
|
3154
|
+
env[CODEX_INSTANCE_API_KEY_ENV] = input.recipe.environment[CODEX_INSTANCE_API_KEY_ENV];
|
|
3155
|
+
}
|
|
3156
|
+
const args = [
|
|
3157
|
+
"exec",
|
|
3158
|
+
...codexExecLaunchArgs(launchArgs),
|
|
3159
|
+
"--ephemeral",
|
|
3160
|
+
"--skip-git-repo-check",
|
|
3161
|
+
"-s",
|
|
3162
|
+
"read-only",
|
|
3163
|
+
"--model",
|
|
3164
|
+
model,
|
|
3165
|
+
"--config",
|
|
3166
|
+
`model_reasoning_effort="${reasoningEffort}"`,
|
|
3167
|
+
...serviceTier ? ["--config", `service_tier="${serviceTier}"`] : [],
|
|
3168
|
+
"--output-schema",
|
|
3169
|
+
schemaPath,
|
|
3170
|
+
"--output-last-message",
|
|
3171
|
+
outputPath,
|
|
3172
|
+
...imagePaths.flatMap((imagePath) => ["--image", imagePath]),
|
|
3173
|
+
"-"
|
|
3174
|
+
];
|
|
3175
|
+
const handle = await input.host.process.spawn({
|
|
3176
|
+
program: input.recipe.binaryPath,
|
|
3177
|
+
args,
|
|
3178
|
+
cwd,
|
|
3179
|
+
env,
|
|
3180
|
+
owner: { bindingId: input.host.bindingId }
|
|
3181
|
+
});
|
|
3182
|
+
const onAbort = () => {
|
|
3183
|
+
void handle.kill("SIGTERM").catch(() => void 0);
|
|
3184
|
+
};
|
|
3185
|
+
signal?.addEventListener("abort", onAbort);
|
|
3186
|
+
let timer;
|
|
3187
|
+
try {
|
|
3188
|
+
await handle.writeStdin(prompt);
|
|
3189
|
+
await handle.closeStdin();
|
|
3190
|
+
const timedOut = await Promise.race([
|
|
3191
|
+
handle.exited.then(() => false),
|
|
3192
|
+
new Promise((resolve2) => {
|
|
3193
|
+
timer = setTimeout(() => resolve2(true), CODEX_TIMEOUT_MS);
|
|
3194
|
+
})
|
|
3195
|
+
]);
|
|
3196
|
+
if (timedOut) {
|
|
3197
|
+
await handle.kill("SIGTERM").catch(() => void 0);
|
|
3198
|
+
throw new Error("Codex CLI request timed out.");
|
|
3199
|
+
}
|
|
3200
|
+
const [stdout, stderr, exitCode] = await Promise.all([
|
|
3201
|
+
collectStream(handle.stdout),
|
|
3202
|
+
collectStream(handle.stderr),
|
|
3203
|
+
handle.exited
|
|
3204
|
+
]);
|
|
3205
|
+
if (exitCode !== 0) {
|
|
3206
|
+
const detail = stderr.trim() || stdout.trim();
|
|
3207
|
+
throw new Error(
|
|
3208
|
+
detail.length > 0 ? `Codex CLI command failed: ${detail}` : `Codex CLI command failed with code ${String(exitCode)}.`
|
|
3209
|
+
);
|
|
3210
|
+
}
|
|
3211
|
+
const raw = await NodeFSP2.readFile(outputPath, "utf8");
|
|
3212
|
+
let parsed;
|
|
3213
|
+
try {
|
|
3214
|
+
parsed = JSON.parse(raw);
|
|
3215
|
+
} catch (cause) {
|
|
3216
|
+
throw new Error("Codex returned invalid structured output.", { cause });
|
|
3217
|
+
}
|
|
3218
|
+
if (!isRecord3(parsed)) throw new Error("Codex returned invalid structured output.");
|
|
3219
|
+
return parsed;
|
|
3220
|
+
} finally {
|
|
3221
|
+
if (timer !== void 0) clearTimeout(timer);
|
|
3222
|
+
signal?.removeEventListener("abort", onAbort);
|
|
3223
|
+
await NodeFSP2.rm(tmp, { recursive: true, force: true }).catch(() => void 0);
|
|
3224
|
+
void operation;
|
|
3225
|
+
}
|
|
3226
|
+
};
|
|
3227
|
+
return {
|
|
3228
|
+
generateCommitMessage: async (raw, signal) => {
|
|
3229
|
+
const inputRecord = isRecord3(raw) ? raw : {};
|
|
3230
|
+
const includeBranch = inputRecord.includeBranch === true;
|
|
3231
|
+
const policy = isRecord3(inputRecord.policy) ? inputRecord.policy : {};
|
|
3232
|
+
const prompt = [
|
|
3233
|
+
"You write concise git commit messages.",
|
|
3234
|
+
includeBranch ? "Return a JSON object with keys: subject, body, branch." : "Return a JSON object with keys: subject, body.",
|
|
3235
|
+
"Rules:",
|
|
3236
|
+
"- subject must be imperative, <= 72 chars, and no trailing period",
|
|
3237
|
+
"- body can be empty string or short bullet points",
|
|
3238
|
+
...includeBranch ? ["- branch must be a short semantic git branch fragment for this change"] : [],
|
|
3239
|
+
"- capture the primary user-visible or developer-visible change",
|
|
3240
|
+
...policyInstruction(asString(policy.commitInstructions) ?? void 0),
|
|
3241
|
+
"",
|
|
3242
|
+
`Branch: ${asString(inputRecord.branch) ?? "(detached)"}`,
|
|
3243
|
+
"",
|
|
3244
|
+
"Staged files:",
|
|
3245
|
+
limitSection(asString(inputRecord.stagedSummary) ?? "", 6e3),
|
|
3246
|
+
"",
|
|
3247
|
+
"Staged patch:",
|
|
3248
|
+
limitSection(asString(inputRecord.stagedPatch) ?? "", 4e4)
|
|
3249
|
+
].join("\n");
|
|
3250
|
+
const generated = await run(
|
|
3251
|
+
"generateCommitMessage",
|
|
3252
|
+
asString(inputRecord.cwd) ?? ".",
|
|
3253
|
+
prompt,
|
|
3254
|
+
includeBranch ? COMMIT_WITH_BRANCH_SCHEMA : COMMIT_SCHEMA,
|
|
3255
|
+
inputRecord.modelSelection,
|
|
3256
|
+
inputRecord.launch,
|
|
3257
|
+
[],
|
|
3258
|
+
signal
|
|
3259
|
+
);
|
|
3260
|
+
return {
|
|
3261
|
+
subject: sanitizeCommitSubject(readStringField(generated, "subject")),
|
|
3262
|
+
body: readStringField(generated, "body").trim(),
|
|
3263
|
+
...typeof generated.branch === "string" ? { branch: sanitizeFeatureBranchName(generated.branch) } : {}
|
|
3264
|
+
};
|
|
3265
|
+
},
|
|
3266
|
+
generatePrContent: async (raw, signal) => {
|
|
3267
|
+
const inputRecord = isRecord3(raw) ? raw : {};
|
|
3268
|
+
const policy = isRecord3(inputRecord.policy) ? inputRecord.policy : {};
|
|
3269
|
+
const template = asString(inputRecord.changeRequestTemplate)?.trim();
|
|
3270
|
+
const bodyRules = template ? [
|
|
3271
|
+
"- body must be markdown and follow the repository change request template structure",
|
|
3272
|
+
"- fill in the template sections appropriately for this change",
|
|
3273
|
+
"- drop HTML comments from the template in the generated body",
|
|
3274
|
+
"- keep the template's markdown structure"
|
|
3275
|
+
] : [
|
|
3276
|
+
"- body must be markdown and include headings '## Summary' and '## Testing'",
|
|
3277
|
+
"- under Summary, provide short bullet points",
|
|
3278
|
+
"- under Testing, include bullet points with concrete checks or 'Not run' where appropriate"
|
|
3279
|
+
];
|
|
3280
|
+
const prompt = [
|
|
3281
|
+
"You write source control change request content.",
|
|
3282
|
+
"Return a JSON object with keys: title, body.",
|
|
3283
|
+
"Rules:",
|
|
3284
|
+
"- title should be concise and specific",
|
|
3285
|
+
...bodyRules,
|
|
3286
|
+
...policyInstruction(asString(policy.changeRequestInstructions) ?? void 0),
|
|
3287
|
+
...template ? ["", "Repository change request template:", limitSection(template, 8e3)] : [],
|
|
3288
|
+
"",
|
|
3289
|
+
`Base branch: ${asString(inputRecord.baseBranch) ?? ""}`,
|
|
3290
|
+
`Head branch: ${asString(inputRecord.headBranch) ?? ""}`,
|
|
3291
|
+
"",
|
|
3292
|
+
"Commits:",
|
|
3293
|
+
limitSection(asString(inputRecord.commitSummary) ?? "", 12e3),
|
|
3294
|
+
"",
|
|
3295
|
+
"Diff stat:",
|
|
3296
|
+
limitSection(asString(inputRecord.diffSummary) ?? "", 12e3),
|
|
3297
|
+
"",
|
|
3298
|
+
"Diff patch:",
|
|
3299
|
+
limitSection(asString(inputRecord.diffPatch) ?? "", 4e4)
|
|
3300
|
+
].join("\n");
|
|
3301
|
+
const generated = await run(
|
|
3302
|
+
"generatePrContent",
|
|
3303
|
+
asString(inputRecord.cwd) ?? ".",
|
|
3304
|
+
prompt,
|
|
3305
|
+
PR_SCHEMA,
|
|
3306
|
+
inputRecord.modelSelection,
|
|
3307
|
+
inputRecord.launch,
|
|
3308
|
+
[],
|
|
3309
|
+
signal
|
|
3310
|
+
);
|
|
3311
|
+
return {
|
|
3312
|
+
title: sanitizePrTitle(readStringField(generated, "title")),
|
|
3313
|
+
body: readStringField(generated, "body").trim()
|
|
3314
|
+
};
|
|
3315
|
+
},
|
|
3316
|
+
generateBranchName: async (raw, signal) => {
|
|
3317
|
+
const inputRecord = isRecord3(raw) ? raw : {};
|
|
3318
|
+
const policy = isRecord3(inputRecord.policy) ? inputRecord.policy : {};
|
|
3319
|
+
const attachments = Array.isArray(inputRecord.attachments) ? inputRecord.attachments : [];
|
|
3320
|
+
const attachmentLines = attachments.flatMap((attachment) => {
|
|
3321
|
+
if (!isRecord3(attachment)) return [];
|
|
3322
|
+
const name = asString(attachment.name) ?? "attachment";
|
|
3323
|
+
const mime = asString(attachment.mimeType) ?? "application/octet-stream";
|
|
3324
|
+
const size = typeof attachment.sizeBytes === "number" ? attachment.sizeBytes : 0;
|
|
3325
|
+
return [`- ${name} (${mime}, ${size} bytes)`];
|
|
3326
|
+
});
|
|
3327
|
+
const prompt = [
|
|
3328
|
+
"You generate concise git branch names.",
|
|
3329
|
+
"Return a JSON object with key: branch.",
|
|
3330
|
+
"Rules:",
|
|
3331
|
+
"- Branch should describe the requested work from the user message.",
|
|
3332
|
+
"- Keep it short and specific (2-6 words).",
|
|
3333
|
+
"- Use plain words only, no issue prefixes and no punctuation-heavy text.",
|
|
3334
|
+
"- If images are attached, use them as primary context for visual/UI issues.",
|
|
3335
|
+
"",
|
|
3336
|
+
"User message:",
|
|
3337
|
+
limitSection(asString(inputRecord.message) ?? "", 8e3),
|
|
3338
|
+
...policyInstruction(asString(policy.branchInstructions) ?? void 0),
|
|
3339
|
+
...attachmentLines.length > 0 ? ["", "Attachment metadata:", limitSection(attachmentLines.join("\n"), 4e3)] : []
|
|
3340
|
+
].join("\n");
|
|
3341
|
+
const generated = await run(
|
|
3342
|
+
"generateBranchName",
|
|
3343
|
+
asString(inputRecord.cwd) ?? ".",
|
|
3344
|
+
prompt,
|
|
3345
|
+
BRANCH_SCHEMA,
|
|
3346
|
+
inputRecord.modelSelection,
|
|
3347
|
+
inputRecord.launch,
|
|
3348
|
+
absoluteImagePaths(attachments),
|
|
3349
|
+
signal
|
|
3350
|
+
);
|
|
3351
|
+
return { branch: sanitizeBranchFragment(readStringField(generated, "branch")) };
|
|
3352
|
+
},
|
|
3353
|
+
generateThreadTitle: async (raw, signal) => {
|
|
3354
|
+
const inputRecord = isRecord3(raw) ? raw : {};
|
|
3355
|
+
const policy = isRecord3(inputRecord.policy) ? inputRecord.policy : {};
|
|
3356
|
+
const attachments = Array.isArray(inputRecord.attachments) ? inputRecord.attachments : [];
|
|
3357
|
+
const attachmentLines = attachments.flatMap((attachment) => {
|
|
3358
|
+
if (!isRecord3(attachment)) return [];
|
|
3359
|
+
const name = asString(attachment.name) ?? "attachment";
|
|
3360
|
+
const mime = asString(attachment.mimeType) ?? "application/octet-stream";
|
|
3361
|
+
const size = typeof attachment.sizeBytes === "number" ? attachment.sizeBytes : 0;
|
|
3362
|
+
return [`- ${name} (${mime}, ${size} bytes)`];
|
|
3363
|
+
});
|
|
3364
|
+
const extra = [
|
|
3365
|
+
...policyInstruction(asString(policy.threadTitleInstructions) ?? void 0),
|
|
3366
|
+
...attachmentLines.length > 0 ? ["", "Attachment metadata:", limitSection(attachmentLines.join("\n"), 4e3)] : []
|
|
3367
|
+
].join("\n");
|
|
3368
|
+
const previousTitle = asString(inputRecord.previousTitle);
|
|
3369
|
+
const prompt = previousTitle === null ? `${INITIAL_THREAD_TITLE_PROMPT}
|
|
3370
|
+
|
|
3371
|
+
User message:
|
|
3372
|
+
${limitSection(asString(inputRecord.message) ?? "", 8e3)}${extra ? `
|
|
3373
|
+
${extra}` : ""}` : `${regenerateThreadTitlePrompt(previousTitle)}
|
|
3374
|
+
|
|
3375
|
+
Thread contents:
|
|
3376
|
+
${preserveMessageEnd(asString(inputRecord.message) ?? "")}${extra ? `
|
|
3377
|
+
${extra}` : ""}`;
|
|
3378
|
+
const generated = await run(
|
|
3379
|
+
"generateThreadTitle",
|
|
3380
|
+
asString(inputRecord.cwd) ?? ".",
|
|
3381
|
+
prompt,
|
|
3382
|
+
TITLE_SCHEMA,
|
|
3383
|
+
inputRecord.modelSelection,
|
|
3384
|
+
inputRecord.launch,
|
|
3385
|
+
absoluteImagePaths(attachments),
|
|
3386
|
+
signal
|
|
3387
|
+
);
|
|
3388
|
+
return { title: sanitizeThreadTitle(readStringField(generated, "title")) };
|
|
3389
|
+
},
|
|
3390
|
+
summarizeHandover: async (raw, signal) => {
|
|
3391
|
+
const inputRecord = isRecord3(raw) ? raw : {};
|
|
3392
|
+
const prompt = [
|
|
3393
|
+
"You write handover notes so a different coding agent can pick up work already in progress.",
|
|
3394
|
+
"Return a JSON object with exactly one key: summary.",
|
|
3395
|
+
"Rules:",
|
|
3396
|
+
"- summary is markdown, at most 400 words",
|
|
3397
|
+
"- state the goal the user is pursuing, in their terms",
|
|
3398
|
+
"- list what has already been done, including files and commands that matter",
|
|
3399
|
+
"- list what is still open, and any decision the user made that must be respected",
|
|
3400
|
+
"- record constraints, conventions, and failed approaches so they are not retried",
|
|
3401
|
+
"- write for the agent taking over, not for the user; no greeting and no sign-off",
|
|
3402
|
+
"- never invent work that the transcript does not show",
|
|
3403
|
+
"",
|
|
3404
|
+
"Thread transcript (oldest first):",
|
|
3405
|
+
limitSection(asString(inputRecord.transcript) ?? "", 6e4)
|
|
3406
|
+
].join("\n");
|
|
3407
|
+
const generated = await run(
|
|
3408
|
+
"summarizeHandover",
|
|
3409
|
+
asString(inputRecord.cwd) ?? ".",
|
|
3410
|
+
prompt,
|
|
3411
|
+
HANDOVER_SCHEMA,
|
|
3412
|
+
inputRecord.modelSelection,
|
|
3413
|
+
inputRecord.launch,
|
|
3414
|
+
[],
|
|
3415
|
+
signal
|
|
3416
|
+
);
|
|
3417
|
+
return { summary: readStringField(generated, "summary").trim() };
|
|
3418
|
+
}
|
|
3419
|
+
};
|
|
3420
|
+
}
|
|
3421
|
+
|
|
3422
|
+
// src/codexInstance.ts
|
|
3423
|
+
var yieldTick = () => new Promise((resolve2) => {
|
|
3424
|
+
setImmediate(resolve2);
|
|
3425
|
+
});
|
|
3426
|
+
var EventBus = class {
|
|
3427
|
+
#queued = [];
|
|
3428
|
+
#waiters = [];
|
|
3429
|
+
#closed = false;
|
|
3430
|
+
push(event) {
|
|
3431
|
+
const waiter = this.#waiters.shift();
|
|
3432
|
+
if (waiter) {
|
|
3433
|
+
waiter({ done: false, value: event });
|
|
3434
|
+
return;
|
|
3435
|
+
}
|
|
3436
|
+
this.#queued.push(event);
|
|
3437
|
+
}
|
|
3438
|
+
close() {
|
|
3439
|
+
if (this.#closed) return;
|
|
3440
|
+
this.#closed = true;
|
|
3441
|
+
while (this.#waiters.length > 0) {
|
|
3442
|
+
this.#waiters.shift()?.({ done: true, value: void 0 });
|
|
3443
|
+
}
|
|
3444
|
+
}
|
|
3445
|
+
[Symbol.asyncIterator]() {
|
|
3446
|
+
return {
|
|
3447
|
+
next: () => {
|
|
3448
|
+
const queued = this.#queued.shift();
|
|
3449
|
+
if (queued !== void 0) return Promise.resolve({ done: false, value: queued });
|
|
3450
|
+
if (this.#closed) return Promise.resolve({ done: true, value: void 0 });
|
|
3451
|
+
return new Promise((resolve2) => {
|
|
3452
|
+
this.#waiters.push(resolve2);
|
|
3453
|
+
});
|
|
3454
|
+
}
|
|
3455
|
+
};
|
|
3456
|
+
}
|
|
3457
|
+
};
|
|
3458
|
+
function accountToAuth2(account) {
|
|
3459
|
+
switch (account.state) {
|
|
3460
|
+
case "signed-in":
|
|
3461
|
+
case "expired":
|
|
3462
|
+
return {
|
|
3463
|
+
status: "authenticated",
|
|
3464
|
+
...account.email === void 0 ? {} : { email: account.email }
|
|
3465
|
+
};
|
|
3466
|
+
case "signed-out":
|
|
3467
|
+
return { status: "unauthenticated" };
|
|
3468
|
+
case "unreadable":
|
|
3469
|
+
return { status: "unknown" };
|
|
3470
|
+
}
|
|
3471
|
+
}
|
|
3472
|
+
function layoutFor(recipe, launchHome) {
|
|
3473
|
+
const environmentHome = recipe.environment["CODEX_HOME"];
|
|
3474
|
+
return resolveCodexHomeLayout({
|
|
3475
|
+
homePath: launchHome?.trim() || recipe.homePath,
|
|
3476
|
+
shadowHomePath: recipe.shadowHomePath,
|
|
3477
|
+
...environmentHome === void 0 ? {} : { environmentHome }
|
|
3478
|
+
});
|
|
3479
|
+
}
|
|
3480
|
+
var createCodexInstance = (config, host) => {
|
|
3481
|
+
const recipe = parseCodexInstanceConfig(config);
|
|
3482
|
+
const events = new EventBus();
|
|
3483
|
+
const threads = /* @__PURE__ */ new Map();
|
|
3484
|
+
const signInOperations = /* @__PURE__ */ new Map();
|
|
3485
|
+
const live = /* @__PURE__ */ new Map();
|
|
3486
|
+
let disposed = false;
|
|
3487
|
+
const requireThread = (threadId) => {
|
|
3488
|
+
const booked = threads.get(threadId);
|
|
3489
|
+
if (!booked) throw new Error(`unknown thread ${threadId}`);
|
|
3490
|
+
return booked;
|
|
3491
|
+
};
|
|
3492
|
+
const probeAuth = () => accountToAuth2(readCodexAccount(accountHomeFromLayout(layoutFor(recipe))));
|
|
3493
|
+
const trackHandle = (handle, command, threadId) => {
|
|
3494
|
+
live.set(handle.pid, { command, ...threadId === void 0 ? {} : { threadId } });
|
|
3495
|
+
void handle.exited.finally(() => {
|
|
3496
|
+
live.delete(handle.pid);
|
|
3497
|
+
});
|
|
3498
|
+
};
|
|
3499
|
+
const stopBooked = async (booked) => {
|
|
3500
|
+
await booked.runtime.close().catch(() => void 0);
|
|
3501
|
+
};
|
|
3502
|
+
const instance = {
|
|
3503
|
+
probe: async () => {
|
|
3504
|
+
if (recipe.binaryPath.length === 0) {
|
|
3505
|
+
return {
|
|
3506
|
+
installed: false,
|
|
3507
|
+
version: null,
|
|
3508
|
+
readiness: "error",
|
|
3509
|
+
auth: { status: "unknown" },
|
|
3510
|
+
detail: "Codex program path is not configured"
|
|
3511
|
+
};
|
|
3512
|
+
}
|
|
3513
|
+
const connectionError = codexApiConnectionError(recipe, recipe.environment);
|
|
3514
|
+
if (connectionError) {
|
|
3515
|
+
return {
|
|
3516
|
+
installed: true,
|
|
3517
|
+
version: null,
|
|
3518
|
+
readiness: "error",
|
|
3519
|
+
auth: probeAuth(),
|
|
3520
|
+
detail: connectionError
|
|
3521
|
+
};
|
|
3522
|
+
}
|
|
3523
|
+
return {
|
|
3524
|
+
installed: true,
|
|
3525
|
+
version: null,
|
|
3526
|
+
readiness: "unchecked",
|
|
3527
|
+
auth: probeAuth()
|
|
3528
|
+
};
|
|
3529
|
+
},
|
|
3530
|
+
startSession: async (input, signal) => {
|
|
3531
|
+
if (disposed) throw new Error("instance disposed");
|
|
3532
|
+
if (signal?.aborted) throw new Error("aborted");
|
|
3533
|
+
if (recipe.binaryPath.length === 0) {
|
|
3534
|
+
throw new Error("Codex program path is not configured");
|
|
3535
|
+
}
|
|
3536
|
+
const cwd = input.cwd ?? host.workspacePath(input.threadId);
|
|
3537
|
+
if (cwd.trim() === "") throw new Error("workspace path missing");
|
|
3538
|
+
const existing = threads.get(input.threadId);
|
|
3539
|
+
if (existing !== void 0) {
|
|
3540
|
+
await stopBooked(existing);
|
|
3541
|
+
threads.delete(input.threadId);
|
|
3542
|
+
}
|
|
3543
|
+
const layout = layoutFor(recipe, input.launch.homePath);
|
|
3544
|
+
await materializeCodexShadowHome(layout, host.log);
|
|
3545
|
+
const env = buildCodexSpawnEnv({
|
|
3546
|
+
recipe,
|
|
3547
|
+
layout,
|
|
3548
|
+
processEnv: input.launch.processEnv,
|
|
3549
|
+
crushApiKeys: shouldCrushApiKeys(recipe.authMode)
|
|
3550
|
+
});
|
|
3551
|
+
const args = [...codexSpawnArgs(recipe, layout)];
|
|
3552
|
+
const handle = await host.process.spawn({
|
|
3553
|
+
program: recipe.binaryPath,
|
|
3554
|
+
args,
|
|
3555
|
+
cwd,
|
|
3556
|
+
owner: { bindingId: host.bindingId },
|
|
3557
|
+
...Object.keys(env).length > 0 ? { env } : {}
|
|
3558
|
+
});
|
|
3559
|
+
trackHandle(handle, [recipe.binaryPath, ...args].join(" "), input.threadId);
|
|
3560
|
+
let runtime;
|
|
3561
|
+
const rpc = await createCodexRpcClient(handle, {
|
|
3562
|
+
onNotification: (notification) => {
|
|
3563
|
+
runtime?.handleNotification(notification.method, notification.params);
|
|
3564
|
+
},
|
|
3565
|
+
onServerRequest: (request) => {
|
|
3566
|
+
void runtime?.handleServerRequest(request.id, request.method, request.params);
|
|
3567
|
+
},
|
|
3568
|
+
onStderrLine: (line) => {
|
|
3569
|
+
runtime?.handleStderrLine(line);
|
|
3570
|
+
},
|
|
3571
|
+
onRequestSettled: (id, status) => {
|
|
3572
|
+
runtime?.settleNativeWrite(id, status);
|
|
3573
|
+
},
|
|
3574
|
+
onExit: () => {
|
|
3575
|
+
if (runtime && !runtime.closed) {
|
|
3576
|
+
void runtime.close().catch(() => void 0);
|
|
3577
|
+
}
|
|
3578
|
+
}
|
|
3579
|
+
});
|
|
3580
|
+
runtime = new CodexSessionRuntime(host, rpc, (event) => events.push(event), input, cwd, args);
|
|
3581
|
+
try {
|
|
3582
|
+
await rpc.request("initialize", CODEX_INITIALIZE_PARAMS);
|
|
3583
|
+
await rpc.notify("initialized");
|
|
3584
|
+
const session = await runtime.openThread(input);
|
|
3585
|
+
await yieldTick();
|
|
3586
|
+
threads.set(input.threadId, { runtime, rpc, handle });
|
|
3587
|
+
host.log.info("session started", {
|
|
3588
|
+
threadId: input.threadId,
|
|
3589
|
+
nativeThreadId: runtime.nativeThreadId
|
|
3590
|
+
});
|
|
3591
|
+
return session;
|
|
3592
|
+
} catch (error) {
|
|
3593
|
+
await runtime.close().catch(() => void 0);
|
|
3594
|
+
throw error;
|
|
3595
|
+
}
|
|
3596
|
+
},
|
|
3597
|
+
sendTurn: async (input, signal) => {
|
|
3598
|
+
if (disposed) throw new Error("instance disposed");
|
|
3599
|
+
const booked = requireThread(input.threadId);
|
|
3600
|
+
if (signal?.aborted) throw new Error("aborted");
|
|
3601
|
+
const onAbort = () => {
|
|
3602
|
+
void booked.runtime.interruptTurn(input.expectedTurnId).catch(() => void 0);
|
|
3603
|
+
};
|
|
3604
|
+
signal?.addEventListener("abort", onAbort);
|
|
3605
|
+
try {
|
|
3606
|
+
return await booked.runtime.sendTurn(input);
|
|
3607
|
+
} finally {
|
|
3608
|
+
signal?.removeEventListener("abort", onAbort);
|
|
3609
|
+
}
|
|
3610
|
+
},
|
|
3611
|
+
interruptTurn: async (threadId, turnId) => {
|
|
3612
|
+
const booked = threads.get(threadId);
|
|
3613
|
+
if (booked === void 0) return;
|
|
3614
|
+
host.log.info("interrupt turn", { threadId, turnId });
|
|
3615
|
+
await booked.runtime.interruptTurn(turnId);
|
|
3616
|
+
},
|
|
3617
|
+
respondToRequest: async (threadId, requestId, decision, reason) => {
|
|
3618
|
+
const booked = requireThread(threadId);
|
|
3619
|
+
await booked.runtime.respondToRequest(requestId, decision, reason);
|
|
3620
|
+
},
|
|
3621
|
+
respondToUserInput: async (threadId, requestId, answers) => {
|
|
3622
|
+
const booked = requireThread(threadId);
|
|
3623
|
+
await booked.runtime.respondToUserInput(requestId, answers);
|
|
3624
|
+
},
|
|
3625
|
+
setRuntimeMode: async (threadId, runtimeMode) => {
|
|
3626
|
+
const booked = requireThread(threadId);
|
|
3627
|
+
booked.runtime.runtimeMode = runtimeMode;
|
|
3628
|
+
booked.runtime.session = {
|
|
3629
|
+
...booked.runtime.session,
|
|
3630
|
+
runtimeMode,
|
|
3631
|
+
updatedAt: nowIso()
|
|
3632
|
+
};
|
|
3633
|
+
},
|
|
3634
|
+
compactContext: async (threadId) => {
|
|
3635
|
+
const booked = requireThread(threadId);
|
|
3636
|
+
await booked.runtime.compactContext();
|
|
3637
|
+
},
|
|
3638
|
+
stopSession: async (threadId) => {
|
|
3639
|
+
const booked = threads.get(threadId);
|
|
3640
|
+
if (booked === void 0) return;
|
|
3641
|
+
threads.delete(threadId);
|
|
3642
|
+
await stopBooked(booked);
|
|
3643
|
+
},
|
|
3644
|
+
readThread: async (threadId) => {
|
|
3645
|
+
const booked = requireThread(threadId);
|
|
3646
|
+
return await booked.runtime.readThread();
|
|
3647
|
+
},
|
|
3648
|
+
uploadFeedback: async (input) => {
|
|
3649
|
+
const booked = requireThread(input.threadId);
|
|
3650
|
+
return await booked.runtime.uploadFeedback(input.reason);
|
|
3651
|
+
},
|
|
3652
|
+
events,
|
|
3653
|
+
dispose: async () => {
|
|
3654
|
+
if (disposed) return;
|
|
3655
|
+
disposed = true;
|
|
3656
|
+
const operations = [...signInOperations.values()];
|
|
3657
|
+
signInOperations.clear();
|
|
3658
|
+
for (const operation of operations) {
|
|
3659
|
+
await operation.close().catch(() => void 0);
|
|
3660
|
+
}
|
|
3661
|
+
const booked = [...threads.values()];
|
|
3662
|
+
threads.clear();
|
|
3663
|
+
await Promise.all(booked.map((entry) => stopBooked(entry)));
|
|
3664
|
+
events.close();
|
|
3665
|
+
},
|
|
3666
|
+
textGeneration: createCodexTextGeneration({
|
|
3667
|
+
host,
|
|
3668
|
+
recipe,
|
|
3669
|
+
layout: layoutFor(recipe)
|
|
3670
|
+
}),
|
|
3671
|
+
sessions: {
|
|
3672
|
+
scan: async (input) => {
|
|
3673
|
+
if (recipe.binaryPath.length === 0) {
|
|
3674
|
+
throw new Error("Codex program path is not configured");
|
|
3675
|
+
}
|
|
3676
|
+
const layout = layoutFor(recipe);
|
|
3677
|
+
const env = buildCodexSpawnEnv({
|
|
3678
|
+
recipe,
|
|
3679
|
+
layout,
|
|
3680
|
+
processEnv: {},
|
|
3681
|
+
crushApiKeys: shouldCrushApiKeys(recipe.authMode)
|
|
3682
|
+
});
|
|
3683
|
+
const args = [...codexSpawnArgs(recipe, layout)];
|
|
3684
|
+
const handle = await host.process.spawn({
|
|
3685
|
+
program: recipe.binaryPath,
|
|
3686
|
+
args,
|
|
3687
|
+
cwd: input.directory ?? host.workspacePath(input.projectId),
|
|
3688
|
+
owner: { bindingId: host.bindingId },
|
|
3689
|
+
...Object.keys(env).length > 0 ? { env } : {}
|
|
3690
|
+
});
|
|
3691
|
+
trackHandle(handle, [recipe.binaryPath, ...args].join(" "));
|
|
3692
|
+
const rpc = await createCodexRpcClient(handle, {
|
|
3693
|
+
onNotification: () => void 0,
|
|
3694
|
+
onServerRequest: () => void 0,
|
|
3695
|
+
onStderrLine: () => void 0
|
|
3696
|
+
});
|
|
3697
|
+
try {
|
|
3698
|
+
await rpc.request("initialize", CODEX_INITIALIZE_PARAMS);
|
|
3699
|
+
await rpc.notify("initialized");
|
|
3700
|
+
const candidates = await scanCodexSessions({
|
|
3701
|
+
client: rpc,
|
|
3702
|
+
directory: input.directory ?? host.workspacePath(input.projectId),
|
|
3703
|
+
caseInsensitive: NodeProcess.platform === "win32"
|
|
3704
|
+
});
|
|
3705
|
+
return { candidates };
|
|
3706
|
+
} finally {
|
|
3707
|
+
await rpc.close().catch(() => void 0);
|
|
3708
|
+
}
|
|
3709
|
+
},
|
|
3710
|
+
read: async (input) => {
|
|
3711
|
+
if (recipe.binaryPath.length === 0) {
|
|
3712
|
+
throw new Error("Codex program path is not configured");
|
|
3713
|
+
}
|
|
3714
|
+
const layout = layoutFor(recipe);
|
|
3715
|
+
const env = buildCodexSpawnEnv({
|
|
3716
|
+
recipe,
|
|
3717
|
+
layout,
|
|
3718
|
+
processEnv: {},
|
|
3719
|
+
crushApiKeys: shouldCrushApiKeys(recipe.authMode)
|
|
3720
|
+
});
|
|
3721
|
+
const args = [...codexSpawnArgs(recipe, layout)];
|
|
3722
|
+
const handle = await host.process.spawn({
|
|
3723
|
+
program: recipe.binaryPath,
|
|
3724
|
+
args,
|
|
3725
|
+
cwd: input.cwd,
|
|
3726
|
+
owner: { bindingId: host.bindingId },
|
|
3727
|
+
...Object.keys(env).length > 0 ? { env } : {}
|
|
3728
|
+
});
|
|
3729
|
+
trackHandle(handle, [recipe.binaryPath, ...args].join(" "));
|
|
3730
|
+
const rpc = await createCodexRpcClient(handle, {
|
|
3731
|
+
onNotification: () => void 0,
|
|
3732
|
+
onServerRequest: () => void 0,
|
|
3733
|
+
onStderrLine: () => void 0
|
|
3734
|
+
});
|
|
3735
|
+
try {
|
|
3736
|
+
await rpc.request("initialize", CODEX_INITIALIZE_PARAMS);
|
|
3737
|
+
await rpc.notify("initialized");
|
|
3738
|
+
return await readCodexSession({
|
|
3739
|
+
client: rpc,
|
|
3740
|
+
threadId: input.nativeSessionId,
|
|
3741
|
+
cwd: input.cwd,
|
|
3742
|
+
homePath: accountHomeFromLayout(layout),
|
|
3743
|
+
caseInsensitive: NodeProcess.platform === "win32"
|
|
3744
|
+
});
|
|
3745
|
+
} finally {
|
|
3746
|
+
await rpc.close().catch(() => void 0);
|
|
3747
|
+
}
|
|
3748
|
+
}
|
|
3749
|
+
},
|
|
3750
|
+
signIn: {
|
|
3751
|
+
start: async () => {
|
|
3752
|
+
const started = await startCodexSignIn({
|
|
3753
|
+
host,
|
|
3754
|
+
recipe,
|
|
3755
|
+
layout: layoutFor(recipe),
|
|
3756
|
+
processEnv: process.env
|
|
3757
|
+
});
|
|
3758
|
+
signInOperations.set(started.operation.operationId, started.operation);
|
|
3759
|
+
trackHandle(started.operation.handle, "codex login --device-auth");
|
|
3760
|
+
return {
|
|
3761
|
+
operationId: started.operation.operationId,
|
|
3762
|
+
...started.userCode === void 0 ? {} : { userCode: started.userCode },
|
|
3763
|
+
...started.verificationUrl === void 0 ? {} : { verificationUrl: started.verificationUrl }
|
|
3764
|
+
};
|
|
3765
|
+
},
|
|
3766
|
+
cancel: async (operationId) => {
|
|
3767
|
+
const operation = signInOperations.get(operationId);
|
|
3768
|
+
if (operation === void 0) return;
|
|
3769
|
+
signInOperations.delete(operationId);
|
|
3770
|
+
await operation.close().catch(() => void 0);
|
|
3771
|
+
},
|
|
3772
|
+
verify: async () => verifyCodexSignIn({
|
|
3773
|
+
host,
|
|
3774
|
+
recipe,
|
|
3775
|
+
layout: layoutFor(recipe),
|
|
3776
|
+
processEnv: process.env
|
|
3777
|
+
})
|
|
3778
|
+
},
|
|
3779
|
+
listLiveProcesses: async () => [...live.entries()].map(([pid, entry]) => ({
|
|
3780
|
+
pid,
|
|
3781
|
+
command: entry.command,
|
|
3782
|
+
...entry.threadId === void 0 ? {} : { threadId: entry.threadId },
|
|
3783
|
+
bindingId: host.bindingId
|
|
3784
|
+
})),
|
|
3785
|
+
drain: async (signal) => {
|
|
3786
|
+
const waiters = [...live.keys()].map((pid) => {
|
|
3787
|
+
const booked = [...threads.values()].find((entry) => entry.handle.pid === pid);
|
|
3788
|
+
return booked?.handle.exited ?? Promise.resolve(null);
|
|
3789
|
+
});
|
|
3790
|
+
if (signal.aborted) return;
|
|
3791
|
+
await Promise.race([
|
|
3792
|
+
Promise.all(waiters),
|
|
3793
|
+
new Promise((resolve2) => {
|
|
3794
|
+
const onAbort = () => {
|
|
3795
|
+
signal.removeEventListener("abort", onAbort);
|
|
3796
|
+
resolve2();
|
|
3797
|
+
};
|
|
3798
|
+
signal.addEventListener("abort", onAbort);
|
|
3799
|
+
})
|
|
3800
|
+
]);
|
|
3801
|
+
},
|
|
3802
|
+
forceStop: async () => {
|
|
3803
|
+
for (const booked of threads.values()) {
|
|
3804
|
+
await booked.handle.kill("SIGKILL").catch(() => void 0);
|
|
3805
|
+
}
|
|
3806
|
+
for (const operation of signInOperations.values()) {
|
|
3807
|
+
await operation.handle.kill("SIGKILL").catch(() => void 0);
|
|
3808
|
+
}
|
|
3809
|
+
}
|
|
3810
|
+
};
|
|
3811
|
+
return instance;
|
|
3812
|
+
};
|
|
3813
|
+
|
|
3814
|
+
// src/index.ts
|
|
3815
|
+
var executors = [
|
|
3816
|
+
{
|
|
3817
|
+
card: CODEX_CARD,
|
|
3818
|
+
createInstance: async (config, host) => {
|
|
3819
|
+
host.log.info("createInstance");
|
|
3820
|
+
return createCodexInstance(config, host);
|
|
3821
|
+
}
|
|
3822
|
+
}
|
|
3823
|
+
];
|
|
3824
|
+
var index_default = definePlugin({
|
|
3825
|
+
id: "agent-codex",
|
|
3826
|
+
apiVersion: 2,
|
|
3827
|
+
executors,
|
|
3828
|
+
activate(ctx) {
|
|
3829
|
+
ctx.log.info("codex executor ready");
|
|
3830
|
+
}
|
|
3831
|
+
});
|
|
3832
|
+
export {
|
|
3833
|
+
index_default as default
|
|
3834
|
+
};
|