@ycodium-ai/agent-codex 0.2.2167 → 0.2.2488

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 CHANGED
@@ -17,7 +17,8 @@ Facts:
17
17
  - Manifest permission is `{ kind: "executor" }`.
18
18
  - `runtimeModeSwitch` is `next-turn` (same table the host adapter reads).
19
19
  - `sessionModelSwitch` is `in-session`.
20
- - Native ChatGPT device-code login is supported (`supportsNativeSubscription`).
20
+ - Native ChatGPT device-code login is supported: `deviceCodeSignInArgs` runs
21
+ `codex login --device-auth`.
21
22
 
22
23
  Develop with `vp run --filter @ycodium-ai/agent-codex typecheck`, `build`, and `test`.
23
24
 
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,10 +407,26 @@ 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"
412
+ };
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 }
191
423
  };
192
424
  };
193
425
 
426
+ // src/codexInstance.ts
427
+ import * as NodeProcess from "node:process";
428
+ import { ExecutorSpawnRefusedError } from "@ycodium-ai/plugin-api/executor";
429
+
194
430
  // src/config.ts
195
431
  import { modelSelectionStringOption } from "@ycodium-ai/plugin-api/executor";
196
432
  var CODEX_INSTANCE_API_KEY_ENV = "YCODIUM_CODEX_API_KEY";
@@ -351,12 +587,6 @@ function codexMcpLaunchConfig(servers) {
351
587
  }
352
588
  return { args, environment };
