@ycodium-ai/agent-codex 0.2.2167 → 0.2.2510

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -11,6 +11,13 @@ var CODEX_CARD = {
11
11
  commandName: "codex",
12
12
  launchArgs: CODEX_DEFAULT_LAUNCH_ARGS,
13
13
  homeEnvVar: "CODEX_HOME",
14
+ // 影子目录只分开凭据,会话文件仍写在共用 home 下(`resolveCodexHomeLayout`
15
+ // 的 authOverlay:auth.json 是真文件,sessions 是指回共用 home 的软链)。
16
+ homeLayout: {
17
+ accountConfigKey: "shadowHomePath",
18
+ credentialFile: "auth.json",
19
+ transcriptSubdir: "sessions"
20
+ },
14
21
  environment: [],
15
22
  install: { kind: "npm" },
16
23
  signIn: [{ kind: "command", command: "codex login" }],
@@ -31,9 +38,6 @@ var CODEX_CARD = {
31
38
  "5.3-spark": "gpt-5.3-codex-spark",
32
39
  "gpt-5.3-spark": "gpt-5.3-codex-spark"
33
40
  },
34
- modelsDevProviderIds: ["openai"],
35
- modelsDevSlugPrefixed: false,
36
- modelSlugPrefix: "",
37
41
  mcpTransports: ["stdio", "http"],
38
42
  pluginManaged: true,
39
43
  verifiedSkills: true,
@@ -51,16 +55,47 @@ var CODEX_CARD = {
51
55
  },
52
56
  credentialSources: [],
53
57
  diagnostics: [],
58
+ // 设置表单照这份画:带 x-ycodium-form 的属性才进表单(enabled、customModels 等另有入口,不在此列),
59
+ // 顺序看 x-ycodium-form-order;文案键在本插件 locales/<locale>.json 里解析,宿主不兜底。
54
60
  configSchema: {
55
61
  type: "object",
62
+ "x-ycodium-form-order": ["binaryPath", "homePath", "shadowHomePath", "launchArgs"],
56
63
  properties: {
57
64
  authMode: { type: "string", enum: ["default", "chatgpt", "apiKey"] },
58
65
  apiBaseUrl: { type: "string" },
59
66
  enabled: { type: "boolean" },
60
- binaryPath: { type: "string" },
61
- homePath: { type: "string" },
62
- shadowHomePath: { type: "string" },
63
- launchArgs: { type: "string" },
67
+ binaryPath: {
68
+ type: "string",
69
+ "x-ycodium-form": {
70
+ titleKey: "form.binaryPath.title",
71
+ descriptionKey: "form.binaryPath.description",
72
+ placeholderKey: "form.binaryPath.placeholder",
73
+ control: "executable-path"
74
+ }
75
+ },
76
+ homePath: {
77
+ type: "string",
78
+ "x-ycodium-form": {
79
+ titleKey: "form.homePath.title",
80
+ descriptionKey: "form.homePath.description",
81
+ placeholderKey: "form.homePath.placeholder"
82
+ }
83
+ },
84
+ shadowHomePath: {
85
+ type: "string",
86
+ "x-ycodium-form": {
87
+ titleKey: "form.shadowHomePath.title",
88
+ descriptionKey: "form.shadowHomePath.description",
89
+ placeholderKey: "form.shadowHomePath.placeholder"
90
+ }
91
+ },
92
+ launchArgs: {
93
+ type: "string",
94
+ "x-ycodium-form": {
95
+ titleKey: "form.launchArgs.title",
96
+ descriptionKey: "form.launchArgs.description"
97
+ }
98
+ },
64
99
  customModels: { type: "array" },
65
100
  driver: { type: "string" },
66
101
  config: { type: "object" },
@@ -74,12 +109,12 @@ var CODEX_CARD = {
74
109
  nativePathSuffix: "/responses",
75
110
  credentialEnvKey: "CODEX_API_KEY",
76
111
  supportsNativeSubscription: true,
77
- // codex 不认「地址一个变量」:端点写在 `-c model_providers.<id>.base_url` 上,
78
- // 凭据只能经环境变量、变量名还得告诉它。`$providerId` / `$credentialEnvKey`
79
- // 由宿主按 binding 生成,同一台机器上两份绑定因此不抢同一个变量名。
112
+ // codex 不认「地址一个变量」:地址写在 `-c model_providers.<id>.base_url` 上,
113
+ // 凭据只能经环境变量、变量名还得告诉它。这只手的「API URL and key」账户连接
114
+ // (`connection.ts`)就按这个形状拼;这一栏只是说明,宿主不读。
80
115
  //
81
- // `forced_login_method=api` 要写在最后:chatgpt 实例自己先注了
82
- // `forced_login_method=chatgpt`,不盖掉的话手仍走订阅登录,代理地址白注。
116
+ // `forced_login_method=api` 要写在最后:chatgpt 实例自己先加了
117
+ // `forced_login_method=chatgpt`,不盖掉的话手仍走订阅登录,填的地址白填。
83
118
  extraLaunchArgs: [
84
119
  { key: "model_provider", value: "$providerId" },
85
120
  { key: "model_providers.$providerId.name", value: "Ycodium API" },
@@ -92,13 +127,196 @@ var CODEX_CARD = {
92
127
  }
93
128
  };
94
129
 
95
- // src/codexInstance.ts
96
- import * as NodeProcess from "node:process";
97
- import { ExecutorSpawnRefusedError } from "@ycodium-ai/plugin-api/executor";
98
-
99
130
  // src/codexAccount.ts
100
131
  import * as NodeFS from "node:fs";
132
+ import * as NodePath3 from "node:path";
133
+
134
+ // src/homeLayout.ts
135
+ import * as NodeFSP from "node:fs/promises";
136
+ import * as NodeOS2 from "node:os";
137
+ import * as NodePath2 from "node:path";
138
+
139
+ // src/paths.ts
140
+ import * as NodeOS from "node:os";
101
141
  import * as NodePath from "node:path";
142
+ function expandHomePath(value) {
143
+ if (value === "~") return NodeOS.homedir();
144
+ if (value.startsWith("~/") || value.startsWith("~\\")) {
145
+ return NodePath.join(NodeOS.homedir(), value.slice(2));
146
+ }
147
+ return value;
148
+ }
149
+ function nowIso() {
150
+ return (/* @__PURE__ */ new Date()).toISOString();
151
+ }
152
+
153
+ // src/homeLayout.ts
154
+ var KNOWN_SHARED_DIRECTORIES = [
155
+ "sessions",
156
+ "archived_sessions",
157
+ "sqlite",
158
+ "shell_snapshots",
159
+ "worktrees",
160
+ "skills",
161
+ "plugins",
162
+ "cache",
163
+ "logs",
164
+ "mcp-oauth-locks"
165
+ ];
166
+ var PRIVATE_ENTRY_NAMES = /* @__PURE__ */ new Set(["auth.json", "models_cache.json"]);
167
+ var SHADOW_LOCAL_ENTRY_NAMES = /* @__PURE__ */ new Set(["log", "memories", "tmp"]);
168
+ var REPLACEABLE_SHARED_RUNTIME_DIRECTORIES = /* @__PURE__ */ new Set(["mcp-oauth-locks"]);
169
+ function resolveCodexHomeDirectory(value) {
170
+ const expanded = value && value.trim().length > 0 ? expandHomePath(value) : NodePath2.join(NodeOS2.homedir(), ".codex");
171
+ return NodePath2.resolve(expanded);
172
+ }
173
+ function resolveCodexHomeLayout(input) {
174
+ const configuredHome = input.homePath.trim() || (input.environmentHome?.trim() ?? "");
175
+ const sharedHomePath = resolveCodexHomeDirectory(configuredHome || void 0);
176
+ const shadowHomePath = input.shadowHomePath.trim();
177
+ if (shadowHomePath.length === 0) {
178
+ return {
179
+ mode: "direct",
180
+ sharedHomePath,
181
+ effectiveHomePath: configuredHome.length > 0 ? sharedHomePath : void 0,
182
+ continuationKey: `codex:home:${sharedHomePath}`
183
+ };
184
+ }
185
+ return {
186
+ mode: "authOverlay",
187
+ sharedHomePath,
188
+ effectiveHomePath: NodePath2.resolve(expandHomePath(shadowHomePath)),
189
+ continuationKey: `codex:home:${sharedHomePath}`
190
+ };
191
+ }
192
+ function codexContinuationKey(input) {
193
+ return resolveCodexHomeLayout({
194
+ homePath: input.homePath,
195
+ shadowHomePath: "",
196
+ environmentHome: input.instanceEnvironment["CODEX_HOME"] ?? input.inheritedEnvironment["CODEX_HOME"]
197
+ }).continuationKey;
198
+ }
199
+ async function readLinkState(linkPath) {
200
+ try {
201
+ const target = await NodeFSP.readlink(linkPath);
202
+ return { _tag: "Symlink", target };
203
+ } catch (error) {
204
+ const code = typeof error === "object" && error !== null && "code" in error ? String(error.code) : "";
205
+ if (code === "ENOENT") return { _tag: "Missing" };
206
+ if (code === "EINVAL") return { _tag: "NotSymlink" };
207
+ throw error;
208
+ }
209
+ }
210
+ async function createEntryLink(target, link) {
211
+ const type = await NodeFSP.stat(target).then(
212
+ (info) => info.isDirectory() ? "junction" : "file",
213
+ () => "file"
214
+ );
215
+ await NodeFSP.symlink(target, link, type);
216
+ }
217
+ async function removePrivateSymlink(effectiveHomePath, entryName) {
218
+ const privatePath = NodePath2.join(effectiveHomePath, entryName);
219
+ const state = await readLinkState(privatePath);
220
+ if (state._tag === "Symlink") await NodeFSP.rm(privatePath, { force: true });
221
+ }
222
+ async function ensureSymlink(input) {
223
+ const target = NodePath2.join(input.sharedHomePath, input.entryName);
224
+ const link = NodePath2.join(input.effectiveHomePath, input.entryName);
225
+ const state = await readLinkState(link);
226
+ if (state._tag === "NotSymlink") {
227
+ if (!REPLACEABLE_SHARED_RUNTIME_DIRECTORIES.has(input.entryName)) {
228
+ throw new Error(
229
+ `Cannot create Codex shadow home entry '${input.entryName}' because '${link}' already exists and is not a symlink.`
230
+ );
231
+ }
232
+ await NodeFSP.rm(link, { recursive: true, force: true });
233
+ await createEntryLink(target, link);
234
+ return;
235
+ }
236
+ if (state._tag === "Missing") {
237
+ await createEntryLink(target, link);
238
+ return;
239
+ }
240
+ const resolvedExisting = NodePath2.resolve(NodePath2.dirname(link), state.target);
241
+ if (resolvedExisting !== target) {
242
+ await NodeFSP.rm(link, { force: true });
243
+ await createEntryLink(target, link);
244
+ }
245
+ }
246
+ async function makeDirectory(directoryPath) {
247
+ try {
248
+ await NodeFSP.mkdir(directoryPath, { recursive: true });
249
+ } catch (error) {
250
+ throw new Error(
251
+ `Codex shadow home filesystem operation 'makeDirectory' failed for '${directoryPath}': ${String(error)}`,
252
+ { cause: error }
253
+ );
254
+ }
255
+ }
256
+ async function materializeCodexShadowHome(layout, log) {
257
+ if (layout.mode !== "authOverlay") return;
258
+ const effectiveHomePath = layout.effectiveHomePath;
259
+ if (!effectiveHomePath) return;
260
+ if (layout.sharedHomePath === effectiveHomePath) {
261
+ throw new Error(
262
+ `Codex shadow home path '${effectiveHomePath}' must be different from the shared home path '${layout.sharedHomePath}'.`
263
+ );
264
+ }
265
+ await makeDirectory(layout.sharedHomePath);
266
+ await makeDirectory(effectiveHomePath);
267
+ await Promise.all(
268
+ KNOWN_SHARED_DIRECTORIES.map(
269
+ (directory) => makeDirectory(NodePath2.join(layout.sharedHomePath, directory))
270
+ )
271
+ );
272
+ let sharedEntryNames = [];
273
+ try {
274
+ sharedEntryNames = await NodeFSP.readdir(layout.sharedHomePath);
275
+ } catch (error) {
276
+ throw new Error(
277
+ `Codex shadow home filesystem operation 'readDirectory' failed for '${layout.sharedHomePath}': ${String(error)}`,
278
+ { cause: error }
279
+ );
280
+ }
281
+ const entries = new Set(KNOWN_SHARED_DIRECTORIES);
282
+ for (const entryName of sharedEntryNames) {
283
+ if (!PRIVATE_ENTRY_NAMES.has(entryName) && !SHADOW_LOCAL_ENTRY_NAMES.has(entryName)) {
284
+ entries.add(entryName);
285
+ }
286
+ }
287
+ for (const entryName of PRIVATE_ENTRY_NAMES) {
288
+ if (entryName === "auth.json") continue;
289
+ await removePrivateSymlink(effectiveHomePath, entryName);
290
+ }
291
+ for (const entryName of entries) {
292
+ if (PRIVATE_ENTRY_NAMES.has(entryName)) continue;
293
+ try {
294
+ await ensureSymlink({
295
+ sharedHomePath: layout.sharedHomePath,
296
+ effectiveHomePath,
297
+ entryName
298
+ });
299
+ } catch (error) {
300
+ log?.warn("Codex shadow home entry left unlinked.", {
301
+ entryName,
302
+ effectiveHomePath,
303
+ reason: error instanceof Error ? error.message : String(error)
304
+ });
305
+ }
306
+ }
307
+ const authPath = NodePath2.join(effectiveHomePath, "auth.json");
308
+ const authState = await readLinkState(authPath);
309
+ if (authState._tag === "Symlink") {
310
+ throw new Error(
311
+ `Codex shadow home private entry 'auth.json' at '${authPath}' must be a real file, not a symlink.`
312
+ );
313
+ }
314
+ }
315
+ function accountHomeFromLayout(layout) {
316
+ return layout.effectiveHomePath ?? layout.sharedHomePath;
317
+ }
318
+
319
+ // src/codexAccount.ts
102
320
  var AUTH_FILE_NAME = "auth.json";
103
321
  var isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
104
322
  var readString = (record, key) => {
@@ -139,14 +357,16 @@ var signedOutProbe = (exists) => ({
139
357
  directoryExists: exists,
140
358
  email: void 0,
141
359
  name: void 0,
142
- expiresAt: void 0
360
+ expiresAt: void 0,
361
+ authType: void 0
143
362
  });
144
363
  var unreadableProbe = (exists) => ({
145
364
  state: "unreadable",
146
365
  directoryExists: exists,
147
366
  email: void 0,
148
367
  name: void 0,
149
- expiresAt: void 0
368
+ expiresAt: void 0,
369
+ authType: void 0
150
370
  });
151
371
  var epochMillisToIso = (ms) => {
152
372
  if (ms === void 0) return void 0;
@@ -166,7 +386,7 @@ function decodeJwtClaims(token) {
166
386
  }
167
387
  var readCodexAccount = (homePath) => {
168
388
  const exists = directoryExists(homePath);
169
- const raw = readCredentialFile(NodePath.join(homePath, AUTH_FILE_NAME));
389
+ const raw = readCredentialFile(NodePath3.join(homePath, AUTH_FILE_NAME));
170
390
  if (raw === void 0) return signedOutProbe(exists);
171
391
  const parsed = parseJsonRecord(raw);
172
392
  if (parsed === void 0 || !("OPENAI_API_KEY" in parsed || "tokens" in parsed)) {
@@ -187,13 +407,33 @@ var readCodexAccount = (homePath) => {
187
407
  directoryExists: exists,
188
408
  email: claims === void 0 ? void 0 : readString(claims, "email"),
189
409
  name: void 0,
190
- expiresAt
410
+ expiresAt,
411
+ authType: accessToken !== void 0 ? "chatgpt" : "apiKey"
191
412
  };
192
413
  };
414
+ var probeCodexAccount = (accountPath) => {
415
+ const resolved = resolveCodexHomeDirectory(accountPath || process.env.CODEX_HOME);
416
+ const account = readCodexAccount(resolved);
417
+ return {
418
+ accountPath: resolved,
419
+ state: account.state,
420
+ ...account.email === void 0 ? {} : { email: account.email },
421
+ ...account.expiresAt === void 0 ? {} : { expiresAt: account.expiresAt },
422
+ ...account.authType === void 0 ? {} : { authType: account.authType }
423
+ };
424
+ };
425
+
426
+ // src/codexInstance.ts
427
+ import * as NodeProcess from "node:process";
428
+ import { ExecutorSpawnRefusedError } from "@ycodium-ai/plugin-api/executor";
193
429
 
194
430
  // src/config.ts
195
431
  import { modelSelectionStringOption } from "@ycodium-ai/plugin-api/executor";
432
+
433
+ // src/apiKeyEnv.ts
196
434
  var CODEX_INSTANCE_API_KEY_ENV = "YCODIUM_CODEX_API_KEY";
435
+
436
+ // src/config.ts
197
437
  var CODEX_INSTANCE_MODEL_PROVIDER = "ycodium_api";
198
438
  function isRecord2(value) {
199
439
  return typeof value === "object" && value !== null && !Array.isArray(value);
@@ -351,12 +591,6 @@ function codexMcpLaunchConfig(servers) {
351
591
  }
352
592
  return { args, environment };
353
593
  }
354
- function codexOverrideArgs(overrides) {
355
- return overrides.flatMap(({ key, value }) => ["-c", `${key}=${JSON.stringify(value)}`]);
356
- }
357
- function encodeCodexConfigOverrides(overrides) {
358
- return overrides.map(({ key, value }) => `-c ${JSON.stringify(`${key}=${JSON.stringify(value)}`)}`).join(" ");
359
- }
360
594
  function modelIdFromSelection(selection) {
361
595
  if (typeof selection !== "object" || selection === null || Array.isArray(selection)) {
362
596
  return void 0;
@@ -366,7 +600,7 @@ function modelIdFromSelection(selection) {
366
600
  }
367
601
 
368
602
  // src/connection.ts
369
- import { mergeLaunchEnv, brainSlotEnvKeys } from "@ycodium-ai/plugin-api/executor";
603
+ import { mergeLaunchEnv } from "@ycodium-ai/plugin-api/executor";
370
604
  function codexApiConnectionError(recipe, environment) {
371
605
  if (recipe.authMode !== "apiKey") return null;
372
606
  try {
@@ -406,16 +640,7 @@ function connectionArgv(recipe, layout) {
406
640
  }
407
641
  return args;
408
642
  }
409
- function codexSpawnArgs(recipe, layout, nativeSubscription = false) {
410
- if (nativeSubscription) {
411
- return [
412
- ...tokenizeLaunchArgs(recipe.launchArgs),
413
- "-c",
414
- `model_provider=${JSON.stringify("openai")}`,
415
- "-c",
416
- `forced_login_method=${JSON.stringify("chatgpt")}`
417
- ];
418
- }
643
+ function codexSpawnArgs(recipe, layout) {
419
644
  const needsOverrides = layout.mode === "authOverlay" || recipe.authMode !== "default";
420
645
  if (!needsOverrides) return tokenizeLaunchArgs(recipe.launchArgs);
421
646
  return connectionArgv(recipe, layout);
@@ -427,7 +652,6 @@ var CRUSHED_API_KEYS = {
427
652
  function buildCodexSpawnEnv(input) {
428
653
  const { HOME: _instanceHome, ...instanceTable } = input.recipe.environment;
429
654
  return mergeLaunchEnv({
430
- brainSlots: brainSlotEnvKeys(CODEX_CARD),
431
655
  accountHome: input.layout.effectiveHomePath !== void 0 ? { CODEX_HOME: input.layout.effectiveHomePath } : {},
432
656
  instanceTable,
433
657
  handOverrides: input.crushApiKeys ? CRUSHED_API_KEYS : {},
@@ -436,7 +660,6 @@ function buildCodexSpawnEnv(input) {
436
660
  }
437
661
  function nativeLoginSpawnEnv(processEnv, isolatedHome) {
438
662
  return mergeLaunchEnv({
439
- brainSlots: brainSlotEnvKeys(CODEX_CARD),
440
663
  inherited: processEnv,
441
664
  handOverrides: { ...CRUSHED_API_KEYS, CODEX_HOME: isolatedHome }
442
665
  });
@@ -445,191 +668,6 @@ function shouldCrushApiKeys(authMode) {
445
668
  return authMode === "chatgpt";
446
669
  }
447
670
 
448
- // src/homeLayout.ts
449
- import * as NodeFSP from "node:fs/promises";
450
- import * as NodeOS2 from "node:os";
451
- import * as NodePath3 from "node:path";
452
-
453
- // src/paths.ts
454
- import * as NodeOS from "node:os";
455
- import * as NodePath2 from "node:path";
456
- function expandHomePath(value) {
457
- if (value === "~") return NodeOS.homedir();
458
- if (value.startsWith("~/") || value.startsWith("~\\")) {
459
- return NodePath2.join(NodeOS.homedir(), value.slice(2));
460
- }
461
- return value;
462
- }
463
- function nowIso() {
464
- return (/* @__PURE__ */ new Date()).toISOString();
465
- }
466
-
467
- // src/homeLayout.ts
468
- var KNOWN_SHARED_DIRECTORIES = [
469
- "sessions",
470
- "archived_sessions",
471
- "sqlite",
472
- "shell_snapshots",
473
- "worktrees",
474
- "skills",
475
- "plugins",
476
- "cache",
477
- "logs",
478
- "mcp-oauth-locks"
479
- ];
480
- var PRIVATE_ENTRY_NAMES = /* @__PURE__ */ new Set(["auth.json", "models_cache.json"]);
481
- var SHADOW_LOCAL_ENTRY_NAMES = /* @__PURE__ */ new Set(["log", "memories", "tmp"]);
482
- var REPLACEABLE_SHARED_RUNTIME_DIRECTORIES = /* @__PURE__ */ new Set(["mcp-oauth-locks"]);
483
- function resolveCodexHomeDirectory(value) {
484
- const expanded = value && value.trim().length > 0 ? expandHomePath(value) : NodePath3.join(NodeOS2.homedir(), ".codex");
485
- return NodePath3.resolve(expanded);
486
- }
487
- function resolveCodexHomeLayout(input) {
488
- const configuredHome = input.homePath.trim() || (input.environmentHome?.trim() ?? "");
489
- const sharedHomePath = resolveCodexHomeDirectory(configuredHome || void 0);
490
- const shadowHomePath = input.shadowHomePath.trim();
491
- if (shadowHomePath.length === 0) {
492
- return {
493
- mode: "direct",
494
- sharedHomePath,
495
- effectiveHomePath: configuredHome.length > 0 ? sharedHomePath : void 0,
496
- continuationKey: `codex:home:${sharedHomePath}`
497
- };
498
- }
499
- return {
500
- mode: "authOverlay",
501
- sharedHomePath,
502
- effectiveHomePath: NodePath3.resolve(expandHomePath(shadowHomePath)),
503
- continuationKey: `codex:home:${sharedHomePath}`
504
- };
505
- }
506
- function codexContinuationKey(input) {
507
- return resolveCodexHomeLayout({
508
- homePath: input.homePath,
509
- shadowHomePath: "",
510
- environmentHome: input.instanceEnvironment["CODEX_HOME"] ?? input.inheritedEnvironment["CODEX_HOME"]
511
- }).continuationKey;
512
- }
513
- async function readLinkState(linkPath) {
514
- try {
515
- const target = await NodeFSP.readlink(linkPath);
516
- return { _tag: "Symlink", target };
517
- } catch (error) {
518
- const code = typeof error === "object" && error !== null && "code" in error ? String(error.code) : "";
519
- if (code === "ENOENT") return { _tag: "Missing" };
520
- if (code === "EINVAL") return { _tag: "NotSymlink" };
521
- throw error;
522
- }
523
- }
524
- async function createEntryLink(target, link) {
525
- const type = await NodeFSP.stat(target).then(
526
- (info) => info.isDirectory() ? "junction" : "file",
527
- () => "file"
528
- );
529
- await NodeFSP.symlink(target, link, type);
530
- }
531
- async function removePrivateSymlink(effectiveHomePath, entryName) {
532
- const privatePath = NodePath3.join(effectiveHomePath, entryName);
533
- const state = await readLinkState(privatePath);
534
- if (state._tag === "Symlink") await NodeFSP.rm(privatePath, { force: true });
535
- }
536
- async function ensureSymlink(input) {
537
- const target = NodePath3.join(input.sharedHomePath, input.entryName);
538
- const link = NodePath3.join(input.effectiveHomePath, input.entryName);
539
- const state = await readLinkState(link);
540
- if (state._tag === "NotSymlink") {
541
- if (!REPLACEABLE_SHARED_RUNTIME_DIRECTORIES.has(input.entryName)) {
542
- throw new Error(
543
- `Cannot create Codex shadow home entry '${input.entryName}' because '${link}' already exists and is not a symlink.`
544
- );
545
- }
546
- await NodeFSP.rm(link, { recursive: true, force: true });
547
- await createEntryLink(target, link);
548
- return;
549
- }
550
- if (state._tag === "Missing") {
551
- await createEntryLink(target, link);
552
- return;
553
- }
554
- const resolvedExisting = NodePath3.resolve(NodePath3.dirname(link), state.target);
555
- if (resolvedExisting !== target) {
556
- await NodeFSP.rm(link, { force: true });
557
- await createEntryLink(target, link);
558
- }
559
- }
560
- async function makeDirectory(directoryPath) {
561
- try {
562
- await NodeFSP.mkdir(directoryPath, { recursive: true });
563
- } catch (error) {
564
- throw new Error(
565
- `Codex shadow home filesystem operation 'makeDirectory' failed for '${directoryPath}': ${String(error)}`,
566
- { cause: error }
567
- );
568
- }
569
- }
570
- async function materializeCodexShadowHome(layout, log) {
571
- if (layout.mode !== "authOverlay") return;
572
- const effectiveHomePath = layout.effectiveHomePath;
573
- if (!effectiveHomePath) return;
574
- if (layout.sharedHomePath === effectiveHomePath) {
575
- throw new Error(
576
- `Codex shadow home path '${effectiveHomePath}' must be different from the shared home path '${layout.sharedHomePath}'.`
577
- );
578
- }
579
- await makeDirectory(layout.sharedHomePath);
580
- await makeDirectory(effectiveHomePath);
581
- await Promise.all(
582
- KNOWN_SHARED_DIRECTORIES.map(
583
- (directory) => makeDirectory(NodePath3.join(layout.sharedHomePath, directory))
584
- )
585
- );
586
- let sharedEntryNames = [];
587
- try {
588
- sharedEntryNames = await NodeFSP.readdir(layout.sharedHomePath);
589
- } catch (error) {
590
- throw new Error(
591
- `Codex shadow home filesystem operation 'readDirectory' failed for '${layout.sharedHomePath}': ${String(error)}`,
592
- { cause: error }
593
- );
594
- }
595
- const entries = new Set(KNOWN_SHARED_DIRECTORIES);
596
- for (const entryName of sharedEntryNames) {
597
- if (!PRIVATE_ENTRY_NAMES.has(entryName) && !SHADOW_LOCAL_ENTRY_NAMES.has(entryName)) {
598
- entries.add(entryName);
599
- }
600
- }
601
- for (const entryName of PRIVATE_ENTRY_NAMES) {
602
- if (entryName === "auth.json") continue;
603
- await removePrivateSymlink(effectiveHomePath, entryName);
604
- }
605
- for (const entryName of entries) {
606
- if (PRIVATE_ENTRY_NAMES.has(entryName)) continue;
607
- try {
608
- await ensureSymlink({
609
- sharedHomePath: layout.sharedHomePath,
610
- effectiveHomePath,
611
- entryName
612
- });
613
- } catch (error) {
614
- log?.warn("Codex shadow home entry left unlinked.", {
615
- entryName,
616
- effectiveHomePath,
617
- reason: error instanceof Error ? error.message : String(error)
618
- });
619
- }
620
- }
621
- const authPath = NodePath3.join(effectiveHomePath, "auth.json");
622
- const authState = await readLinkState(authPath);
623
- if (authState._tag === "Symlink") {
624
- throw new Error(
625
- `Codex shadow home private entry 'auth.json' at '${authPath}' must be a real file, not a symlink.`
626
- );
627
- }
628
- }
629
- function accountHomeFromLayout(layout) {
630
- return layout.effectiveHomePath ?? layout.sharedHomePath;
631
- }
632
-
633
671
  // src/protocol.ts
634
672
  var CODEX_INITIALIZE_PARAMS = {
635
673
  clientInfo: { name: "ycodium", title: "Ycodium", version: "0" },
@@ -879,6 +917,28 @@ function jsonRpcErrorMessage(error) {
879
917
  }
880
918
 
881
919
  // src/rpc.ts
920
+ function createLineAssembler() {
921
+ const fragments = [];
922
+ return {
923
+ push: (text) => {
924
+ const lines = [];
925
+ let start = 0;
926
+ for (let newline = text.indexOf("\n"); newline !== -1; newline = text.indexOf("\n", start)) {
927
+ fragments.push(text.slice(start, newline));
928
+ lines.push(fragments.join(""));
929
+ fragments.length = 0;
930
+ start = newline + 1;
931
+ }
932
+ if (start < text.length) fragments.push(text.slice(start));
933
+ return lines;
934
+ },
935
+ end: () => {
936
+ const line = fragments.join("");
937
+ fragments.length = 0;
938
+ return line;
939
+ }
940
+ };
941
+ }
882
942
  var STDERR_TAIL_LIMIT = 4e3;
883
943
  var DEFAULT_REQUEST_TIMEOUT_MS = 3e4;
884
944
  var ANSI_ESCAPE_CHAR = String.fromCharCode(27);
@@ -946,17 +1006,12 @@ async function createCodexRpcClient(handle, handlers) {
946
1006
  }
947
1007
  };
948
1008
  const pumpStdout = (async () => {
949
- let buffer = "";
1009
+ const lines = createLineAssembler();
950
1010
  for await (const chunk of handle.stdout) {
951
- buffer += decoder.decode(chunk, { stream: true });
952
- let index = buffer.indexOf("\n");
953
- while (index >= 0) {
954
- dispatch(buffer.slice(0, index));
955
- buffer = buffer.slice(index + 1);
956
- index = buffer.indexOf("\n");
957
- }
1011
+ for (const line of lines.push(decoder.decode(chunk, { stream: true }))) dispatch(line);
958
1012
  }
959
- if (buffer.length > 0) dispatch(buffer);
1013
+ const last = lines.end();
1014
+ if (last.length > 0) dispatch(last);
960
1015
  })().catch(() => void 0);
961
1016
  const pumpStderr = (async () => {
962
1017
  const errorDecoder = new TextDecoder();
@@ -1059,6 +1114,9 @@ async function createCodexRpcClient(handle, handlers) {
1059
1114
  };
1060
1115
  }
1061
1116
 
1117
+ // src/sessionRuntime.ts
1118
+ import * as NodeCrypto2 from "node:crypto";
1119
+
1062
1120
  // src/approvalGrant.ts
1063
1121
  function codexApprovalGrantKey(kind, identity) {
1064
1122
  if (kind === "mcp-elicitation") return `codex:mcp:${identity || "unknown"}`;
@@ -1113,7 +1171,7 @@ You are running inside Ycodium. The \`ycodium\` MCP server is the product-native
1113
1171
 
1114
1172
  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.
1115
1173
 
1116
- 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.
1174
+ 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. The exception is a tool result saying the user has not allowed agents to use a tab's browser profile: that is the user's decision, not a fault. Do not retry on that tab or work around it with another browser; follow the message (open a new tab, or ask the user).
1117
1175
  `;
1118
1176
  var YCODIUM_THREAD_TOOL_INSTRUCTIONS = `
1119
1177
 
@@ -1281,11 +1339,13 @@ function buildCodexDeveloperInstructions(interactionMode, runtime, browserToolsA
1281
1339
  }
1282
1340
 
1283
1341
  // src/events.ts
1342
+ import * as NodeCrypto from "node:crypto";
1284
1343
  var CODEX_PROVIDER = "codex";
1344
+ var EVENT_ID_PREFIX = `codex-evt-${NodeCrypto.randomUUID()}`;
1285
1345
  var eventSeq = 0;
1286
1346
  function nextEventId() {
1287
1347
  eventSeq += 1;
1288
- return `codex-evt-${eventSeq}`;
1348
+ return `${EVENT_ID_PREFIX}-${eventSeq}`;
1289
1349
  }
1290
1350
  function trimText(value) {
1291
1351
  if (typeof value !== "string") return void 0;
@@ -1416,7 +1476,7 @@ function turnCompletedControlFlow(params) {
1416
1476
  const error = isRecord3(turn["error"]) ? trimText(turn["error"]["message"]) : void 0;
1417
1477
  return error ? { state, errorMessage: error } : { state };
1418
1478
  }
1419
- function toApplyPatchFileChanges(params) {
1479
+ function fileChangeEntries(params) {
1420
1480
  if (!isRecord3(params)) return [];
1421
1481
  const item = isRecord3(params["item"]) ? params["item"] : params;
1422
1482
  const changes = item["changes"] ?? item["fileChanges"] ?? params["fileChanges"];
@@ -1430,10 +1490,32 @@ function toApplyPatchFileChanges(params) {
1430
1490
  if (!isRecord3(entry)) continue;
1431
1491
  const path = asString(entry["path"]) ?? asString(entry["file"]);
1432
1492
  if (!path) continue;
1433
- const kind = (asString(entry["type"]) ?? asString(entry["kind"]) ?? "update").toLowerCase();
1434
- const movePath = asString(entry["move_path"]) ?? asString(entry["movePath"]);
1435
- const diff = asString(entry["unified_diff"]) ?? asString(entry["unifiedDiff"]);
1436
- const content = asString(entry["content"]);
1493
+ out.push({
1494
+ path,
1495
+ kind: (asString(entry["type"]) ?? asString(entry["kind"]) ?? "update").toLowerCase(),
1496
+ movePath: asString(entry["move_path"]) ?? asString(entry["movePath"]),
1497
+ diff: asString(entry["unified_diff"]) ?? asString(entry["unifiedDiff"]),
1498
+ content: asString(entry["content"])
1499
+ });
1500
+ }
1501
+ return out;
1502
+ }
1503
+ var MAX_DESCRIBED_FILE_CHANGES = 20;
1504
+ function describeFileChanges(params) {
1505
+ const entries = fileChangeEntries(params).toSorted(
1506
+ (left, right) => left.path.localeCompare(right.path)
1507
+ );
1508
+ if (entries.length === 0) return void 0;
1509
+ const lines = entries.slice(0, MAX_DESCRIBED_FILE_CHANGES).map(
1510
+ (entry) => entry.movePath ? `${entry.kind} ${entry.path} -> ${entry.movePath}` : `${entry.kind} ${entry.path}`
1511
+ );
1512
+ const remaining = entries.length - lines.length;
1513
+ return remaining > 0 ? `${lines.join("\n")}
1514
+ +${remaining} more` : lines.join("\n");
1515
+ }
1516
+ function toApplyPatchFileChanges(params) {
1517
+ const out = [];
1518
+ for (const { path, kind, movePath, diff, content } of fileChangeEntries(params)) {
1437
1519
  if (kind === "add") {
1438
1520
  out.push({
1439
1521
  path,
@@ -1689,8 +1771,10 @@ function describeFileChangeApproval(params) {
1689
1771
  const reason = trimText(record["reason"]);
1690
1772
  const grantRoot = asString(record["grantRoot"]);
1691
1773
  const callId = asString(record["callId"]);
1774
+ const detail = reason ?? describeFileChanges(record) ?? trimText(grantRoot);
1692
1775
  return {
1693
1776
  ...reason ? { reason } : {},
1777
+ ...detail ? { detail } : {},
1694
1778
  ...grantRoot ? { grantRoot } : {},
1695
1779
  ...callId ? { callId } : {},
1696
1780
  fileChanges: toApplyPatchFileChanges(record)
@@ -2320,7 +2404,7 @@ function elicitationOptions(params) {
2320
2404
  ];
2321
2405
  }
2322
2406
  function mapServerRequestToEvents(input) {
2323
- const requestId = String(input.id);
2407
+ const requestId = input.requestId;
2324
2408
  if (input.method === "item/tool/requestUserInput") {
2325
2409
  const questions = toUserInputQuestions(input.params);
2326
2410
  if (!questions) return [];
@@ -2351,7 +2435,7 @@ function mapServerRequestToEvents(input) {
2351
2435
  payload["args"] = input.params;
2352
2436
  } else if (input.method === "item/fileChange/requestApproval" || input.method.includes("applyPatch")) {
2353
2437
  const described = describeFileChangeApproval(input.params);
2354
- if (described.reason) payload["detail"] = described.reason;
2438
+ if (described.detail) payload["detail"] = described.detail;
2355
2439
  if (described.fileChanges.length > 0) payload["fileChanges"] = described.fileChanges;
2356
2440
  payload["args"] = {
2357
2441
  ...described.callId ? { callId: described.callId } : {},
@@ -2382,6 +2466,9 @@ function mapServerRequestToEvents(input) {
2382
2466
  }
2383
2467
 
2384
2468
  // src/sessionRuntime.ts
2469
+ function cancelWireFor(pending) {
2470
+ return pending.kind === "mcp-elicitation" ? toMcpElicitationResponse("cancel", pending.params) : { decision: "cancel" };
2471
+ }
2385
2472
  var sleep = (ms) => new Promise((resolve2) => {
2386
2473
  setTimeout(resolve2, ms);
2387
2474
  });
@@ -2434,8 +2521,18 @@ var CodexSessionRuntime = class {
2434
2521
  memoryThreadIds = /* @__PURE__ */ new Set();
2435
2522
  appServerArgs;
2436
2523
  host;
2524
+ attachments;
2437
2525
  rpc;
2438
2526
  emit;
2527
+ /**
2528
+ * 对外 requestId 的前缀,每个会话(即每个 app-server 进程)一份。JSON-RPC id 每个进程
2529
+ * 都从 0 数起,宿主按 requestId 记「已接受」,不加前缀的话重启后的新请求会撞上旧请求。
2530
+ */
2531
+ #requestIdPrefix = `codex-${NodeCrypto2.randomUUID()}:`;
2532
+ /** JSON-RPC id → 对外 requestId。`serverRequest/resolved` 里的 id 可能是数字也可能是字符串。 */
2533
+ requestIdFor(jsonRpcId) {
2534
+ return `${this.#requestIdPrefix}${String(jsonRpcId)}`;
2535
+ }
2439
2536
  /** 这一轮该用哪套三字段组合:只由这一轮的档决定(见 `codexNativeModeFor`)。 */
2440
2537
  #threadConfig() {
2441
2538
  return codexThreadConfigForNativeMode(codexNativeModeFor(this.runtimeMode, this.nativeModeId));
@@ -2455,7 +2552,8 @@ var CodexSessionRuntime = class {
2455
2552
  error: error instanceof Error ? error.message : String(error)
2456
2553
  });
2457
2554
  }
2458
- constructor(host, rpc, emit, input, cwd, appServerArgs, grants, declineRelay) {
2555
+ constructor(host, rpc, emit, input, cwd, appServerArgs, grants, declineRelay, attachments) {
2556
+ this.attachments = attachments;
2459
2557
  this.host = host;
2460
2558
  this.rpc = rpc;
2461
2559
  this.emit = emit;
@@ -2514,7 +2612,7 @@ var CodexSessionRuntime = class {
2514
2612
  if (kind === "drop") return;
2515
2613
  if (isFatalStderr(line)) {
2516
2614
  this.emit({
2517
- eventId: `codex-stderr-${Date.now()}`,
2615
+ eventId: nextEventId(),
2518
2616
  provider: CODEX_DRIVER_KIND,
2519
2617
  threadId: this.threadId,
2520
2618
  createdAt: nowIso(),
@@ -2557,21 +2655,24 @@ var CodexSessionRuntime = class {
2557
2655
  }
2558
2656
  dispatchMapped(method, params) {
2559
2657
  if (method === "serverRequest/resolved") {
2560
- const resolvedId = (isRecord3(params) ? asString(params["requestId"]) : null) ?? void 0;
2561
- if (resolvedId !== void 0) {
2562
- this.emit({
2563
- eventId: `codex-req-resolved-${resolvedId}`,
2564
- provider: CODEX_DRIVER_KIND,
2565
- threadId: this.threadId,
2566
- createdAt: nowIso(),
2567
- type: "request.resolved",
2568
- requestId: resolvedId,
2569
- payload: {
2570
- requestType: toRequestTypeFromKind(this.requestKinds.get(resolvedId)),
2571
- resolution: params
2572
- }
2573
- });
2574
- }
2658
+ const rawId = isRecord3(params) ? params["requestId"] : void 0;
2659
+ if (typeof rawId !== "number" && typeof rawId !== "string") return;
2660
+ const resolvedId = this.requestIdFor(rawId);
2661
+ const pending = this.pendingApprovals.get(resolvedId);
2662
+ if (pending === void 0) return;
2663
+ pending.release();
2664
+ this.emit({
2665
+ eventId: nextEventId(),
2666
+ provider: CODEX_DRIVER_KIND,
2667
+ threadId: this.threadId,
2668
+ createdAt: nowIso(),
2669
+ type: "request.resolved",
2670
+ requestId: resolvedId,
2671
+ payload: {
2672
+ requestType: toRequestTypeFromKind(this.requestKinds.get(resolvedId)),
2673
+ resolution: params
2674
+ }
2675
+ });
2575
2676
  return;
2576
2677
  }
2577
2678
  for (const event of mapNotificationToEvents({
@@ -2750,9 +2851,9 @@ var CodexSessionRuntime = class {
2750
2851
  return;
2751
2852
  }
2752
2853
  if (method === "item/tool/requestUserInput") {
2753
- const requestId = String(id);
2854
+ const requestId = this.requestIdFor(id);
2754
2855
  const events = mapServerRequestToEvents({
2755
- id,
2856
+ requestId,
2756
2857
  method,
2757
2858
  params,
2758
2859
  threadId: this.threadId,
@@ -2783,9 +2884,9 @@ var CodexSessionRuntime = class {
2783
2884
  await this.rpc.respond(id, { decision });
2784
2885
  return;
2785
2886
  }
2786
- const requestId = String(id);
2887
+ const requestId = this.requestIdFor(id);
2787
2888
  const events = mapServerRequestToEvents({
2788
- id,
2889
+ requestId,
2789
2890
  method,
2790
2891
  params,
2791
2892
  threadId: this.threadId,
@@ -2802,6 +2903,10 @@ var CodexSessionRuntime = class {
2802
2903
  settle: (decision) => {
2803
2904
  this.pendingApprovals.delete(requestId);
2804
2905
  void this.rpc.respond(id, decision).catch((error) => this.#warnAnswerWriteFailed(method, id, error)).finally(resolve2);
2906
+ },
2907
+ release: () => {
2908
+ this.pendingApprovals.delete(requestId);
2909
+ resolve2();
2805
2910
  }
2806
2911
  });
2807
2912
  for (const event of events) this.emit(event);
@@ -2809,7 +2914,7 @@ var CodexSessionRuntime = class {
2809
2914
  }
2810
2915
  async respondToRequest(requestId, decision, reason, persist) {
2811
2916
  const pending = this.pendingApprovals.get(requestId);
2812
- if (!pending) throw new Error(`unknown request ${requestId}`);
2917
+ if (!pending) throw new Error(`Unknown pending Codex approval request: ${requestId}`);
2813
2918
  if (decision === "deny" && reason) this.declineRelay.queue(this.threadId, reason);
2814
2919
  let wire = { decision: decision === "allow" ? "accept" : "decline" };
2815
2920
  if (decision === "allow" && pending.kind === "mcp-elicitation") {
@@ -2825,7 +2930,7 @@ var CodexSessionRuntime = class {
2825
2930
  pending.settle(wire);
2826
2931
  const delivery = await ack.catch(() => "closed");
2827
2932
  this.emit({
2828
- eventId: `codex-req-${requestId}`,
2933
+ eventId: nextEventId(),
2829
2934
  provider: CODEX_DRIVER_KIND,
2830
2935
  threadId: this.threadId,
2831
2936
  createdAt: nowIso(),
@@ -2842,7 +2947,7 @@ var CodexSessionRuntime = class {
2842
2947
  }
2843
2948
  async respondToUserInput(requestId, answers) {
2844
2949
  const pending = this.pendingUserInputs.get(requestId);
2845
- if (!pending) throw new Error(`unknown user-input ${requestId}`);
2950
+ if (!pending) throw new Error(`Unknown pending Codex user input request: ${requestId}`);
2846
2951
  const mapped = toCodexUserInputAnswers(answers);
2847
2952
  if (mapped === void 0) {
2848
2953
  throw new Error(`invalid user input answers for request ${requestId}`);
@@ -2851,7 +2956,7 @@ var CodexSessionRuntime = class {
2851
2956
  pending.settle({ answers: mapped });
2852
2957
  const delivery = await ack.catch(() => "closed");
2853
2958
  this.emit({
2854
- eventId: `codex-ui-${requestId}`,
2959
+ eventId: nextEventId(),
2855
2960
  provider: CODEX_DRIVER_KIND,
2856
2961
  threadId: this.threadId,
2857
2962
  createdAt: nowIso(),
@@ -2903,7 +3008,7 @@ var CodexSessionRuntime = class {
2903
3008
  }
2904
3009
  async openThread(input) {
2905
3010
  const resume = input.discardResumeCursor === true ? void 0 : parseCodexResumeCursor(input.resumeCursor);
2906
- const requestedModel = input.launch.requestedModelId?.trim() || modelIdFromSelection(input.modelSelection);
3011
+ const requestedModel = modelIdFromSelection(input.modelSelection);
2907
3012
  const startParams = {
2908
3013
  ...this.threadConfigParams(),
2909
3014
  ...requestedModel ? { model: requestedModel } : {}
@@ -2939,7 +3044,7 @@ var CodexSessionRuntime = class {
2939
3044
  ...this.model ? { model: this.model } : {}
2940
3045
  };
2941
3046
  this.emit({
2942
- eventId: `codex-session-${this.threadId}`,
3047
+ eventId: nextEventId(),
2943
3048
  provider: CODEX_DRIVER_KIND,
2944
3049
  threadId: this.threadId,
2945
3050
  createdAt: nowIso(),
@@ -2957,24 +3062,45 @@ var CodexSessionRuntime = class {
2957
3062
  const effort = modelSelectionStringOption(input.modelSelection, "reasoningEffort") ?? DEFAULT_TEXT_GENERATION_REASONING_EFFORT;
2958
3063
  return buildCodexDeveloperInstructions(mode, { model, reasoningEffort: effort }, tools, tools);
2959
3064
  }
2960
- turnInput(input) {
3065
+ async turnInput(input) {
3066
+ const images = await this.#imageInputs(input.attachments);
2961
3067
  const items = [];
2962
3068
  const declined = this.declineRelay.take(this.threadId);
2963
3069
  const text = [declined, input.input ?? ""].filter((part) => part && part.length > 0).join("\n\n");
2964
3070
  if (text.length > 0) items.push({ type: "text", text });
2965
- if (Array.isArray(input.attachments)) {
2966
- for (const attachment of input.attachments) {
2967
- if (!isRecord3(attachment)) continue;
2968
- const url = asString(attachment["dataUrl"]) ?? asString(attachment["url"]);
2969
- if (url) items.push({ type: "image", url });
3071
+ items.push(...images);
3072
+ return items;
3073
+ }
3074
+ /** 宿主只给附件 id:字节经附件接口读回,拼成 data URL 交给 codex。读不到就报错,不悄悄丢图。 */
3075
+ async #imageInputs(attachments) {
3076
+ const items = [];
3077
+ for (const attachment of attachments ?? []) {
3078
+ if (!isRecord3(attachment) || attachment["type"] !== "image") continue;
3079
+ const id = asString(attachment["id"]);
3080
+ const mimeType = asString(attachment["mimeType"]);
3081
+ if (!id || !mimeType) {
3082
+ throw new Error("codex image attachment is missing its id or mime type");
2970
3083
  }
3084
+ if (!this.attachments) {
3085
+ throw new Error(`codex cannot read image attachment ${id}: no attachment access`);
3086
+ }
3087
+ const bytes = await this.attachments.read(id);
3088
+ items.push({
3089
+ type: "image",
3090
+ url: `data:${mimeType};base64,${Buffer.from(bytes).toString("base64")}`
3091
+ });
2971
3092
  }
2972
3093
  return items;
2973
3094
  }
2974
3095
  async sendTurn(input) {
2975
3096
  if (!this.nativeThreadId) throw new Error("codex session has no native thread");
2976
3097
  if (hasConfiguredMcpServer(this.appServerArgs)) {
2977
- await this.rpc.request("mcpServer/reload", {}, 0).catch(() => void 0);
3098
+ await this.rpc.request("config/mcpServer/reload", void 0, 0).catch((error) => {
3099
+ this.host.log.warn("refreshing the codex MCP tool list before a turn failed", {
3100
+ threadId: this.threadId,
3101
+ error: error instanceof Error ? error.message : String(error)
3102
+ });
3103
+ });
2978
3104
  }
2979
3105
  const sandboxPolicy = this.#turnSandboxPolicy();
2980
3106
  const config = this.#threadConfig();
@@ -2983,7 +3109,7 @@ var CodexSessionRuntime = class {
2983
3109
  const serviceTier = modelSelectionStringOption(input.modelSelection, "serviceTier");
2984
3110
  const params = {
2985
3111
  threadId: this.nativeThreadId,
2986
- input: this.turnInput(input),
3112
+ input: await this.turnInput(input),
2987
3113
  approvalPolicy: config.approvalPolicy,
2988
3114
  approvalsReviewer: config.approvalsReviewer,
2989
3115
  sandboxPolicy,
@@ -3043,24 +3169,29 @@ var CodexSessionRuntime = class {
3043
3169
  );
3044
3170
  if (!this.nativeThreadId) return;
3045
3171
  const target = turnId ?? this.activeTurnId;
3046
- if (!target) return;
3047
- await this.rpc.request("turn/interrupt", { threadId: this.nativeThreadId, turnId: target }, 0).catch(() => void 0);
3172
+ if (target) {
3173
+ await this.rpc.request("turn/interrupt", { threadId: this.nativeThreadId, turnId: target }, 0).catch(() => void 0);
3174
+ }
3175
+ this.cancelPendingApprovals();
3176
+ }
3177
+ /** 把还挂着的审批答成「取消」,逐条发 request.resolved。 */
3178
+ cancelPendingApprovals() {
3179
+ for (const [requestId, pending] of this.pendingApprovals) {
3180
+ pending.settle(cancelWireFor(pending));
3181
+ this.emit({
3182
+ eventId: nextEventId(),
3183
+ provider: CODEX_DRIVER_KIND,
3184
+ threadId: this.threadId,
3185
+ createdAt: nowIso(),
3186
+ type: "request.resolved",
3187
+ requestId,
3188
+ payload: { requestType: toRequestTypeFromKind(pending.kind), decision: "cancel" }
3189
+ });
3190
+ }
3048
3191
  }
3049
3192
  async compactContext() {
3050
3193
  if (!this.nativeThreadId) return;
3051
- await this.rpc.request("thread/compact", { threadId: this.nativeThreadId }, 0).catch(() => void 0);
3052
- }
3053
- async uploadFeedback(reason) {
3054
- const result = await this.rpc.request(
3055
- "feedback/upload",
3056
- {
3057
- threadId: this.nativeThreadId,
3058
- ...reason ? { reason } : {}
3059
- },
3060
- 0
3061
- );
3062
- const id = (isRecord3(result) ? asString(result["feedbackId"]) : null) ?? `codex-feedback-${Date.now()}`;
3063
- return { feedbackId: id };
3194
+ await this.rpc.request("thread/compact/start", { threadId: this.nativeThreadId }, 0);
3064
3195
  }
3065
3196
  async readThread() {
3066
3197
  if (!this.nativeThreadId) return { threadId: this.threadId, turns: this.turns };
@@ -3115,7 +3246,7 @@ var CodexSessionRuntime = class {
3115
3246
  }
3116
3247
  this.closeCompleted = true;
3117
3248
  this.emit({
3118
- eventId: `codex-close-${this.threadId}`,
3249
+ eventId: nextEventId(),
3119
3250
  provider: CODEX_DRIVER_KIND,
3120
3251
  threadId: this.threadId,
3121
3252
  createdAt: nowIso(),
@@ -3470,10 +3601,6 @@ function serviceTierDescriptor(row) {
3470
3601
  currentValue: defaultTier
3471
3602
  };
3472
3603
  }
3473
- function isSupersededModel(row) {
3474
- if (isRecord3(row["upgradeInfo"])) return true;
3475
- return (asString(row["upgrade"]) ?? "").length > 0;
3476
- }
3477
3604
  function codexProbeModel(row) {
3478
3605
  if (!isRecord3(row)) return null;
3479
3606
  const slug = asString(row["model"]);
@@ -3486,7 +3613,6 @@ function codexProbeModel(row) {
3486
3613
  slug,
3487
3614
  ...displayName ? { name: codexModelDisplayName(displayName) } : {},
3488
3615
  ...row["isDefault"] === true ? { isDefault: true } : {},
3489
- ...isSupersededModel(row) ? { isLegacy: true } : {},
3490
3616
  ...row["hidden"] === true ? { hidden: true } : {},
3491
3617
  // 空表也给 `capabilities`:`{}` 与 `null` 对宿主是一回事(都不画那一格),
3492
3618
  // 但给对象保住「这只手确实答过这一问」这层意思。
@@ -3519,6 +3645,47 @@ async function listCodexModels(rpc) {
3519
3645
  return models;
3520
3646
  }
3521
3647
 
3648
+ // src/skills.ts
3649
+ var SKILLS_LIST_TIMEOUT_MS = 15e3;
3650
+ function codexProbeSkill(raw) {
3651
+ if (!isRecord3(raw)) return null;
3652
+ const name = asString(raw["name"]);
3653
+ const path = asString(raw["path"]);
3654
+ const scope = asString(raw["scope"]);
3655
+ if (!name || !path || scope === "repo") return null;
3656
+ const face = isRecord3(raw["interface"]) ? raw["interface"] : void 0;
3657
+ const description = asString(raw["description"]);
3658
+ const displayName = asString(face?.["displayName"]);
3659
+ const shortDescription = asString(face?.["shortDescription"]) || asString(raw["shortDescription"]);
3660
+ return {
3661
+ name,
3662
+ path,
3663
+ enabled: raw["enabled"] !== false,
3664
+ ...scope ? { scope } : {},
3665
+ ...description ? { description } : {},
3666
+ ...displayName ? { displayName } : {},
3667
+ ...shortDescription ? { shortDescription } : {}
3668
+ };
3669
+ }
3670
+ async function listCodexSkills(rpc) {
3671
+ let response;
3672
+ try {
3673
+ response = await rpc.request("skills/list", {}, SKILLS_LIST_TIMEOUT_MS);
3674
+ } catch {
3675
+ return void 0;
3676
+ }
3677
+ if (!isRecord3(response) || !Array.isArray(response["data"])) return void 0;
3678
+ const skills = /* @__PURE__ */ new Map();
3679
+ for (const entry of response["data"]) {
3680
+ if (!isRecord3(entry) || !Array.isArray(entry["skills"])) continue;
3681
+ for (const raw of entry["skills"]) {
3682
+ const skill = codexProbeSkill(raw);
3683
+ if (skill !== null) skills.set(skill.path, skill);
3684
+ }
3685
+ }
3686
+ return [...skills.values()].toSorted((left, right) => left.name.localeCompare(right.name));
3687
+ }
3688
+
3522
3689
  // src/signIn.ts
3523
3690
  var DEVICE_CODE_URL = /https?:\/\/\S+/;
3524
3691
  var DEVICE_CODE_VALUE = /\b([A-Z0-9]{4,}(?:-[A-Z0-9]{4,})+)\b/;
@@ -3708,8 +3875,7 @@ import * as NodeFSP2 from "node:fs/promises";
3708
3875
  import * as NodeOS3 from "node:os";
3709
3876
  import * as NodePath5 from "node:path";
3710
3877
  import {
3711
- mergeLaunchEnv as mergeLaunchEnv2,
3712
- brainSlotEnvKeys as brainSlotEnvKeys2
3878
+ mergeLaunchEnv as mergeLaunchEnv2
3713
3879
  } from "@ycodium-ai/plugin-api/executor";
3714
3880
  var CODEX_TIMEOUT_MS = 18e4;
3715
3881
  var EARLIER_CONTENT_TRUNCATION_MARKER = "[Earlier content truncated]\n\n";
@@ -3856,7 +4022,7 @@ async function collectStream(stream) {
3856
4022
  return out;
3857
4023
  }
3858
4024
  function createCodexTextGeneration(input) {
3859
- const run = async (operation, cwd, prompt, schema, modelSelection, launch, imagePaths, signal) => {
4025
+ const run = async (operation, cwd, prompt, schema, modelSelection, imagePaths, signal) => {
3860
4026
  if (input.recipe.binaryPath.length === 0) {
3861
4027
  throw new Error("Codex CLI is required but not available on PATH.");
3862
4028
  }
@@ -3865,33 +4031,16 @@ function createCodexTextGeneration(input) {
3865
4031
  const outputPath = NodePath5.join(tmp, "output.json");
3866
4032
  await NodeFSP2.writeFile(schemaPath, JSON.stringify(schema));
3867
4033
  await NodeFSP2.writeFile(outputPath, "");
3868
- const launchRecord = isRecord3(launch) ? launch : {};
3869
- const processEnv = isRecord3(launchRecord.processEnv) ? Object.fromEntries(
3870
- Object.entries(launchRecord.processEnv).filter(
3871
- (entry) => typeof entry[1] === "string"
3872
- )
3873
- ) : {};
3874
- const overlay = Array.isArray(launchRecord.launchArgOverrides) && launchRecord.launchArgOverrides.length > 0 ? encodeCodexConfigOverrides(
3875
- launchRecord.launchArgOverrides.flatMap((entry) => {
3876
- if (!isRecord3(entry) || typeof entry.key !== "string") return [];
3877
- const value = entry.value;
3878
- if (typeof value !== "string" && typeof value !== "boolean") return [];
3879
- return [{ key: entry.key, value }];
3880
- })
3881
- ) : "";
3882
- const launchArgs = [input.recipe.launchArgs, overlay].filter((value) => value.length > 0).join(" ");
3883
4034
  const reasoningEffort = modelSelectionStringOption(modelSelection, "reasoningEffort") ?? DEFAULT_TEXT_GENERATION_REASONING_EFFORT;
3884
4035
  const serviceTier = modelSelectionStringOption(modelSelection, "serviceTier");
3885
- const model = asString(launchRecord.requestedModelId) ?? modelIdFromSelection(modelSelection) ?? "gpt-5.6-luna";
4036
+ const model = modelIdFromSelection(modelSelection) ?? "gpt-5.6-luna";
3886
4037
  const env = mergeLaunchEnv2({
3887
- brainSlots: brainSlotEnvKeys2(CODEX_CARD),
3888
4038
  accountHome: input.layout.effectiveHomePath ? { CODEX_HOME: input.layout.effectiveHomePath } : input.recipe.homePath.trim() ? { CODEX_HOME: expandHomePath(input.recipe.homePath) } : {},
3889
- instanceTable: input.recipe.environment,
3890
- launch: processEnv
4039
+ instanceTable: input.recipe.environment
3891
4040
  });
3892
4041
  const args = [
3893
4042
  "exec",
3894
- ...codexExecLaunchArgs(launchArgs),
4043
+ ...codexExecLaunchArgs(input.recipe.launchArgs),
3895
4044
  "--ephemeral",
3896
4045
  "--skip-git-repo-check",
3897
4046
  "-s",
@@ -3989,7 +4138,6 @@ function createCodexTextGeneration(input) {
3989
4138
  prompt,
3990
4139
  includeBranch ? COMMIT_WITH_BRANCH_SCHEMA : COMMIT_SCHEMA,
3991
4140
  inputRecord.modelSelection,
3992
- inputRecord.launch,
3993
4141
  [],
3994
4142
  signal
3995
4143
  );
@@ -4040,7 +4188,6 @@ function createCodexTextGeneration(input) {
4040
4188
  prompt,
4041
4189
  PR_SCHEMA,
4042
4190
  inputRecord.modelSelection,
4043
- inputRecord.launch,
4044
4191
  [],
4045
4192
  signal
4046
4193
  );
@@ -4080,7 +4227,6 @@ function createCodexTextGeneration(input) {
4080
4227
  prompt,
4081
4228
  BRANCH_SCHEMA,
4082
4229
  inputRecord.modelSelection,
4083
- inputRecord.launch,
4084
4230
  absoluteImagePaths(attachments),
4085
4231
  signal
4086
4232
  );
@@ -4117,7 +4263,6 @@ ${extra}` : ""}`;
4117
4263
  prompt,
4118
4264
  TITLE_SCHEMA,
4119
4265
  inputRecord.modelSelection,
4120
- inputRecord.launch,
4121
4266
  absoluteImagePaths(attachments),
4122
4267
  signal
4123
4268
  );
@@ -4146,7 +4291,6 @@ ${extra}` : ""}`;
4146
4291
  prompt,
4147
4292
  HANDOVER_SCHEMA,
4148
4293
  inputRecord.modelSelection,
4149
- inputRecord.launch,
4150
4294
  [],
4151
4295
  signal
4152
4296
  );
@@ -4205,18 +4349,15 @@ function accountToAuth2(account) {
4205
4349
  return { status: "unknown" };
4206
4350
  }
4207
4351
  }
4208
- function layoutFor(recipe, launchHome, nativeSubscription = false) {
4352
+ function layoutFor(recipe) {
4209
4353
  const environmentHome = recipe.environment["CODEX_HOME"];
4210
- if (nativeSubscription && launchHome?.trim()) {
4211
- return resolveCodexHomeLayout({ homePath: launchHome.trim(), shadowHomePath: "" });
4212
- }
4213
4354
  return resolveCodexHomeLayout({
4214
- homePath: launchHome?.trim() || recipe.homePath,
4355
+ homePath: recipe.homePath,
4215
4356
  shadowHomePath: recipe.shadowHomePath,
4216
4357
  ...environmentHome === void 0 ? {} : { environmentHome }
4217
4358
  });
4218
4359
  }
4219
- var createCodexInstance = (config, host) => {
4360
+ var createCodexInstance = (config, host, attachments) => {
4220
4361
  const recipe = parseCodexInstanceConfig(config);
4221
4362
  const events = new EventBus();
4222
4363
  const threads = /* @__PURE__ */ new Map();
@@ -4256,10 +4397,11 @@ var createCodexInstance = (config, host) => {
4256
4397
  await rpc.request("initialize", CODEX_INITIALIZE_PARAMS)
4257
4398
  );
4258
4399
  await rpc.notify("initialized");
4259
- return { version, models: input.listModels ? await listCodexModels(rpc) : void 0 };
4400
+ const models = input.listModels ? await listCodexModels(rpc) : void 0;
4401
+ return { version, models, skills: await listCodexSkills(rpc) };
4260
4402
  } catch (error) {
4261
4403
  if (error instanceof ExecutorSpawnRefusedError) throw error;
4262
- return { version, models: void 0 };
4404
+ return { version, models: void 0, skills: void 0 };
4263
4405
  } finally {
4264
4406
  await rpc?.close().catch(() => void 0);
4265
4407
  await handle?.kill("SIGTERM").catch(() => void 0);
@@ -4286,7 +4428,9 @@ var createCodexInstance = (config, host) => {
4286
4428
  };
4287
4429
  }
4288
4430
  const connectionError = codexApiConnectionError(recipe, recipe.environment);
4289
- const { version, models } = await probeAppServer({ listModels: connectionError === null });
4431
+ const { version, models, skills } = await probeAppServer({
4432
+ listModels: connectionError === null
4433
+ });
4290
4434
  if (connectionError) {
4291
4435
  return {
4292
4436
  installed: true,
@@ -4294,6 +4438,7 @@ var createCodexInstance = (config, host) => {
4294
4438
  readiness: "error",
4295
4439
  auth: probeAuth(),
4296
4440
  detail: connectionError,
4441
+ ...skills === void 0 ? {} : { skills },
4297
4442
  ...CODEX_NATIVE_MODE_REPORT
4298
4443
  };
4299
4444
  }
@@ -4303,11 +4448,14 @@ var createCodexInstance = (config, host) => {
4303
4448
  readiness: models === void 0 ? "unchecked" : "ready",
4304
4449
  auth: probeAuth(),
4305
4450
  ...models === void 0 ? {} : { models },
4451
+ ...skills === void 0 ? {} : { skills },
4306
4452
  // 宿主拿的是这一份。登录核验那份(`verifyCodexSignIn`)报了档、这里没报,
4307
4453
  // 界面就一直画宿主四档——2026-09-20 真发生过,浏览器里才看得见。
4308
4454
  ...CODEX_NATIVE_MODE_REPORT
4309
4455
  };
4310
4456
  },
4457
+ // 有意不处理 input.readOnlyDirectories:codex 的沙箱只限制写入,读不受限,
4458
+ // 附件目录本来就能读,无需放行。
4311
4459
  startSession: async (input, signal) => {
4312
4460
  if (disposed) throw new Error("instance disposed");
4313
4461
  if (signal?.aborted) throw new Error("aborted");
@@ -4321,22 +4469,17 @@ var createCodexInstance = (config, host) => {
4321
4469
  await stopBooked(existing);
4322
4470
  threads.delete(input.threadId);
4323
4471
  }
4324
- const nativeSubscription = input.launch.nativeSubscription === true;
4325
- const layout = layoutFor(recipe, input.launch.homePath, nativeSubscription);
4472
+ const layout = layoutFor(recipe);
4326
4473
  await materializeCodexShadowHome(layout, host.log);
4327
4474
  const mcp = codexMcpLaunchConfig(input.mcpServers ?? []);
4328
4475
  const env = buildCodexSpawnEnv({
4329
4476
  recipe,
4330
4477
  layout,
4331
- // MCP 那几个变量是宿主挑好的 server 带来的,与绑定同一层:实例表盖不得。
4332
- launchEnv: { ...input.launch.processEnv, ...mcp.environment },
4333
- crushApiKeys: nativeSubscription || shouldCrushApiKeys(recipe.authMode)
4478
+ // MCP 那几个变量是宿主挑好的 server 带来的,铺在最终层:实例表盖不得。
4479
+ launchEnv: mcp.environment,
4480
+ crushApiKeys: shouldCrushApiKeys(recipe.authMode)
4334
4481
  });
4335
- const args = [
4336
- ...codexSpawnArgs(recipe, layout, nativeSubscription),
4337
- ...codexOverrideArgs(input.launch.launchArgOverrides),
4338
- ...mcp.args
4339
- ];
4482
+ const args = [...codexSpawnArgs(recipe, layout), ...mcp.args];
4340
4483
  const handle = await host.process.spawn({
4341
4484
  program: recipe.binaryPath,
4342
4485
  args,
@@ -4371,7 +4514,17 @@ var createCodexInstance = (config, host) => {
4371
4514
  }
4372
4515
  }
4373
4516
  });
4374
- runtime = new CodexSessionRuntime(host, rpc, (event) => events.push(event), input, cwd, args);
4517
+ runtime = new CodexSessionRuntime(
4518
+ host,
4519
+ rpc,
4520
+ (event) => events.push(event),
4521
+ input,
4522
+ cwd,
4523
+ args,
4524
+ void 0,
4525
+ void 0,
4526
+ attachments
4527
+ );
4375
4528
  try {
4376
4529
  await rpc.request("initialize", CODEX_INITIALIZE_PARAMS);
4377
4530
  await rpc.notify("initialized");
@@ -4439,10 +4592,6 @@ var createCodexInstance = (config, host) => {
4439
4592
  const booked = requireThread(threadId);
4440
4593
  return await booked.runtime.readThread();
4441
4594
  },
4442
- uploadFeedback: async (input) => {
4443
- const booked = requireThread(input.threadId);
4444
- return await booked.runtime.uploadFeedback(input.reason);
4445
- },
4446
4595
  events,
4447
4596
  dispose: async () => {
4448
4597
  if (disposed) return;
@@ -4628,13 +4777,15 @@ var createCodexInstance = (config, host) => {
4628
4777
  };
4629
4778
 
4630
4779
  // src/index.ts
4780
+ var capturedAttachments;
4631
4781
  var executors = [
4632
4782
  {
4633
4783
  card: CODEX_CARD,
4634
4784
  createInstance: async (config, host) => {
4635
4785
  host.log.info("createInstance");
4636
- return createCodexInstance(config, host);
4637
- }
4786
+ return createCodexInstance(config, host, capturedAttachments);
4787
+ },
4788
+ probeAccount: async ({ accountPath }) => probeCodexAccount(accountPath)
4638
4789
  }
4639
4790
  ];
4640
4791
  var index_default = definePlugin({
@@ -4642,6 +4793,7 @@ var index_default = definePlugin({
4642
4793
  apiVersion: 2,
4643
4794
  executors,
4644
4795
  activate(ctx) {
4796
+ capturedAttachments = ctx.attachments;
4645
4797
  ctx.log.info("codex executor ready");
4646
4798
  }
4647
4799
  });