353
589
  }
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
590
  function modelIdFromSelection(selection) {
361
591
  if (typeof selection !== "object" || selection === null || Array.isArray(selection)) {
362
592
  return void 0;
@@ -366,7 +596,7 @@ function modelIdFromSelection(selection) {
366
596
  }
367
597
 
368
598
  // src/connection.ts
369
- import { mergeLaunchEnv, brainSlotEnvKeys } from "@ycodium-ai/plugin-api/executor";
599
+ import { mergeLaunchEnv } from "@ycodium-ai/plugin-api/executor";
370
600
  function codexApiConnectionError(recipe, environment) {
371
601
  if (recipe.authMode !== "apiKey") return null;
372
602
  try {
@@ -406,16 +636,7 @@ function connectionArgv(recipe, layout) {
406
636
  }
407
637
  return args;
408
638
  }
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
- }
639
+ function codexSpawnArgs(recipe, layout) {
419
640
  const needsOverrides = layout.mode === "authOverlay" || recipe.authMode !== "default";
420
641
  if (!needsOverrides) return tokenizeLaunchArgs(recipe.launchArgs);
421
642
  return connectionArgv(recipe, layout);
@@ -427,7 +648,6 @@ var CRUSHED_API_KEYS = {
427
648
  function buildCodexSpawnEnv(input) {
428
649
  const { HOME: _instanceHome, ...instanceTable } = input.recipe.environment;
429
650
  return mergeLaunchEnv({
430
- brainSlots: brainSlotEnvKeys(CODEX_CARD),
431
651
  accountHome: input.layout.effectiveHomePath !== void 0 ? { CODEX_HOME: input.layout.effectiveHomePath } : {},
432
652
  instanceTable,
433
653
  handOverrides: input.crushApiKeys ? CRUSHED_API_KEYS : {},
@@ -436,7 +656,6 @@ function buildCodexSpawnEnv(input) {
436
656
  }
437
657
  function nativeLoginSpawnEnv(processEnv, isolatedHome) {
438
658
  return mergeLaunchEnv({
439
- brainSlots: brainSlotEnvKeys(CODEX_CARD),
440
659
  inherited: processEnv,
441
660
  handOverrides: { ...CRUSHED_API_KEYS, CODEX_HOME: isolatedHome }
442
661
  });
@@ -445,191 +664,6 @@ function shouldCrushApiKeys(authMode) {
445
664
  return authMode === "chatgpt";
446
665
  }
447
666
 
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
667
  // src/protocol.ts
634
668
  var CODEX_INITIALIZE_PARAMS = {
635
669
  clientInfo: { name: "ycodium", title: "Ycodium", version: "0" },
@@ -1059,6 +1093,9 @@ async function createCodexRpcClient(handle, handlers) {
1059
1093
  };
1060
1094
  }
1061
1095
 
1096
+ // src/sessionRuntime.ts
1097
+ import * as NodeCrypto2 from "node:crypto";
1098
+
1062
1099
  // src/approvalGrant.ts
1063
1100
  function codexApprovalGrantKey(kind, identity) {
1064
1101
  if (kind === "mcp-elicitation") return `codex:mcp:${identity || "unknown"}`;
@@ -1113,7 +1150,7 @@ You are running inside Ycodium. The \`ycodium\` MCP server is the product-native
1113
1150
 
1114
1151
  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
1152
 
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.
1153
+ 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
1154
  `;
1118
1155
  var YCODIUM_THREAD_TOOL_INSTRUCTIONS = `
1119
1156
 
@@ -1281,11 +1318,13 @@ function buildCodexDeveloperInstructions(interactionMode, runtime, browserToolsA
1281
1318
  }
1282
1319
 
1283
1320
  // src/events.ts
1321
+ import * as NodeCrypto from "node:crypto";
1284
1322
  var CODEX_PROVIDER = "codex";
1323
+ var EVENT_ID_PREFIX = `codex-evt-${NodeCrypto.randomUUID()}`;
1285
1324
  var eventSeq = 0;
1286
1325
  function nextEventId() {
1287
1326
  eventSeq += 1;
1288
- return `codex-evt-${eventSeq}`;
1327
+ return `${EVENT_ID_PREFIX}-${eventSeq}`;
1289
1328
  }
1290
1329
  function trimText(value) {
1291
1330
  if (typeof value !== "string") return void 0;
@@ -2320,7 +2359,7 @@ function elicitationOptions(params) {
2320
2359
  ];
2321
2360
  }
2322
2361
  function mapServerRequestToEvents(input) {
2323
- const requestId = String(input.id);
2362
+ const requestId = input.requestId;
2324
2363
  if (input.method === "item/tool/requestUserInput") {
2325
2364
  const questions = toUserInputQuestions(input.params);
2326
2365
  if (!questions) return [];
@@ -2382,6 +2421,9 @@ function mapServerRequestToEvents(input) {
2382
2421
  }
2383
2422
 
2384
2423
  // src/sessionRuntime.ts
2424
+ function cancelWireFor(pending) {
2425
+ return pending.kind === "mcp-elicitation" ? toMcpElicitationResponse("cancel", pending.params) : { decision: "cancel" };
2426
+ }
2385
2427
  var sleep = (ms) => new Promise((resolve2) => {
2386
2428
  setTimeout(resolve2, ms);
2387
2429
  });
@@ -2434,8 +2476,18 @@ var CodexSessionRuntime = class {
2434
2476
  memoryThreadIds = /* @__PURE__ */ new Set();
2435
2477
  appServerArgs;
2436
2478
  host;
2479
+ attachments;
2437
2480
  rpc;
2438
2481
  emit;
2482
+ /**
2483
+ * 对外 requestId 的前缀,每个会话(即每个 app-server 进程)一份。JSON-RPC id 每个进程
2484
+ * 都从 0 数起,宿主按 requestId 记「已接受」,不加前缀的话重启后的新请求会撞上旧请求。
2485
+ */
2486
+ #requestIdPrefix = `codex-${NodeCrypto2.randomUUID()}:`;
2487
+ /** JSON-RPC id → 对外 requestId。`serverRequest/resolved` 里的 id 可能是数字也可能是字符串。 */
2488
+ requestIdFor(jsonRpcId) {
2489
+ return `${this.#requestIdPrefix}${String(jsonRpcId)}`;
2490
+ }
2439
2491
  /** 这一轮该用哪套三字段组合:只由这一轮的档决定(见 `codexNativeModeFor`)。 */
2440
2492
  #threadConfig() {
2441
2493
  return codexThreadConfigForNativeMode(codexNativeModeFor(this.runtimeMode, this.nativeModeId));
@@ -2455,7 +2507,8 @@ var CodexSessionRuntime = class {
2455
2507
  error: error instanceof Error ? error.message : String(error)
2456
2508
  });
2457
2509
  }
2458
- constructor(host, rpc, emit, input, cwd, appServerArgs, grants, declineRelay) {
2510
+ constructor(host, rpc, emit, input, cwd, appServerArgs, grants, declineRelay, attachments) {
2511
+ this.attachments = attachments;
2459
2512
  this.host = host;
2460
2513
  this.rpc = rpc;
2461
2514
  this.emit = emit;
@@ -2514,7 +2567,7 @@ var CodexSessionRuntime = class {
2514
2567
  if (kind === "drop") return;
2515
2568
  if (isFatalStderr(line)) {
2516
2569
  this.emit({
2517
- eventId: `codex-stderr-${Date.now()}`,
2570
+ eventId: nextEventId(),
2518
2571
  provider: CODEX_DRIVER_KIND,
2519
2572
  threadId: this.threadId,
2520
2573
  createdAt: nowIso(),
@@ -2557,21 +2610,24 @@ var CodexSessionRuntime = class {
2557
2610
  }
2558
2611
  dispatchMapped(method, params) {
2559
2612
  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
- }
2613
+ const rawId = isRecord3(params) ? params["requestId"] : void 0;
2614
+ if (typeof rawId !== "number" && typeof rawId !== "string") return;
2615
+ const resolvedId = this.requestIdFor(rawId);
2616
+ const pending = this.pendingApprovals.get(resolvedId);
2617
+ if (pending === void 0) return;
2618
+ pending.release();
2619
+ this.emit({
2620
+ eventId: nextEventId(),
2621
+ provider: CODEX_DRIVER_KIND,
2622
+ threadId: this.threadId,
2623
+ createdAt: nowIso(),
2624
+ type: "request.resolved",
2625
+ requestId: resolvedId,
2626
+ payload: {
2627
+ requestType: toRequestTypeFromKind(this.requestKinds.get(resolvedId)),
2628
+ resolution: params
2629
+ }
2630
+ });
2575
2631
  return;
2576
2632
  }
2577
2633
  for (const event of mapNotificationToEvents({
@@ -2750,9 +2806,9 @@ var CodexSessionRuntime = class {
2750
2806
  return;
2751
2807
  }
2752
2808
  if (method === "item/tool/requestUserInput") {
2753
- const requestId = String(id);
2809
+ const requestId = this.requestIdFor(id);
2754
2810
  const events = mapServerRequestToEvents({
2755
- id,
2811
+ requestId,
2756
2812
  method,
2757
2813
  params,
2758
2814
  threadId: this.threadId,
@@ -2783,9 +2839,9 @@ var CodexSessionRuntime = class {
2783
2839
  await this.rpc.respond(id, { decision });
2784
2840
  return;
2785
2841
  }
2786
- const requestId = String(id);
2842
+ const requestId = this.requestIdFor(id);
2787
2843
  const events = mapServerRequestToEvents({
2788
- id,
2844
+ requestId,
2789
2845
  method,
2790
2846
  params,
2791
2847
  threadId: this.threadId,
@@ -2802,6 +2858,10 @@ var CodexSessionRuntime = class {
2802
2858
  settle: (decision) => {
2803
2859
  this.pendingApprovals.delete(requestId);
2804
2860
  void this.rpc.respond(id, decision).catch((error) => this.#warnAnswerWriteFailed(method, id, error)).finally(resolve2);
2861
+ },
2862
+ release: () => {
2863
+ this.pendingApprovals.delete(requestId);
2864
+ resolve2();
2805
2865
  }
2806
2866
  });
2807
2867
  for (const event of events) this.emit(event);
@@ -2809,7 +2869,7 @@ var CodexSessionRuntime = class {
2809
2869
  }
2810
2870
  async respondToRequest(requestId, decision, reason, persist) {
2811
2871
  const pending = this.pendingApprovals.get(requestId);
2812
- if (!pending) throw new Error(`unknown request ${requestId}`);
2872
+ if (!pending) throw new Error(`Unknown pending Codex approval request: ${requestId}`);
2813
2873
  if (decision === "deny" && reason) this.declineRelay.queue(this.threadId, reason);
2814
2874
  let wire = { decision: decision === "allow" ? "accept" : "decline" };
2815
2875
  if (decision === "allow" && pending.kind === "mcp-elicitation") {
@@ -2825,7 +2885,7 @@ var CodexSessionRuntime = class {
2825
2885
  pending.settle(wire);
2826
2886
  const delivery = await ack.catch(() => "closed");
2827
2887
  this.emit({
2828
- eventId: `codex-req-${requestId}`,
2888
+ eventId: nextEventId(),
2829
2889
  provider: CODEX_DRIVER_KIND,
2830
2890
  threadId: this.threadId,
2831
2891
  createdAt: nowIso(),
@@ -2842,7 +2902,7 @@ var CodexSessionRuntime = class {
2842
2902
  }
2843
2903
  async respondToUserInput(requestId, answers) {
2844
2904
  const pending = this.pendingUserInputs.get(requestId);
2845
- if (!pending) throw new Error(`unknown user-input ${requestId}`);
2905
+ if (!pending) throw new Error(`Unknown pending Codex user input request: ${requestId}`);
2846
2906
  const mapped = toCodexUserInputAnswers(answers);
2847
2907
  if (mapped === void 0) {
2848
2908
  throw new Error(`invalid user input answers for request ${requestId}`);
@@ -2851,7 +2911,7 @@ var CodexSessionRuntime = class {
2851
2911
  pending.settle({ answers: mapped });
2852
2912
  const delivery = await ack.catch(() => "closed");
2853
2913
  this.emit({
2854
- eventId: `codex-ui-${requestId}`,
2914
+ eventId: nextEventId(),
2855
2915
  provider: CODEX_DRIVER_KIND,
2856
2916
  threadId: this.threadId,
2857
2917
  createdAt: nowIso(),
@@ -2903,7 +2963,7 @@ var CodexSessionRuntime = class {
2903
2963
  }
2904
2964
  async openThread(input) {
2905
2965
  const resume = input.discardResumeCursor === true ? void 0 : parseCodexResumeCursor(input.resumeCursor);
2906
- const requestedModel = input.launch.requestedModelId?.trim() || modelIdFromSelection(input.modelSelection);
2966
+ const requestedModel = modelIdFromSelection(input.modelSelection);
2907
2967
  const startParams = {
2908
2968
  ...this.threadConfigParams(),
2909
2969
  ...requestedModel ? { model: requestedModel } : {}
@@ -2939,7 +2999,7 @@ var CodexSessionRuntime = class {
2939
2999
  ...this.model ? { model: this.model } : {}
2940
3000
  };
2941
3001
  this.emit({
2942
- eventId: `codex-session-${this.threadId}`,
3002
+ eventId: nextEventId(),
2943
3003
  provider: CODEX_DRIVER_KIND,
2944
3004
  threadId: this.threadId,
2945
3005
  createdAt: nowIso(),
@@ -2957,24 +3017,45 @@ var CodexSessionRuntime = class {
2957
3017
  const effort = modelSelectionStringOption(input.modelSelection, "reasoningEffort") ?? DEFAULT_TEXT_GENERATION_REASONING_EFFORT;
2958
3018
  return buildCodexDeveloperInstructions(mode, { model, reasoningEffort: effort }, tools, tools);
2959
3019
  }
2960
- turnInput(input) {
3020
+ async turnInput(input) {
3021
+ const images = await this.#imageInputs(input.attachments);
2961
3022
  const items = [];
2962
3023
  const declined = this.declineRelay.take(this.threadId);
2963
3024
  const text = [declined, input.input ?? ""].filter((part) => part && part.length > 0).join("\n\n");
2964
3025
  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 });
3026
+ items.push(...images);
3027
+ return items;
3028
+ }
3029
+ /** 宿主只给附件 id:字节经附件接口读回,拼成 data URL 交给 codex。读不到就报错,不悄悄丢图。 */
3030
+ async #imageInputs(attachments) {
3031
+ const items = [];
3032
+ for (const attachment of attachments ?? []) {
3033
+ if (!isRecord3(attachment) || attachment["type"] !== "image") continue;
3034
+ const id = asString(attachment["id"]);
3035
+ const mimeType = asString(attachment["mimeType"]);
3036
+ if (!id || !mimeType) {
3037
+ throw new Error("codex image attachment is missing its id or mime type");
2970
3038
  }
3039
+ if (!this.attachments) {
3040
+ throw new Error(`codex cannot read image attachment ${id}: no attachment access`);
3041
+ }
3042
+ const bytes = await this.attachments.read(id);
3043
+ items.push({
3044
+ type: "image",
3045
+ url: `data:${mimeType};base64,${Buffer.from(bytes).toString("base64")}`
3046
+ });
2971
3047
  }
2972
3048
  return items;
2973
3049
  }
2974
3050
  async sendTurn(input) {
2975
3051
  if (!this.nativeThreadId) throw new Error("codex session has no native thread");
2976
3052
  if (hasConfiguredMcpServer(this.appServerArgs)) {
2977
- await this.rpc.request("mcpServer/reload", {}, 0).catch(() => void 0);
3053
+ await this.rpc.request("config/mcpServer/reload", void 0, 0).catch((error) => {
3054
+ this.host.log.warn("refreshing the codex MCP tool list before a turn failed", {
3055
+ threadId: this.threadId,
3056
+ error: error instanceof Error ? error.message : String(error)
3057
+ });
3058
+ });
2978
3059
  }
2979
3060
  const sandboxPolicy = this.#turnSandboxPolicy();
2980
3061
  const config = this.#threadConfig();
@@ -2983,7 +3064,7 @@ var CodexSessionRuntime = class {
2983
3064
  const serviceTier = modelSelectionStringOption(input.modelSelection, "serviceTier");
2984
3065
  const params = {
2985
3066
  threadId: this.nativeThreadId,
2986
- input: this.turnInput(input),
3067
+ input: await this.turnInput(input),
2987
3068
  approvalPolicy: config.approvalPolicy,
2988
3069
  approvalsReviewer: config.approvalsReviewer,
2989
3070
  sandboxPolicy,
@@ -3043,24 +3124,29 @@ var CodexSessionRuntime = class {
3043
3124
  );
3044
3125
  if (!this.nativeThreadId) return;
3045
3126
  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);
3127
+ if (target) {
3128
+ await this.rpc.request("turn/interrupt", { threadId: this.nativeThreadId, turnId: target }, 0).catch(() => void 0);
3129
+ }
3130
+ this.cancelPendingApprovals();
3131
+ }
3132
+ /** 把还挂着的审批答成「取消」,逐条发 request.resolved。 */
3133
+ cancelPendingApprovals() {
3134
+ for (const [requestId, pending] of this.pendingApprovals) {
3135
+ pending.settle(cancelWireFor(pending));
3136
+ this.emit({
3137
+ eventId: nextEventId(),
3138
+ provider: CODEX_DRIVER_KIND,
3139
+ threadId: this.threadId,
3140
+ createdAt: nowIso(),
3141
+ type: "request.resolved",
3142
+ requestId,
3143
+ payload: { requestType: toRequestTypeFromKind(pending.kind), decision: "cancel" }
3144
+ });
3145
+ }
3048
3146
  }
3049
3147
  async compactContext() {
3050
3148
  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 };
3149
+ await this.rpc.request("thread/compact/start", { threadId: this.nativeThreadId }, 0);
3064
3150
  }
3065
3151
  async readThread() {
3066
3152
  if (!this.nativeThreadId) return { threadId: this.threadId, turns: this.turns };
@@ -3115,7 +3201,7 @@ var CodexSessionRuntime = class {
3115
3201
  }
3116
3202
  this.closeCompleted = true;
3117
3203
  this.emit({
3118
- eventId: `codex-close-${this.threadId}`,
3204
+ eventId: nextEventId(),
3119
3205
  provider: CODEX_DRIVER_KIND,
3120
3206
  threadId: this.threadId,
3121
3207
  createdAt: nowIso(),
@@ -3470,10 +3556,6 @@ function serviceTierDescriptor(row) {
3470
3556
  currentValue: defaultTier
3471
3557
  };
3472
3558
  }
3473
- function isSupersededModel(row) {
3474
- if (isRecord3(row["upgradeInfo"])) return true;
3475
- return (asString(row["upgrade"]) ?? "").length > 0;
3476
- }
3477
3559
  function codexProbeModel(row) {
3478
3560
  if (!isRecord3(row)) return null;
3479
3561
  const slug = asString(row["model"]);
@@ -3486,7 +3568,6 @@ function codexProbeModel(row) {
3486
3568
  slug,
3487
3569
  ...displayName ? { name: codexModelDisplayName(displayName) } : {},
3488
3570
  ...row["isDefault"] === true ? { isDefault: true } : {},
3489
- ...isSupersededModel(row) ? { isLegacy: true } : {},
3490
3571
  ...row["hidden"] === true ? { hidden: true } : {},
3491
3572
  // 空表也给 `capabilities`:`{}` 与 `null` 对宿主是一回事(都不画那一格),
3492
3573
  // 但给对象保住「这只手确实答过这一问」这层意思。
@@ -3519,6 +3600,47 @@ async function listCodexModels(rpc) {
3519
3600
  return models;
3520
3601
  }
3521
3602
 
3603
+ // src/skills.ts
3604
+ var SKILLS_LIST_TIMEOUT_MS = 15e3;
3605
+ function codexProbeSkill(raw) {
3606
+ if (!isRecord3(raw)) return null;
3607
+ const name = asString(raw["name"]);
3608
+ const path = asString(raw["path"]);
3609
+ const scope = asString(raw["scope"]);
3610
+ if (!name || !path || scope === "repo") return null;
3611
+ const face = isRecord3(raw["interface"]) ? raw["interface"] : void 0;
3612
+ const description = asString(raw["description"]);
3613
+ const displayName = asString(face?.["displayName"]);
3614
+ const shortDescription = asString(face?.["shortDescription"]) || asString(raw["shortDescription"]);
3615
+ return {
3616
+ name,
3617
+ path,
3618
+ enabled: raw["enabled"] !== false,
3619
+ ...scope ? { scope } : {},
3620
+ ...description ? { description } : {},
3621
+ ...displayName ? { displayName } : {},
3622
+ ...shortDescription ? { shortDescription } : {}
3623
+ };
3624
+ }
3625
+ async function listCodexSkills(rpc) {
3626
+ let response;
3627
+ try {
3628
+ response = await rpc.request("skills/list", {}, SKILLS_LIST_TIMEOUT_MS);
3629
+ } catch {
3630
+ return void 0;
3631
+ }
3632
+ if (!isRecord3(response) || !Array.isArray(response["data"])) return void 0;
3633
+ const skills = /* @__PURE__ */ new Map();
3634
+ for (const entry of response["data"]) {
3635
+ if (!isRecord3(entry) || !Array.isArray(entry["skills"])) continue;
3636
+ for (const raw of entry["skills"]) {
3637
+ const skill = codexProbeSkill(raw);
3638
+ if (skill !== null) skills.set(skill.path, skill);
3639
+ }
3640
+ }
3641
+ return [...skills.values()].toSorted((left, right) => left.name.localeCompare(right.name));
3642
+ }
3643
+
3522
3644
  // src/signIn.ts
3523
3645
  var DEVICE_CODE_URL = /https?:\/\/\S+/;
3524
3646
  var DEVICE_CODE_VALUE = /\b([A-Z0-9]{4,}(?:-[A-Z0-9]{4,})+)\b/;
@@ -3708,8 +3830,7 @@ import * as NodeFSP2 from "node:fs/promises";
3708
3830
  import * as NodeOS3 from "node:os";
3709
3831
  import * as NodePath5 from "node:path";
3710
3832
  import {
3711
- mergeLaunchEnv as mergeLaunchEnv2,
3712
- brainSlotEnvKeys as brainSlotEnvKeys2
3833
+ mergeLaunchEnv as mergeLaunchEnv2
3713
3834
  } from "@ycodium-ai/plugin-api/executor";
3714
3835
  var CODEX_TIMEOUT_MS = 18e4;
3715
3836
  var EARLIER_CONTENT_TRUNCATION_MARKER = "[Earlier content truncated]\n\n";
@@ -3856,7 +3977,7 @@ async function collectStream(stream) {
3856
3977
  return out;
3857
3978
  }
3858
3979
  function createCodexTextGeneration(input) {
3859
- const run = async (operation, cwd, prompt, schema, modelSelection, launch, imagePaths, signal) => {
3980
+ const run = async (operation, cwd, prompt, schema, modelSelection, imagePaths, signal) => {
3860
3981
  if (input.recipe.binaryPath.length === 0) {
3861
3982
  throw new Error("Codex CLI is required but not available on PATH.");
3862
3983
  }
@@ -3865,33 +3986,16 @@ function createCodexTextGeneration(input) {
3865
3986
  const outputPath = NodePath5.join(tmp, "output.json");
3866
3987
  await NodeFSP2.writeFile(schemaPath, JSON.stringify(schema));
3867
3988
  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
3989
  const reasoningEffort = modelSelectionStringOption(modelSelection, "reasoningEffort") ?? DEFAULT_TEXT_GENERATION_REASONING_EFFORT;
3884
3990
  const serviceTier = modelSelectionStringOption(modelSelection, "serviceTier");
3885
- const model = asString(launchRecord.requestedModelId) ?? modelIdFromSelection(modelSelection) ?? "gpt-5.6-luna";
3991
+ const model = modelIdFromSelection(modelSelection) ?? "gpt-5.6-luna";
3886
3992
  const env = mergeLaunchEnv2({
3887
- brainSlots: brainSlotEnvKeys2(CODEX_CARD),
3888
3993
  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
3994
+ instanceTable: input.recipe.environment
3891
3995
  });
3892
3996
  const args = [
3893
3997
  "exec",
3894
- ...codexExecLaunchArgs(launchArgs),
3998
+ ...codexExecLaunchArgs(input.recipe.launchArgs),
3895
3999
  "--ephemeral",
3896
4000
  "--skip-git-repo-check",
3897
4001
  "-s",
@@ -3989,7 +4093,6 @@ function createCodexTextGeneration(input) {
3989
4093
  prompt,
3990
4094
  includeBranch ? COMMIT_WITH_BRANCH_SCHEMA : COMMIT_SCHEMA,
3991
4095
  inputRecord.modelSelection,
3992
- inputRecord.launch,
3993
4096
  [],
3994
4097
  signal
3995
4098
  );
@@ -4040,7 +4143,6 @@ function createCodexTextGeneration(input) {
4040
4143
  prompt,
4041
4144
  PR_SCHEMA,
4042
4145
  inputRecord.modelSelection,
4043
- inputRecord.launch,
4044
4146
  [],
4045
4147
  signal
4046
4148
  );
@@ -4080,7 +4182,6 @@ function createCodexTextGeneration(input) {
4080
4182
  prompt,
4081
4183
  BRANCH_SCHEMA,
4082
4184
  inputRecord.modelSelection,
4083
- inputRecord.launch,
4084
4185
  absoluteImagePaths(attachments),
4085
4186
  signal
4086
4187
  );
@@ -4117,7 +4218,6 @@ ${extra}` : ""}`;
4117
4218
  prompt,
4118
4219
  TITLE_SCHEMA,
4119
4220
  inputRecord.modelSelection,
4120
- inputRecord.launch,
4121
4221
  absoluteImagePaths(attachments),
4122
4222
  signal
4123
4223
  );
@@ -4146,7 +4246,6 @@ ${extra}` : ""}`;
4146
4246
  prompt,
4147
4247
  HANDOVER_SCHEMA,
4148
4248
  inputRecord.modelSelection,
4149
- inputRecord.launch,
4150
4249
  [],
4151
4250
  signal
4152
4251
  );
@@ -4205,18 +4304,15 @@ function accountToAuth2(account) {
4205
4304
  return { status: "unknown" };
4206
4305
  }
4207
4306
  }
4208
- function layoutFor(recipe, launchHome, nativeSubscription = false) {
4307
+ function layoutFor(recipe) {
4209
4308
  const environmentHome = recipe.environment["CODEX_HOME"];
4210
- if (nativeSubscription && launchHome?.trim()) {
4211
- return resolveCodexHomeLayout({ homePath: launchHome.trim(), shadowHomePath: "" });
4212
- }
4213
4309
  return resolveCodexHomeLayout({
4214
- homePath: launchHome?.trim() || recipe.homePath,
4310
+ homePath: recipe.homePath,
4215
4311
  shadowHomePath: recipe.shadowHomePath,
4216
4312
  ...environmentHome === void 0 ? {} : { environmentHome }
4217
4313
  });
4218
4314
  }
4219
- var createCodexInstance = (config, host) => {
4315
+ var createCodexInstance = (config, host, attachments) => {
4220
4316
  const recipe = parseCodexInstanceConfig(config);
4221
4317
  const events = new EventBus();
4222
4318
  const threads = /* @__PURE__ */ new Map();
@@ -4256,10 +4352,11 @@ var createCodexInstance = (config, host) => {
4256
4352
  await rpc.request("initialize", CODEX_INITIALIZE_PARAMS)
4257
4353
  );
4258
4354
  await rpc.notify("initialized");
4259
- return { version, models: input.listModels ? await listCodexModels(rpc) : void 0 };
4355
+ const models = input.listModels ? await listCodexModels(rpc) : void 0;
4356
+ return { version, models, skills: await listCodexSkills(rpc) };
4260
4357
  } catch (error) {
4261
4358
  if (error instanceof ExecutorSpawnRefusedError) throw error;
4262
- return { version, models: void 0 };
4359
+ return { version, models: void 0, skills: void 0 };
4263
4360
  } finally {
4264
4361
  await rpc?.close().catch(() => void 0);
4265
4362
  await handle?.kill("SIGTERM").catch(() => void 0);
@@ -4286,7 +4383,9 @@ var createCodexInstance = (config, host) => {
4286
4383
  };
4287
4384
  }
4288
4385
  const connectionError = codexApiConnectionError(recipe, recipe.environment);
4289
- const { version, models } = await probeAppServer({ listModels: connectionError === null });
4386
+ const { version, models, skills } = await probeAppServer({
4387
+ listModels: connectionError === null
4388
+ });
4290
4389
  if (connectionError) {
4291
4390
  return {
4292
4391
  installed: true,
@@ -4294,6 +4393,7 @@ var createCodexInstance = (config, host) => {
4294
4393
  readiness: "error",
4295
4394
  auth: probeAuth(),
4296
4395
  detail: connectionError,
4396
+ ...skills === void 0 ? {} : { skills },
4297
4397
  ...CODEX_NATIVE_MODE_REPORT
4298
4398
  };
4299
4399
  }
@@ -4303,6 +4403,7 @@ var createCodexInstance = (config, host) => {
4303
4403
  readiness: models === void 0 ? "unchecked" : "ready",
4304
4404
  auth: probeAuth(),
4305
4405
  ...models === void 0 ? {} : { models },
4406
+ ...skills === void 0 ? {} : { skills },
4306
4407
  // 宿主拿的是这一份。登录核验那份(`verifyCodexSignIn`)报了档、这里没报,
4307
4408
  // 界面就一直画宿主四档——2026-09-20 真发生过,浏览器里才看得见。
4308
4409
  ...CODEX_NATIVE_MODE_REPORT
@@ -4321,22 +4422,17 @@ var createCodexInstance = (config, host) => {
4321
4422
  await stopBooked(existing);
4322
4423
  threads.delete(input.threadId);
4323
4424
  }
4324
- const nativeSubscription = input.launch.nativeSubscription === true;
4325
- const layout = layoutFor(recipe, input.launch.homePath, nativeSubscription);
4425
+ const layout = layoutFor(recipe);
4326
4426
  await materializeCodexShadowHome(layout, host.log);
4327
4427
  const mcp = codexMcpLaunchConfig(input.mcpServers ?? []);
4328
4428
  const env = buildCodexSpawnEnv({
4329
4429
  recipe,
4330
4430
  layout,
4331
- // MCP 那几个变量是宿主挑好的 server 带来的,与绑定同一层:实例表盖不得。
4332
- launchEnv: { ...input.launch.processEnv, ...mcp.environment },
4333
- crushApiKeys: nativeSubscription || shouldCrushApiKeys(recipe.authMode)
4431
+ // MCP 那几个变量是宿主挑好的 server 带来的,铺在最终层:实例表盖不得。
4432
+ launchEnv: mcp.environment,
4433
+ crushApiKeys: shouldCrushApiKeys(recipe.authMode)
4334
4434
  });
4335
- const args = [
4336
- ...codexSpawnArgs(recipe, layout, nativeSubscription),
4337
- ...codexOverrideArgs(input.launch.launchArgOverrides),
4338
- ...mcp.args
4339
- ];
4435
+ const args = [...codexSpawnArgs(recipe, layout), ...mcp.args];
4340
4436
  const handle = await host.process.spawn({
4341
4437
  program: recipe.binaryPath,
4342
4438
  args,
@@ -4371,7 +4467,17 @@ var createCodexInstance = (config, host) => {
4371
4467
  }
4372
4468
  }
4373
4469
  });
4374
- runtime = new CodexSessionRuntime(host, rpc, (event) => events.push(event), input, cwd, args);
4470
+ runtime = new CodexSessionRuntime(
4471
+ host,
4472
+ rpc,
4473
+ (event) => events.push(event),
4474
+ input,
4475
+ cwd,
4476
+ args,
4477
+ void 0,
4478
+ void 0,
4479
+ attachments
4480
+ );
4375
4481
  try {
4376
4482
  await rpc.request("initialize", CODEX_INITIALIZE_PARAMS);
4377
4483
  await rpc.notify("initialized");
@@ -4439,10 +4545,6 @@ var createCodexInstance = (config, host) => {
4439
4545
  const booked = requireThread(threadId);
4440
4546
  return await booked.runtime.readThread();
4441
4547
  },
4442
- uploadFeedback: async (input) => {
4443
- const booked = requireThread(input.threadId);
4444
- return await booked.runtime.uploadFeedback(input.reason);
4445
- },
4446
4548
  events,
4447
4549
  dispose: async () => {
4448
4550
  if (disposed) return;
@@ -4628,13 +4730,15 @@ var createCodexInstance = (config, host) => {
4628
4730
  };
4629
4731
 
4630
4732
  // src/index.ts
4733
+ var capturedAttachments;
4631
4734
  var executors = [
4632
4735
  {
4633
4736
  card: CODEX_CARD,
4634
4737
  createInstance: async (config, host) => {
4635
4738
  host.log.info("createInstance");
4636
- return createCodexInstance(config, host);
4637
- }
4739
+ return createCodexInstance(config, host, capturedAttachments);
4740
+ },
4741
+ probeAccount: async ({ accountPath }) => probeCodexAccount(accountPath)
4638
4742
  }
4639
4743
  ];
4640
4744
  var index_default = definePlugin({
@@ -4642,6 +4746,7 @@ var index_default = definePlugin({
4642
4746
  apiVersion: 2,
4643
4747
  executors,
4644
4748
  activate(ctx) {
4749
+ capturedAttachments = ctx.attachments;
4645
4750
  ctx.log.info("codex executor ready");
4646
4751
  }
4647
4752
  });
@@ -0,0 +1,23 @@
1
+ {
2
+ "form": {
3
+ "binaryPath": {
4
+ "title": "Program path",
5
+ "description": "Path to the Codex program this provider runs.",
6
+ "placeholder": "codex"
7
+ },
8
+ "homePath": {
9
+ "title": "CODEX_HOME path",
10
+ "description": "Custom Codex home and config directory.",
11
+ "placeholder": "~/.codex"
12
+ },
13
+ "shadowHomePath": {
14
+ "title": "Shadow home path",
15
+ "description": "Account-specific Codex home. Keeps auth.json separate while sharing state from CODEX_HOME.",
16
+ "placeholder": "~/.codex-personal"
17
+ },
18
+ "launchArgs": {
19
+ "title": "Launch arguments",
20
+ "description": "Additional CLI arguments passed to codex app-server on session start."
21
+ }
22
+ }
23
+ }
@@ -0,0 +1,23 @@
1
+ {
2
+ "form": {
3
+ "binaryPath": {
4
+ "title": "程序路径",
5
+ "description": "此 provider 运行的 Codex 程序的路径。",
6
+ "placeholder": "codex"
7
+ },
8
+ "homePath": {
9
+ "title": "CODEX_HOME 路径",
10
+ "description": "自定义的 Codex 主目录与配置目录。",
11
+ "placeholder": "~/.codex"
12
+ },
13
+ "shadowHomePath": {
14
+ "title": "影子目录路径",
15
+ "description": "账户专用的 Codex 目录。auth.json 单独保存,其余状态仍与 CODEX_HOME 共用。",
16
+ "placeholder": "~/.codex-personal"
17
+ },
18
+ "launchArgs": {
19
+ "title": "启动参数",
20
+ "description": "会话启动时传给 codex app-server 的额外命令行参数。"
21
+ }
22
+ }
23
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ycodium-ai/agent-codex",
3
- "version": "0.2.2167",
3
+ "version": "0.2.2488",
4
4
  "description": "Codex executor: Codex CLI over the app-server JSON-RPC protocol.",
5
5
  "keywords": [
6
6
  "ycodium",
@@ -10,6 +10,7 @@
10
10
  "files": [
11
11
  "dist",
12
12
  "assets",
13
+ "locales",
13
14
  "README.md"
14
15
  ],
15
16
  "type": "module",
@@ -20,7 +21,7 @@
20
21
  "@types/node": "24.12.4",
21
22
  "esbuild": "0.28.1",
22
23
  "vite-plus": "0.2.2",
23
- "@ycodium-ai/plugin-api": "0.2.2167",
24
+ "@ycodium-ai/plugin-api": "0.2.2488",
24
25
  "ycodium-executor-testing": "0.0.0"
25
26
  },
26
27
  "ycodium": {