@mrciphersmith/keryx 0.3.5 → 0.3.6

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.
Files changed (76) hide show
  1. package/dist/cli.js +872 -316
  2. package/docs/README.md +2 -0
  3. package/package.json +1 -1
  4. package/src/gdskills/bundled/install-manifest.json +319 -76
  5. package/src/gdskills/bundled/stacks/django/agent-refs.json +3 -0
  6. package/src/gdskills/bundled/stacks/django/governance/eval.json +1763 -0
  7. package/src/gdskills/bundled/stacks/django/governance/scout.json +40 -0
  8. package/src/gdskills/bundled/stacks/django/pack.json +43 -0
  9. package/src/gdskills/bundled/stacks/django/rules/coding-style.mdc +80 -0
  10. package/src/gdskills/bundled/stacks/django/rules/patterns.mdc +92 -0
  11. package/src/gdskills/bundled/stacks/django/rules/security.mdc +92 -0
  12. package/src/gdskills/bundled/stacks/django/rules/testing.mdc +89 -0
  13. package/src/gdskills/bundled/stacks/django/skills/django-build-fix/SKILL.md +149 -0
  14. package/src/gdskills/bundled/stacks/django/skills/django-build-fix/evals.json +49 -0
  15. package/src/gdskills/bundled/stacks/django/skills/django-code-review/SKILL.md +137 -0
  16. package/src/gdskills/bundled/stacks/django/skills/django-code-review/evals.json +48 -0
  17. package/src/gdskills/bundled/stacks/django/skills/django-implementation/SKILL.md +147 -0
  18. package/src/gdskills/bundled/stacks/django/skills/django-implementation/evals.json +75 -0
  19. package/src/gdskills/bundled/stacks/django/skills/django-migrate/SKILL.md +166 -0
  20. package/src/gdskills/bundled/stacks/django/skills/django-migrate/evals.json +49 -0
  21. package/src/gdskills/bundled/stacks/django/skills/django-testing/SKILL.md +130 -0
  22. package/src/gdskills/bundled/stacks/django/skills/django-testing/evals.json +48 -0
  23. package/src/gdskills/bundled/stacks/fastapi/agent-refs.json +3 -0
  24. package/src/gdskills/bundled/stacks/fastapi/governance/eval.json +1777 -0
  25. package/src/gdskills/bundled/stacks/fastapi/governance/scout.json +34 -0
  26. package/src/gdskills/bundled/stacks/fastapi/pack.json +43 -0
  27. package/src/gdskills/bundled/stacks/fastapi/rules/coding-style.mdc +68 -0
  28. package/src/gdskills/bundled/stacks/fastapi/rules/patterns.mdc +108 -0
  29. package/src/gdskills/bundled/stacks/fastapi/rules/security.mdc +99 -0
  30. package/src/gdskills/bundled/stacks/fastapi/rules/testing.mdc +85 -0
  31. package/src/gdskills/bundled/stacks/fastapi/skills/fastapi-build-fix/SKILL.md +157 -0
  32. package/src/gdskills/bundled/stacks/fastapi/skills/fastapi-build-fix/evals.json +76 -0
  33. package/src/gdskills/bundled/stacks/fastapi/skills/fastapi-code-review/SKILL.md +150 -0
  34. package/src/gdskills/bundled/stacks/fastapi/skills/fastapi-code-review/evals.json +74 -0
  35. package/src/gdskills/bundled/stacks/fastapi/skills/fastapi-implementation/SKILL.md +158 -0
  36. package/src/gdskills/bundled/stacks/fastapi/skills/fastapi-implementation/evals.json +75 -0
  37. package/src/gdskills/bundled/stacks/fastapi/skills/fastapi-testing/SKILL.md +146 -0
  38. package/src/gdskills/bundled/stacks/fastapi/skills/fastapi-testing/evals.json +74 -0
  39. package/src/gdskills/bundled/stacks/java-kotlin-spring/agent-refs.json +3 -0
  40. package/src/gdskills/bundled/stacks/java-kotlin-spring/governance/eval.json +2194 -0
  41. package/src/gdskills/bundled/stacks/java-kotlin-spring/governance/scout.json +39 -0
  42. package/src/gdskills/bundled/stacks/java-kotlin-spring/pack.json +40 -0
  43. package/src/gdskills/bundled/stacks/java-kotlin-spring/rules/coding-style.mdc +67 -0
  44. package/src/gdskills/bundled/stacks/java-kotlin-spring/rules/patterns.mdc +65 -0
  45. package/src/gdskills/bundled/stacks/java-kotlin-spring/rules/security.mdc +69 -0
  46. package/src/gdskills/bundled/stacks/java-kotlin-spring/rules/testing.mdc +80 -0
  47. package/src/gdskills/bundled/stacks/java-kotlin-spring/skills/java-kotlin-spring-build-fix/SKILL.md +144 -0
  48. package/src/gdskills/bundled/stacks/java-kotlin-spring/skills/java-kotlin-spring-build-fix/evals.json +74 -0
  49. package/src/gdskills/bundled/stacks/java-kotlin-spring/skills/java-kotlin-spring-code-review/SKILL.md +129 -0
  50. package/src/gdskills/bundled/stacks/java-kotlin-spring/skills/java-kotlin-spring-code-review/evals.json +74 -0
  51. package/src/gdskills/bundled/stacks/java-kotlin-spring/skills/java-kotlin-spring-implementation/SKILL.md +147 -0
  52. package/src/gdskills/bundled/stacks/java-kotlin-spring/skills/java-kotlin-spring-implementation/evals.json +75 -0
  53. package/src/gdskills/bundled/stacks/java-kotlin-spring/skills/java-kotlin-spring-migrate/SKILL.md +139 -0
  54. package/src/gdskills/bundled/stacks/java-kotlin-spring/skills/java-kotlin-spring-migrate/evals.json +74 -0
  55. package/src/gdskills/bundled/stacks/java-kotlin-spring/skills/java-kotlin-spring-testing/SKILL.md +128 -0
  56. package/src/gdskills/bundled/stacks/java-kotlin-spring/skills/java-kotlin-spring-testing/evals.json +73 -0
  57. package/src/gdskills/bundled/stacks/python/agent-refs.json +2 -1
  58. package/src/gdskills/bundled/stacks/python/pack.json +1 -1
  59. package/src/gdskills/bundled/stacks/rust/agent-refs.json +3 -0
  60. package/src/gdskills/bundled/stacks/rust/governance/eval.json +1823 -0
  61. package/src/gdskills/bundled/stacks/rust/governance/scout.json +32 -0
  62. package/src/gdskills/bundled/stacks/rust/pack.json +42 -0
  63. package/src/gdskills/bundled/stacks/rust/rules/coding-style.mdc +93 -0
  64. package/src/gdskills/bundled/stacks/rust/rules/patterns.mdc +85 -0
  65. package/src/gdskills/bundled/stacks/rust/rules/security.mdc +85 -0
  66. package/src/gdskills/bundled/stacks/rust/rules/testing.mdc +82 -0
  67. package/src/gdskills/bundled/stacks/rust/skills/rust-build-fix/SKILL.md +141 -0
  68. package/src/gdskills/bundled/stacks/rust/skills/rust-build-fix/evals.json +78 -0
  69. package/src/gdskills/bundled/stacks/rust/skills/rust-code-review/SKILL.md +127 -0
  70. package/src/gdskills/bundled/stacks/rust/skills/rust-code-review/evals.json +72 -0
  71. package/src/gdskills/bundled/stacks/rust/skills/rust-implementation/SKILL.md +133 -0
  72. package/src/gdskills/bundled/stacks/rust/skills/rust-implementation/evals.json +79 -0
  73. package/src/gdskills/bundled/stacks/rust/skills/rust-testing/SKILL.md +130 -0
  74. package/src/gdskills/bundled/stacks/rust/skills/rust-testing/evals.json +75 -0
  75. package/src/gdskills/bundled/agents/python-build-fixer.md +0 -52
  76. package/src/gdskills/bundled/agents/python-code-auditor.md +0 -49
package/dist/cli.js CHANGED
@@ -690,7 +690,7 @@ function deviceCodeMethodLabel(provider) {
690
690
  return "SuperGrok Subscription";
691
691
  if (provider === "github-copilot")
692
692
  return "Login with GitHub Copilot";
693
- if (provider === "openai")
693
+ if (provider === "openai-codex")
694
694
  return "ChatGPT Plus/Pro (headless)";
695
695
  return "Device authorization";
696
696
  }
@@ -728,8 +728,13 @@ var init_catalog = __esm(() => {
728
728
  },
729
729
  {
730
730
  provider: "openai",
731
- label: "OpenAI",
732
- methods: ["oauth-pkce-loopback", "device-code", "api-key"]
731
+ label: "OpenAI API",
732
+ methods: ["api-key"]
733
+ },
734
+ {
735
+ provider: "openai-codex",
736
+ label: "ChatGPT / Codex subscription",
737
+ methods: ["device-code"]
733
738
  },
734
739
  {
735
740
  provider: "github-copilot",
@@ -1991,19 +1996,47 @@ var init_model_profile = __esm(() => {
1991
1996
  STRENGTH_TIERS = new Set(["light", "standard", "deep"]);
1992
1997
  });
1993
1998
 
1999
+ // src/lib/oauth/openai-token.ts
2000
+ function codexTokenMetadata(token) {
2001
+ if (typeof token !== "string" || token.length > 1e5 || token.split(".").length !== 3)
2002
+ return {};
2003
+ try {
2004
+ const payload = JSON.parse(Buffer.from(token.split(".")[1], "base64url").toString("utf8"));
2005
+ if (typeof payload !== "object" || payload === null)
2006
+ return {};
2007
+ const claims = payload;
2008
+ const auth = claims["https://api.openai.com/auth"];
2009
+ const accountId = typeof auth === "object" && auth !== null ? auth.chatgpt_account_id : undefined;
2010
+ return {
2011
+ ...typeof accountId === "string" && accountId.length > 0 && accountId.trim() === accountId && !/[\r\n]/.test(accountId) ? { accountId } : {},
2012
+ ...typeof claims.exp === "number" && Number.isFinite(claims.exp) && claims.exp > 0 ? { expiresAt: claims.exp * 1000 } : {}
2013
+ };
2014
+ } catch {
2015
+ return {};
2016
+ }
2017
+ }
2018
+
1994
2019
  // src/lib/oauth/grants.ts
1995
2020
  function loadOAuthGrant(provider, dir) {
1996
2021
  const grants = loadShellConfig(dir).oauthGrants;
1997
2022
  if (grants === undefined) {
1998
2023
  return;
1999
2024
  }
2000
- const grant = grants[provider];
2025
+ const grant = grants[provider] ?? (provider === "openai-codex" ? grants.openai : undefined);
2001
2026
  if (grant === undefined || typeof grant.access !== "string" || grant.access.length === 0) {
2002
2027
  return;
2003
2028
  }
2004
2029
  if (grant.method !== "device-code" && grant.method !== "oauth-pkce-loopback") {
2005
2030
  return;
2006
2031
  }
2032
+ if (provider === "openai-codex" || provider === "openai") {
2033
+ const metadata = codexTokenMetadata(grant.access);
2034
+ return {
2035
+ ...grant,
2036
+ ...grant.accountId === undefined && metadata.accountId !== undefined ? { accountId: metadata.accountId } : {},
2037
+ ...grant.expires === undefined && metadata.expiresAt !== undefined ? { expires: metadata.expiresAt } : {}
2038
+ };
2039
+ }
2007
2040
  return grant;
2008
2041
  }
2009
2042
  function saveOAuthGrant(provider, grant, dir) {
@@ -2012,15 +2045,17 @@ function saveOAuthGrant(provider, grant, dir) {
2012
2045
  }
2013
2046
  function deleteOAuthGrant(provider, dir) {
2014
2047
  const existing = { ...loadShellConfig(dir).oauthGrants ?? {} };
2015
- if (existing[provider] === undefined) {
2048
+ if (existing[provider] === undefined && !(provider === "openai-codex" && existing.openai !== undefined)) {
2016
2049
  return;
2017
2050
  }
2018
2051
  delete existing[provider];
2052
+ if (provider === "openai-codex")
2053
+ delete existing.openai;
2019
2054
  saveShellConfig({ oauthGrants: existing }, dir);
2020
2055
  }
2021
2056
  function listOAuthGrantProviders(dir) {
2022
2057
  const grants = loadShellConfig(dir).oauthGrants ?? {};
2023
- return Object.keys(grants).filter((name) => loadOAuthGrant(name, dir) !== undefined).sort();
2058
+ return [...new Set(Object.keys(grants).map((name) => name === "openai" ? "openai-codex" : name))].filter((name) => loadOAuthGrant(name, dir) !== undefined).sort();
2024
2059
  }
2025
2060
  function oauthGrantStatus(provider, dir, now = Date.now) {
2026
2061
  const grant = loadOAuthGrant(provider, dir);
@@ -2087,6 +2122,8 @@ function grantFromTokens(method, tokens, now = Date.now) {
2087
2122
  access: tokens.accessToken,
2088
2123
  ...tokens.refreshToken !== undefined ? { refresh: tokens.refreshToken } : {},
2089
2124
  ...tokens.expiresInSeconds !== undefined ? { expires: now() + tokens.expiresInSeconds * 1000 } : {},
2125
+ ...tokens.accountId !== undefined ? { accountId: tokens.accountId } : {},
2126
+ ...tokens.expiresInSeconds === undefined && tokens.expiresAt !== undefined ? { expires: tokens.expiresAt } : {},
2090
2127
  obtainedAt
2091
2128
  };
2092
2129
  }
@@ -2265,6 +2302,315 @@ var init_device_code = __esm(() => {
2265
2302
  };
2266
2303
  });
2267
2304
 
2305
+ // src/lib/oauth/openai-codex.ts
2306
+ function withSignal(base, signal) {
2307
+ if (signal?.aborted === true)
2308
+ throw new DeviceCodeError("cancelled", "device authorization was cancelled");
2309
+ const timeout = AbortSignal.timeout(30000);
2310
+ return { ...base, signal: signal === undefined ? timeout : AbortSignal.any([signal, timeout]) };
2311
+ }
2312
+ async function requestCodexDeviceCode(http) {
2313
+ const response = await http.fetch(`${OPENAI_CODEX_ISSUER}/api/accounts/deviceauth/usercode`, withSignal({
2314
+ method: "POST",
2315
+ headers: { "Content-Type": "application/json", Accept: "application/json", "User-Agent": "keryx" },
2316
+ body: JSON.stringify({ client_id: OPENAI_CODEX_CLIENT_ID })
2317
+ }, http.signal));
2318
+ const json = await response.json().catch(() => ({}));
2319
+ if (!response.ok || typeof json !== "object" || json === null) {
2320
+ throw new DeviceCodeError("failed", `ChatGPT device authorization failed (HTTP ${response.status})`);
2321
+ }
2322
+ const rec = json;
2323
+ rec.user_code ??= rec.usercode;
2324
+ if (typeof rec.device_auth_id !== "string" || rec.device_auth_id.length === 0 || typeof rec.user_code !== "string" || rec.user_code.length === 0 || [...rec.user_code].some((char) => char.charCodeAt(0) < 32 || char.charCodeAt(0) === 127)) {
2325
+ throw new DeviceCodeError("failed", "ChatGPT device authorization is missing device_auth_id / user_code");
2326
+ }
2327
+ const intervalSeconds = Number(rec.interval);
2328
+ return {
2329
+ deviceAuthId: rec.device_auth_id,
2330
+ userCode: rec.user_code,
2331
+ verificationUri: `${OPENAI_CODEX_ISSUER}/codex/device`,
2332
+ intervalMs: Number.isFinite(intervalSeconds) && intervalSeconds > 0 ? intervalSeconds * 1000 : 5000
2333
+ };
2334
+ }
2335
+ async function pollCodexDeviceToken(challenge, http) {
2336
+ const sleep = http.sleep;
2337
+ const now = http.now ?? Date.now;
2338
+ const deadline = now() + DEVICE_LOGIN_TIMEOUT_MS;
2339
+ while (now() < deadline) {
2340
+ if (http.signal?.aborted === true) {
2341
+ throw new DeviceCodeError("cancelled", "device authorization was cancelled");
2342
+ }
2343
+ const response = await http.fetch(`${OPENAI_CODEX_ISSUER}/api/accounts/deviceauth/token`, withSignal({
2344
+ method: "POST",
2345
+ headers: { "Content-Type": "application/json", Accept: "application/json", "User-Agent": "keryx" },
2346
+ body: JSON.stringify({ device_auth_id: challenge.deviceAuthId, user_code: challenge.userCode })
2347
+ }, http.signal));
2348
+ if (response.ok) {
2349
+ const data = await response.json();
2350
+ if (typeof data.authorization_code !== "string" || typeof data.code_verifier !== "string") {
2351
+ throw new DeviceCodeError("failed", "ChatGPT device token is missing authorization_code");
2352
+ }
2353
+ return exchangeCodexAuthorizationCode(data.authorization_code, data.code_verifier, http);
2354
+ }
2355
+ if (response.status !== 403 && response.status !== 404) {
2356
+ throw new DeviceCodeError("failed", `ChatGPT device token poll failed (HTTP ${response.status})`);
2357
+ }
2358
+ await abortableSleep(Math.min(challenge.intervalMs, Math.max(0, deadline - now())), sleep, http.signal);
2359
+ }
2360
+ throw new DeviceCodeError("timeout", "ChatGPT device authorization timed out");
2361
+ }
2362
+ async function exchangeCodexAuthorizationCode(code, codeVerifier, http) {
2363
+ const response = await http.fetch(`${OPENAI_CODEX_ISSUER}/oauth/token`, withSignal({
2364
+ method: "POST",
2365
+ headers: { "Content-Type": "application/x-www-form-urlencoded", Accept: "application/json" },
2366
+ body: new URLSearchParams({
2367
+ grant_type: "authorization_code",
2368
+ code,
2369
+ redirect_uri: `${OPENAI_CODEX_ISSUER}/deviceauth/callback`,
2370
+ client_id: OPENAI_CODEX_CLIENT_ID,
2371
+ code_verifier: codeVerifier
2372
+ }).toString()
2373
+ }, http.signal));
2374
+ const json = await response.json().catch(() => ({}));
2375
+ if (!response.ok || typeof json !== "object" || json === null || typeof json.access_token !== "string") {
2376
+ throw new DeviceCodeError("failed", `ChatGPT token exchange failed (HTTP ${response.status})`);
2377
+ }
2378
+ const rec = json;
2379
+ const access = codexTokenMetadata(rec.access_token);
2380
+ const accountId = codexTokenMetadata(rec.id_token).accountId ?? access.accountId;
2381
+ return {
2382
+ accessToken: rec.access_token,
2383
+ ...accountId !== undefined ? { accountId } : {},
2384
+ ...access.expiresAt !== undefined ? { expiresAt: access.expiresAt } : {},
2385
+ ...typeof rec.refresh_token === "string" ? { refreshToken: rec.refresh_token } : {},
2386
+ ...typeof rec.expires_in === "number" && Number.isFinite(rec.expires_in) && rec.expires_in > 0 ? { expiresInSeconds: rec.expires_in } : {}
2387
+ };
2388
+ }
2389
+ async function abortableSleep(ms, sleep, signal) {
2390
+ if (signal?.aborted === true)
2391
+ throw new DeviceCodeError("cancelled", "device authorization was cancelled");
2392
+ await new Promise((resolve, reject) => {
2393
+ let timer;
2394
+ const cleanup = () => {
2395
+ if (timer !== undefined)
2396
+ clearTimeout(timer);
2397
+ signal?.removeEventListener("abort", onAbort);
2398
+ };
2399
+ const onAbort = () => {
2400
+ cleanup();
2401
+ reject(new DeviceCodeError("cancelled", "device authorization was cancelled"));
2402
+ };
2403
+ const done = () => {
2404
+ cleanup();
2405
+ resolve();
2406
+ };
2407
+ signal?.addEventListener("abort", onAbort, { once: true });
2408
+ if (sleep === undefined)
2409
+ timer = setTimeout(done, ms);
2410
+ else
2411
+ sleep(ms).then(done, () => {
2412
+ cleanup();
2413
+ reject(new DeviceCodeError("failed", "device authorization wait failed"));
2414
+ });
2415
+ });
2416
+ }
2417
+ var OPENAI_CODEX_ISSUER = "https://auth.openai.com", OPENAI_CODEX_CLIENT_ID = "app_EMoamEEZ73f0CkXaXp7hrann", DEVICE_LOGIN_TIMEOUT_MS;
2418
+ var init_openai_codex = __esm(() => {
2419
+ init_device_code();
2420
+ DEVICE_LOGIN_TIMEOUT_MS = 15 * 60 * 1000;
2421
+ });
2422
+
2423
+ // src/lib/oauth/openai-subscription.ts
2424
+ import { resolve, join } from "path";
2425
+ function failure(detail) {
2426
+ return new SubscriptionAuthError(`ChatGPT subscription: ${detail}. ${LOGIN}`);
2427
+ }
2428
+ function enrich(grant) {
2429
+ const metadata = codexTokenMetadata(grant.access);
2430
+ return {
2431
+ ...grant,
2432
+ ...grant.accountId === undefined && metadata.accountId !== undefined ? { accountId: metadata.accountId } : {},
2433
+ ...grant.expires === undefined && metadata.expiresAt !== undefined ? { expires: metadata.expiresAt } : {}
2434
+ };
2435
+ }
2436
+ function cancelled(signal) {
2437
+ if (signal?.aborted)
2438
+ throw failure("authorization cancelled");
2439
+ }
2440
+ async function refreshOpenAiCodexGrant(input) {
2441
+ cancelled(input.signal);
2442
+ const loaded = loadOAuthGrant("openai-codex", input.configDir);
2443
+ if (loaded === undefined)
2444
+ throw failure("not signed in");
2445
+ const grant = enrich(loaded);
2446
+ const now = input.now ?? Date.now;
2447
+ if (!input.forceRefresh && grant.expires !== undefined && grant.expires > now() + 120000)
2448
+ return grant;
2449
+ if (!grant.refresh) {
2450
+ if (input.forceRefresh || grant.expires !== undefined && grant.expires <= now())
2451
+ throw failure("login expired and cannot be refreshed");
2452
+ return grant;
2453
+ }
2454
+ const key = resolve(input.configDir ?? keryxConfigDir());
2455
+ let pending = refreshes.get(key);
2456
+ if (pending === undefined) {
2457
+ pending = refreshWithLock(grant, input, key);
2458
+ refreshes.set(key, pending);
2459
+ pending.finally(() => {
2460
+ if (refreshes.get(key) === pending)
2461
+ refreshes.delete(key);
2462
+ }).catch(() => {});
2463
+ }
2464
+ if (input.signal === undefined)
2465
+ return pending;
2466
+ return new Promise((resolveGrant, reject) => {
2467
+ const abort = () => {
2468
+ cleanup();
2469
+ reject(failure("authorization cancelled"));
2470
+ };
2471
+ const cleanup = () => input.signal?.removeEventListener("abort", abort);
2472
+ input.signal?.addEventListener("abort", abort, { once: true });
2473
+ if (input.signal?.aborted) {
2474
+ abort();
2475
+ return;
2476
+ }
2477
+ pending.then((result) => {
2478
+ cleanup();
2479
+ resolveGrant(result);
2480
+ }, (error) => {
2481
+ cleanup();
2482
+ reject(error);
2483
+ });
2484
+ });
2485
+ }
2486
+ async function refreshWithLock(initial, input, configDir) {
2487
+ try {
2488
+ return await withFileLock(join(configDir, ".openai-codex-refresh.lock"), async () => {
2489
+ const current = loadOAuthGrant("openai-codex", input.configDir);
2490
+ if (current === undefined || current.obtainedAt !== initial.obtainedAt || initial.accountId !== undefined && current.accountId !== initial.accountId) {
2491
+ throw failure("login changed while waiting for token refresh");
2492
+ }
2493
+ if (current.access !== initial.access || current.refresh !== initial.refresh)
2494
+ return enrich(current);
2495
+ return refreshGrant(current, input);
2496
+ }, { timeoutMs: 35000, retryMs: 50, staleMs: 30000, heartbeatMs: 1000 });
2497
+ } catch (error) {
2498
+ if (error instanceof SubscriptionAuthError)
2499
+ throw error;
2500
+ throw failure("could not acquire or release the token refresh lock");
2501
+ }
2502
+ }
2503
+ async function refreshGrant(grant, input) {
2504
+ const now = input.now ?? Date.now;
2505
+ try {
2506
+ const response = await input.fetch(`${OPENAI_CODEX_ISSUER}/oauth/token`, {
2507
+ method: "POST",
2508
+ signal: AbortSignal.timeout(30000),
2509
+ headers: { "Content-Type": "application/json", Accept: "application/json" },
2510
+ body: JSON.stringify({ grant_type: "refresh_token", client_id: OPENAI_CODEX_CLIENT_ID, refresh_token: grant.refresh })
2511
+ });
2512
+ if (!response.ok)
2513
+ throw failure(`token refresh failed (HTTP ${response.status})`);
2514
+ const json = await response.json();
2515
+ if (typeof json !== "object" || json === null)
2516
+ throw failure("invalid token refresh response");
2517
+ const rec = json;
2518
+ if (typeof rec.access_token !== "string" || rec.access_token.length === 0)
2519
+ throw failure("token refresh returned no access token");
2520
+ const id = codexTokenMetadata(rec.id_token);
2521
+ const access = codexTokenMetadata(rec.access_token);
2522
+ const expires = typeof rec.expires_in === "number" && Number.isFinite(rec.expires_in) && rec.expires_in > 0 ? now() + rec.expires_in * 1000 : access.expiresAt;
2523
+ const accountId = id.accountId ?? access.accountId ?? grant.accountId;
2524
+ if (grant.accountId !== undefined && accountId !== grant.accountId)
2525
+ throw failure("refreshed account does not match saved login");
2526
+ const next = {
2527
+ ...grant,
2528
+ access: rec.access_token,
2529
+ ...typeof rec.refresh_token === "string" && rec.refresh_token.length > 0 ? { refresh: rec.refresh_token } : {},
2530
+ ...accountId !== undefined ? { accountId } : {},
2531
+ lastRefreshedAt: new Date(now()).toISOString()
2532
+ };
2533
+ if (expires !== undefined)
2534
+ next.expires = expires;
2535
+ else
2536
+ delete next.expires;
2537
+ const current = loadOAuthGrant("openai-codex", input.configDir);
2538
+ if (current?.access !== grant.access || current.refresh !== grant.refresh)
2539
+ throw failure("login changed during token refresh");
2540
+ saveOAuthGrant("openai-codex", next, input.configDir);
2541
+ const persisted = loadOAuthGrant("openai-codex", input.configDir);
2542
+ if (persisted?.access !== next.access || persisted.refresh !== next.refresh)
2543
+ throw failure("could not save refreshed login");
2544
+ return next;
2545
+ } catch (error) {
2546
+ if (error instanceof SubscriptionAuthError)
2547
+ throw error;
2548
+ throw failure("token refresh failed");
2549
+ }
2550
+ }
2551
+ async function ensureOpenAiCodexGrant(input) {
2552
+ const grant = await refreshOpenAiCodexGrant(input);
2553
+ cancelled(input.signal);
2554
+ if (grant.accountId === undefined || grant.accountId.length === 0 || /[\r\n]/.test(grant.accountId))
2555
+ throw failure("saved login has no account identity");
2556
+ if (grant.expires !== undefined && grant.expires <= (input.now ?? Date.now)())
2557
+ throw failure("login has expired");
2558
+ return { ...grant, accountId: grant.accountId };
2559
+ }
2560
+ var refreshes, LOGIN = "Run `keryx auth login openai-codex`.", SubscriptionAuthError;
2561
+ var init_openai_subscription = __esm(() => {
2562
+ init_config_dir();
2563
+ init_fs();
2564
+ init_grants();
2565
+ init_openai_codex();
2566
+ refreshes = new Map;
2567
+ SubscriptionAuthError = class SubscriptionAuthError extends Error {
2568
+ };
2569
+ });
2570
+
2571
+ // src/commands/subscription-models.ts
2572
+ async function fetchOpenAiCodexModels(fetchFn, opts = {}) {
2573
+ const controller = new AbortController;
2574
+ const timer = setTimeout(() => controller.abort(), opts.timeoutMs ?? 8000);
2575
+ let authenticated = false;
2576
+ try {
2577
+ const grant = await ensureOpenAiCodexGrant({ fetch: fetchFn, signal: controller.signal, ...opts.configDir !== undefined ? { configDir: opts.configDir } : {} });
2578
+ authenticated = true;
2579
+ const response = await fetchFn(`${OPENAI_CODEX_PICKER.baseUrl}/models?client_version=${CODEX_CATALOG_CLIENT_VERSION}`, {
2580
+ headers: { Authorization: `Bearer ${grant.access}`, "ChatGPT-Account-ID": grant.accountId, originator: "keryx", Accept: "application/json" },
2581
+ signal: controller.signal,
2582
+ redirect: "error"
2583
+ });
2584
+ if (!response.ok) {
2585
+ return { models: [], source: "fallback", failure: { kind: response.status === 401 || response.status === 403 ? "rejected" : "http", status: response.status } };
2586
+ }
2587
+ const body = await response.json();
2588
+ const entries = typeof body === "object" && body !== null && "models" in body && Array.isArray(body.models) ? body.models : [];
2589
+ const models = [...new Set(entries.flatMap((entry) => {
2590
+ if (typeof entry !== "object" || entry === null || !("slug" in entry) || typeof entry.slug !== "string" || entry.slug.length === 0)
2591
+ return [];
2592
+ if (!("visibility" in entry) || entry.visibility !== "list")
2593
+ return [];
2594
+ return [entry.slug];
2595
+ }))];
2596
+ return models.length > 0 ? { models, source: "live" } : { models: [], source: "fallback", failure: { kind: "empty" } };
2597
+ } catch {
2598
+ return { models: [], source: "fallback", failure: controller.signal.aborted || authenticated ? { kind: "unreachable", detail: controller.signal.aborted ? "model listing timed out" : "subscription model listing failed" } : { kind: "rejected", status: 401, detail: "Run `keryx auth login openai-codex` to connect your ChatGPT subscription." } };
2599
+ } finally {
2600
+ clearTimeout(timer);
2601
+ }
2602
+ }
2603
+ var OPENAI_CODEX_PICKER, CODEX_CATALOG_CLIENT_VERSION = "0.157.0";
2604
+ var init_subscription_models = __esm(() => {
2605
+ init_openai_subscription();
2606
+ OPENAI_CODEX_PICKER = {
2607
+ name: "openai-codex",
2608
+ label: "ChatGPT / Codex",
2609
+ baseUrl: "https://chatgpt.com/backend-api/codex",
2610
+ models: ["gpt-5.3-codex"]
2611
+ };
2612
+ });
2613
+
2268
2614
  // src/lib/oauth/copilot.ts
2269
2615
  function copilotApiBaseFromExchange(raw) {
2270
2616
  if (typeof raw !== "string" || raw.length === 0) {
@@ -2357,87 +2703,6 @@ var init_copilot = __esm(() => {
2357
2703
  init_device_code();
2358
2704
  });
2359
2705
 
2360
- // src/lib/oauth/openai-codex.ts
2361
- function withSignal(base, signal) {
2362
- return signal === undefined ? base : { ...base, signal };
2363
- }
2364
- async function requestCodexDeviceCode(http) {
2365
- const response = await http.fetch(`${OPENAI_CODEX_ISSUER}/api/accounts/deviceauth/usercode`, withSignal({
2366
- method: "POST",
2367
- headers: { "Content-Type": "application/json", Accept: "application/json", "User-Agent": "keryx" },
2368
- body: JSON.stringify({ client_id: OPENAI_CODEX_CLIENT_ID })
2369
- }, http.signal));
2370
- const json = await response.json().catch(() => ({}));
2371
- if (!response.ok || typeof json !== "object" || json === null) {
2372
- throw new DeviceCodeError("failed", `ChatGPT device authorization failed (HTTP ${response.status})`);
2373
- }
2374
- const rec = json;
2375
- if (typeof rec.device_auth_id !== "string" || typeof rec.user_code !== "string") {
2376
- throw new DeviceCodeError("failed", "ChatGPT device authorization is missing device_auth_id / user_code");
2377
- }
2378
- const intervalSeconds = Number(rec.interval);
2379
- return {
2380
- deviceAuthId: rec.device_auth_id,
2381
- userCode: rec.user_code,
2382
- verificationUri: `${OPENAI_CODEX_ISSUER}/codex/device`,
2383
- intervalMs: Number.isFinite(intervalSeconds) && intervalSeconds > 0 ? intervalSeconds * 1000 : 5000
2384
- };
2385
- }
2386
- async function pollCodexDeviceToken(challenge, http) {
2387
- const sleep = http.sleep ?? ((ms) => new Promise((resolve) => setTimeout(resolve, ms)));
2388
- const now = http.now ?? Date.now;
2389
- const deadline = now() + 5 * 60 * 1000;
2390
- while (now() < deadline) {
2391
- if (http.signal?.aborted === true) {
2392
- throw new DeviceCodeError("cancelled", "device authorization was cancelled");
2393
- }
2394
- const response = await http.fetch(`${OPENAI_CODEX_ISSUER}/api/accounts/deviceauth/token`, withSignal({
2395
- method: "POST",
2396
- headers: { "Content-Type": "application/json", Accept: "application/json", "User-Agent": "keryx" },
2397
- body: JSON.stringify({ device_auth_id: challenge.deviceAuthId, user_code: challenge.userCode })
2398
- }, http.signal));
2399
- if (response.ok) {
2400
- const data = await response.json();
2401
- if (typeof data.authorization_code !== "string" || typeof data.code_verifier !== "string") {
2402
- throw new DeviceCodeError("failed", "ChatGPT device token is missing authorization_code");
2403
- }
2404
- return exchangeCodexAuthorizationCode(data.authorization_code, data.code_verifier, http);
2405
- }
2406
- if (response.status !== 403 && response.status !== 404) {
2407
- throw new DeviceCodeError("failed", `ChatGPT device token poll failed (HTTP ${response.status})`);
2408
- }
2409
- await sleep(challenge.intervalMs + POLL_SAFETY_MARGIN_MS2);
2410
- }
2411
- throw new DeviceCodeError("timeout", "ChatGPT device authorization timed out");
2412
- }
2413
- async function exchangeCodexAuthorizationCode(code, codeVerifier, http) {
2414
- const response = await http.fetch(`${OPENAI_CODEX_ISSUER}/oauth/token`, withSignal({
2415
- method: "POST",
2416
- headers: { "Content-Type": "application/x-www-form-urlencoded", Accept: "application/json" },
2417
- body: new URLSearchParams({
2418
- grant_type: "authorization_code",
2419
- code,
2420
- redirect_uri: `${OPENAI_CODEX_ISSUER}/deviceauth/callback`,
2421
- client_id: OPENAI_CODEX_CLIENT_ID,
2422
- code_verifier: codeVerifier
2423
- }).toString()
2424
- }, http.signal));
2425
- const json = await response.json().catch(() => ({}));
2426
- if (!response.ok || typeof json !== "object" || json === null || typeof json.access_token !== "string") {
2427
- throw new DeviceCodeError("failed", `ChatGPT token exchange failed (HTTP ${response.status})`);
2428
- }
2429
- const rec = json;
2430
- return {
2431
- accessToken: rec.access_token,
2432
- ...typeof rec.refresh_token === "string" ? { refreshToken: rec.refresh_token } : {},
2433
- ...typeof rec.expires_in === "number" ? { expiresInSeconds: rec.expires_in } : {}
2434
- };
2435
- }
2436
- var OPENAI_CODEX_ISSUER = "https://auth.openai.com", OPENAI_CODEX_CLIENT_ID = "app_EMoamEEZ73f0CkXaXp7hrann", POLL_SAFETY_MARGIN_MS2 = 3000;
2437
- var init_openai_codex = __esm(() => {
2438
- init_device_code();
2439
- });
2440
-
2441
2706
  // src/lib/oauth/login.ts
2442
2707
  function httpOf(input) {
2443
2708
  return {
@@ -2452,7 +2717,7 @@ function present(challenge, provider) {
2452
2717
  userCode: challenge.userCode,
2453
2718
  verificationUri: challenge.verificationUri,
2454
2719
  ...challenge.verificationUriComplete !== undefined ? { verificationUriComplete: challenge.verificationUriComplete } : {},
2455
- instructions: `Open ${challenge.verificationUri} and enter code: ${challenge.userCode} (${deviceCodeMethodLabel(provider)})`
2720
+ instructions: `Open ${challenge.verificationUri} and enter code: ${challenge.userCode} (${deviceCodeMethodLabel(provider)}).` + (provider === "openai-codex" ? " Enable device code login in ChatGPT Settings > Security if required. Cancel with Ctrl+C." : "")
2456
2721
  };
2457
2722
  }
2458
2723
  async function loginRfc8628(input) {
@@ -2497,24 +2762,37 @@ async function loginDeviceCode(input) {
2497
2762
  return { ok: false, error: `${input.provider} does not support device authorization` };
2498
2763
  }
2499
2764
  try {
2500
- const tokens = input.provider === "openai" ? await loginCodex(input) : await loginRfc8628(input);
2765
+ const tokens = input.provider === "openai-codex" ? await loginCodex(input) : await loginRfc8628(input);
2501
2766
  saveOAuthGrant(input.provider, grantFromTokens("device-code", tokens, input.now), input.dir);
2767
+ if (input.provider === "openai-codex" && loadOAuthGrant(input.provider, input.dir)?.access !== tokens.accessToken) {
2768
+ throw new DeviceCodeError("failed", "Could not save ChatGPT login securely. Check the Keryx config directory permissions.");
2769
+ }
2502
2770
  return { ok: true, provider: input.provider };
2503
2771
  } catch (err) {
2504
- const message = err instanceof DeviceCodeError ? err.message : err instanceof Error ? err.message : "device authorization failed";
2772
+ const message = input.signal?.aborted === true ? "Device authorization was cancelled." : input.provider === "openai-codex" && !(err instanceof DeviceCodeError) ? "ChatGPT device authorization failed. Run `keryx auth login openai-codex` to retry." : err instanceof DeviceCodeError ? err.message : err instanceof Error ? err.message : "device authorization failed";
2505
2773
  return { ok: false, error: message };
2506
2774
  }
2507
2775
  }
2508
2776
  async function loginCodex(input) {
2509
- const http = httpOf(input);
2777
+ const timeout = AbortSignal.timeout(15 * 60 * 1000);
2778
+ const http = { ...httpOf(input), signal: input.signal === undefined ? timeout : AbortSignal.any([input.signal, timeout]) };
2510
2779
  const challenge = await requestCodexDeviceCode(http);
2511
2780
  input.onChallenge(present({
2512
2781
  userCode: challenge.userCode,
2513
2782
  verificationUri: challenge.verificationUri
2514
- }, "openai"));
2515
- return pollCodexDeviceToken(challenge, http);
2783
+ }, "openai-codex"));
2784
+ const tokens = await pollCodexDeviceToken(challenge, http);
2785
+ if (tokens.accountId === undefined) {
2786
+ throw new DeviceCodeError("failed", "ChatGPT login returned no account identity. Run `keryx auth login openai-codex` to retry.");
2787
+ }
2788
+ return tokens;
2516
2789
  }
2517
2790
  async function refreshProviderGrant(provider, http, dir) {
2791
+ if (provider === "openai-codex" || provider === "openai") {
2792
+ if (loadOAuthGrant("openai-codex", dir) === undefined)
2793
+ return;
2794
+ return refreshOpenAiCodexGrant({ ...http, ...dir !== undefined ? { configDir: dir } : {} });
2795
+ }
2518
2796
  const grant = loadOAuthGrant(provider, dir);
2519
2797
  if (grant === undefined || !grantNeedsRefresh(grant, http.now)) {
2520
2798
  return grant;
@@ -2537,9 +2815,6 @@ async function refreshProviderGrant(provider, http, dir) {
2537
2815
  saveOAuthGrant(provider, next, dir);
2538
2816
  return next;
2539
2817
  }
2540
- if (provider === "openai") {
2541
- return grant;
2542
- }
2543
2818
  const tokens = await refreshAccessToken({
2544
2819
  tokenEndpoint: endpoints.tokenEndpoint,
2545
2820
  clientId: endpoints.clientId,
@@ -2566,7 +2841,7 @@ async function refreshSavedGrants(http, dir, providers = SESSION_REFRESHED_GRANT
2566
2841
  }
2567
2842
  continue;
2568
2843
  }
2569
- if (!grantNeedsRefresh(grant, http.now))
2844
+ if (!grantNeedsRefresh(grant, http.now) && !(provider === "openai-codex" && grant.expires === undefined))
2570
2845
  continue;
2571
2846
  try {
2572
2847
  await refreshProviderGrant(provider, http, dir);
@@ -2578,7 +2853,7 @@ async function refreshSavedGrants(http, dir, providers = SESSION_REFRESHED_GRANT
2578
2853
  return warnings;
2579
2854
  }
2580
2855
  function logoutProvider(provider, dir) {
2581
- deleteOAuthGrant(provider, dir);
2856
+ deleteOAuthGrant(provider === "openai" ? "openai-codex" : provider, dir);
2582
2857
  }
2583
2858
  var SESSION_REFRESHED_GRANTS;
2584
2859
  var init_login = __esm(() => {
@@ -2588,7 +2863,8 @@ var init_login = __esm(() => {
2588
2863
  init_device_code();
2589
2864
  init_grants();
2590
2865
  init_openai_codex();
2591
- SESSION_REFRESHED_GRANTS = ["grok", "github-copilot"];
2866
+ init_openai_subscription();
2867
+ SESSION_REFRESHED_GRANTS = ["grok", "github-copilot", "openai-codex"];
2592
2868
  });
2593
2869
 
2594
2870
  // src/lib/caller-session.ts
@@ -3635,6 +3911,40 @@ var init_provider_catalog_cache = __esm(() => {
3635
3911
  CATALOG_CACHE_TTL_MS = 5 * 60 * 1000;
3636
3912
  });
3637
3913
 
3914
+ // src/lib/oauth/open-url.ts
3915
+ import { spawn } from "child_process";
3916
+ function browserOpenPlan(url, platform = process.platform, env = process.env) {
3917
+ if (url.length === 0) {
3918
+ return;
3919
+ }
3920
+ if (platform === "darwin") {
3921
+ return { cmd: "open", args: [url] };
3922
+ }
3923
+ if (platform === "win32") {
3924
+ return { cmd: "cmd", args: ["/c", "start", "", url] };
3925
+ }
3926
+ if (typeof env.DISPLAY === "string" && env.DISPLAY.length > 0) {
3927
+ return { cmd: "xdg-open", args: [url] };
3928
+ }
3929
+ if (typeof env.WAYLAND_DISPLAY === "string" && env.WAYLAND_DISPLAY.length > 0) {
3930
+ return { cmd: "xdg-open", args: [url] };
3931
+ }
3932
+ return;
3933
+ }
3934
+ function openVerificationUrl(url, deps = {}) {
3935
+ const plan = browserOpenPlan(url, deps.platform ?? process.platform, deps.env ?? process.env);
3936
+ if (plan === undefined) {
3937
+ return;
3938
+ }
3939
+ const spawnFn = deps.spawn ?? spawn;
3940
+ try {
3941
+ const child = spawnFn(plan.cmd, plan.args, { stdio: "ignore", detached: true });
3942
+ child.on("error", () => {});
3943
+ child.unref();
3944
+ } catch {}
3945
+ }
3946
+ var init_open_url = () => {};
3947
+
3638
3948
  // src/harness/policy/engine.ts
3639
3949
  import path8 from "path";
3640
3950
  function baseOutcomeFor(risk, defaults) {
@@ -4151,15 +4461,12 @@ async function detectProviders(deps) {
4151
4461
  if (typeof anthropicKey === "string" && anthropicKey.length > 0) {
4152
4462
  detected.push({ name: "anthropic", models: [...ANTHROPIC_MODELS] });
4153
4463
  }
4154
- const openaiKey = deps.env.OPENAI_API_KEY;
4155
- if (typeof openaiKey === "string" && openaiKey.length > 0) {
4156
- detected.push({ name: "openai", models: [...OPENAI_MODELS] });
4157
- }
4464
+ detected.push({ name: OPENAI_CODEX_PICKER.name, label: OPENAI_CODEX_PICKER.label, models: [...OPENAI_CODEX_PICKER.models], note: "ChatGPT subscription \xB7 browser device login" });
4158
4465
  const geminiKey = deps.env.GEMINI_API_KEY ?? deps.env.GOOGLE_API_KEY;
4159
4466
  if (typeof geminiKey === "string" && geminiKey.length > 0) {
4160
4467
  detected.push({ name: "gemini", models: [...GEMINI_MODELS] });
4161
4468
  }
4162
- for (const p of allOpenAiCompatProviders()) {
4469
+ for (const p of allOpenAiCompatProviders(deps.configDir)) {
4163
4470
  if (!isProviderPlatformSupported(p, platform)) {
4164
4471
  continue;
4165
4472
  }
@@ -4219,6 +4526,31 @@ function parseChoice(line, count) {
4219
4526
  function toSelection(provider, model) {
4220
4527
  return provider.baseUrl === undefined ? { provider: provider.name, model } : { provider: provider.name, model, baseUrl: provider.baseUrl };
4221
4528
  }
4529
+ async function loginSubscriptionInReadline(io, deps, fetchFn) {
4530
+ const controller = new AbortController;
4531
+ const cancel = () => controller.abort();
4532
+ const signal = deps.signal === undefined ? controller.signal : AbortSignal.any([controller.signal, deps.signal]);
4533
+ process.on("SIGINT", cancel);
4534
+ try {
4535
+ io.write(`Connect your ChatGPT subscription (Ctrl+C to cancel).
4536
+ `);
4537
+ const result = await loginDeviceCode({
4538
+ provider: "openai-codex",
4539
+ fetch: (url, init) => fetchFn(url, init),
4540
+ signal,
4541
+ ...deps.configDir === undefined ? {} : { dir: deps.configDir },
4542
+ onChallenge: (challenge) => {
4543
+ io.write(`${challenge.instructions}
4544
+ `);
4545
+ (deps.openVerificationUrl ?? openVerificationUrl)(challenge.verificationUriComplete ?? challenge.verificationUri);
4546
+ }
4547
+ });
4548
+ if (!result.ok)
4549
+ throw new SubscriptionSelectionError(signal.aborted ? "ChatGPT subscription login cancelled" : result.error);
4550
+ } finally {
4551
+ process.removeListener("SIGINT", cancel);
4552
+ }
4553
+ }
4222
4554
  async function pickProviderModel(io, detected, deps = {}) {
4223
4555
  const fetchFn = deps.fetch ?? globalThis.fetch;
4224
4556
  const env = deps.env ?? process.env;
@@ -4266,9 +4598,20 @@ async function pickProviderModel(io, detected, deps = {}) {
4266
4598
  chosenProvider = provider;
4267
4599
  break;
4268
4600
  }
4601
+ if (chosenProvider.name === "openai-codex" && loadOAuthGrant("openai-codex", deps.configDir) === undefined) {
4602
+ await loginSubscriptionInReadline(io, deps, fetchFn);
4603
+ }
4269
4604
  io.write(`Fetching models for ${chosenProvider.name}\u2026
4270
4605
  `);
4271
- const resolved = await resolveModelsForPicker(fetchFn, chosenProvider, env);
4606
+ const modelOptions = deps.configDir === undefined ? {} : { configDir: deps.configDir };
4607
+ let resolved = await resolveModelsForPicker(fetchFn, chosenProvider, env, modelOptions);
4608
+ if (chosenProvider.name === "openai-codex" && resolved.failure?.kind === "rejected") {
4609
+ await loginSubscriptionInReadline(io, deps, fetchFn);
4610
+ resolved = await resolveModelsForPicker(fetchFn, chosenProvider, env, modelOptions);
4611
+ }
4612
+ if (chosenProvider.name === "openai-codex" && (resolved.source !== "live" || resolved.models.length === 0)) {
4613
+ throw new SubscriptionSelectionError("ChatGPT subscription models are unavailable. Try again or run `keryx auth login openai-codex`.");
4614
+ }
4272
4615
  const models = resolved.models.length > 0 ? resolved.models : chosenProvider.models;
4273
4616
  if (resolved.source === "live") {
4274
4617
  io.write(` (${models.length} model(s) from API)
@@ -4303,12 +4646,18 @@ async function pickProviderModel(io, detected, deps = {}) {
4303
4646
  return toSelection(chosenProvider, model);
4304
4647
  }
4305
4648
  }
4306
- var DEFAULT_OLLAMA_BASE_URL = "http://localhost:11434", FAKE_MODELS;
4649
+ var DEFAULT_OLLAMA_BASE_URL = "http://localhost:11434", FAKE_MODELS, SubscriptionSelectionError;
4307
4650
  var init_select = __esm(() => {
4651
+ init_subscription_models();
4652
+ init_grants();
4653
+ init_login();
4654
+ init_open_url();
4308
4655
  init_guard();
4309
4656
  init_providers();
4310
4657
  init_curated_model_lists();
4311
4658
  FAKE_MODELS = ["fake-echo"];
4659
+ SubscriptionSelectionError = class SubscriptionSelectionError extends Error {
4660
+ };
4312
4661
  });
4313
4662
 
4314
4663
  // src/harness/provider-catalog.ts
@@ -4326,6 +4675,12 @@ async function buildCatalogEntry(detected, deps, fetchedAt) {
4326
4675
  ...detected.label !== undefined ? { label: detected.label } : {}
4327
4676
  };
4328
4677
  }
4678
+ if (detected.name === "openai-codex") {
4679
+ if (loadOAuthGrant("openai-codex", deps.dir) === undefined)
4680
+ return;
4681
+ const result = await fetchOpenAiCodexModels(deps.fetch, { ...deps.dir !== undefined ? { configDir: deps.dir } : {}, timeoutMs: deps.timeoutMs ?? CATALOG_FETCH_TIMEOUT_MS });
4682
+ return { name: detected.name, status: classifyModelsStatus(result), models: result.models, fallbackModels: [...detected.models], fetchedAt, label: detected.label ?? "ChatGPT / Codex" };
4683
+ }
4329
4684
  const registry = providerByName(detected.name, deps.dir);
4330
4685
  if (registry === undefined) {
4331
4686
  return {
@@ -4338,7 +4693,7 @@ async function buildCatalogEntry(detected, deps, fetchedAt) {
4338
4693
  };
4339
4694
  }
4340
4695
  const requiresApiKey = registry.requiresApiKey ?? true;
4341
- const apiKey = providerApiKey(registry, deps.env) ?? registry.apiKey;
4696
+ const apiKey = providerApiKey(registry, envWithSavedApiKeys(deps.env, deps.dir)) ?? registry.apiKey;
4342
4697
  if (requiresApiKey && (apiKey === undefined || apiKey.length === 0)) {
4343
4698
  return;
4344
4699
  }
@@ -4375,6 +4730,7 @@ async function refreshProviderCatalog(deps) {
4375
4730
  const detectDeps = {
4376
4731
  fetch: deps.fetch,
4377
4732
  env: deps.env,
4733
+ ...deps.dir !== undefined ? { configDir: deps.dir } : {},
4378
4734
  ...deps.baseUrl !== undefined ? { baseUrl: deps.baseUrl } : {},
4379
4735
  ...deps.platform !== undefined ? { platform: deps.platform } : {}
4380
4736
  };
@@ -4446,6 +4802,9 @@ function describeCatalogStatus(status) {
4446
4802
  }
4447
4803
  var CATALOG_FETCH_TIMEOUT_MS = 8000;
4448
4804
  var init_provider_catalog = __esm(() => {
4805
+ init_grants();
4806
+ init_shell_config();
4807
+ init_subscription_models();
4449
4808
  init_select();
4450
4809
  init_providers();
4451
4810
  init_provider_catalog_cache();
@@ -4511,8 +4870,11 @@ function resolveProviderModelParamsByName(name, shellConfig = loadShellConfig(),
4511
4870
  function providerByName(name, dir) {
4512
4871
  return allOpenAiCompatProviders(dir).find((p) => p.name === name);
4513
4872
  }
4873
+ function connectionProviderByName(name, dir) {
4874
+ return name === "openai-codex" ? OPENAI_CODEX_PICKER : providerByName(name, dir);
4875
+ }
4514
4876
  function customCompatProviders(dir) {
4515
- const builtinNames = new Set(OPENAI_COMPAT_PROVIDERS.map((p) => p.name));
4877
+ const builtinNames = new Set([...OPENAI_COMPAT_PROVIDERS.map((p) => p.name), "openai-codex"]);
4516
4878
  return loadCustomCompatProviders(dir).filter((p) => !builtinNames.has(p.name)).map((p) => ({
4517
4879
  name: p.name,
4518
4880
  label: p.label ?? p.name,
@@ -4690,12 +5052,14 @@ async function fetchOpenAiCompatModelsDetailed(fetchFn, provider, apiKey, opts)
4690
5052
  }
4691
5053
  }
4692
5054
  async function resolveModelsForPicker(fetchFn, provider, env = process.env, opts) {
4693
- const compat = providerByName(provider.name);
5055
+ if (provider.name === "openai-codex")
5056
+ return fetchOpenAiCodexModels(fetchFn, opts);
5057
+ const compat = providerByName(provider.name, opts?.configDir);
4694
5058
  if (compat === undefined) {
4695
5059
  return { models: [...provider.models], source: "fallback" };
4696
5060
  }
4697
5061
  const envKey = provider.envKey ?? compat.envKey;
4698
- const raw = envKey === undefined ? undefined : env[envKey];
5062
+ const raw = envKey === undefined ? undefined : envWithSavedApiKeys(env, opts?.configDir)[envKey];
4699
5063
  const apiKey = typeof raw === "string" && raw.length > 0 ? raw : compat.apiKey;
4700
5064
  return fetchOpenAiCompatModelsDetailed(fetchFn, { ...compat, ...provider.baseUrl !== undefined ? { baseUrl: provider.baseUrl } : {} }, apiKey, opts);
4701
5065
  }
@@ -4819,7 +5183,9 @@ function providerApiKey(provider, env = process.env) {
4819
5183
  return typeof raw === "string" && raw.length > 0 ? raw : undefined;
4820
5184
  }
4821
5185
  async function testProviderConnection(provider, fetchFn = globalThis.fetch, env = process.env, profiles) {
4822
- const apiKey = providerApiKey(provider, env) ?? provider.apiKey;
5186
+ if (provider.name === "openai-codex")
5187
+ return fetchOpenAiCodexModels(fetchFn, { ...profiles?.dir !== undefined ? { configDir: profiles.dir } : {} });
5188
+ const apiKey = providerApiKey(provider, envWithSavedApiKeys(env, profiles?.dir)) ?? provider.apiKey;
4823
5189
  return fetchOpenAiCompatModelsDetailed(fetchFn, provider, apiKey, {
4824
5190
  timeoutMs: MODELS_FETCH_TIMEOUT_MS,
4825
5191
  ...profiles !== undefined ? { refreshProfiles: profiles } : {}
@@ -4837,7 +5203,7 @@ function classifyProviderConnection(name, env = process.env, dir) {
4837
5203
  if (customCompatProviders(dir).some((p) => p.name === name)) {
4838
5204
  return { kind: "custom", sharedWith: [] };
4839
5205
  }
4840
- if (loadShellConfig(dir).oauthGrants?.[name] !== undefined) {
5206
+ if (name !== "openai" && loadOAuthGrant(name, dir) !== undefined) {
4841
5207
  const envKey = oauthEnvKeyFor(name);
4842
5208
  const sharedWith = envKey === undefined ? [] : providersSharingEnvKey(envKey, name, allOpenAiCompatProviders(dir));
4843
5209
  return { kind: "oauth-grant", ...envKey !== undefined ? { envKey } : {}, sharedWith };
@@ -4905,7 +5271,7 @@ function configuredProviders(env = process.env, dir) {
4905
5271
  if (provider.requiresApiKey === false)
4906
5272
  return true;
4907
5273
  return providerApiKey(provider, env) !== undefined || provider.apiKey !== undefined;
4908
- }).filter((provider) => isProviderPlatformSupported(provider)).map((provider) => ({ name: provider.name, models: provider.models }));
5274
+ }).filter((provider) => isProviderPlatformSupported(provider)).map((provider) => ({ name: provider.name, models: provider.models })).concat(loadOAuthGrant("openai-codex", dir) === undefined ? [] : [{ name: "openai-codex", models: [...OPENAI_CODEX_PICKER.models] }]);
4909
5275
  }
4910
5276
  async function providersCommand(args, deps = {}) {
4911
5277
  const command = args[0];
@@ -4945,7 +5311,7 @@ async function runProvidersTest(args, deps) {
4945
5311
  return;
4946
5312
  }
4947
5313
  const dir = deps.dir;
4948
- const provider = providerByName(name, dir);
5314
+ const provider = connectionProviderByName(name, dir);
4949
5315
  if (provider === undefined) {
4950
5316
  console.error(`Unknown provider: ${name}`);
4951
5317
  process.exitCode = 1;
@@ -5017,7 +5383,7 @@ async function runProvidersRemove(args, deps) {
5017
5383
  const dir = deps.dir;
5018
5384
  const env = deps.env ?? process.env;
5019
5385
  const json = args.includes("--json");
5020
- if (providerByName(name, dir) === undefined) {
5386
+ if (connectionProviderByName(name, dir) === undefined) {
5021
5387
  if (json) {
5022
5388
  console.log(JSON.stringify({ provider: name, ok: false, reason: "unknown provider" }, null, 2));
5023
5389
  } else {
@@ -5218,6 +5584,8 @@ var init_providers = __esm(() => {
5218
5584
  init_provider_config();
5219
5585
  init_catalog();
5220
5586
  init_model_profile();
5587
+ init_subscription_models();
5588
+ init_curated_model_lists();
5221
5589
  init_grants();
5222
5590
  init_login();
5223
5591
  init_caller_session();
@@ -5228,6 +5596,7 @@ var init_providers = __esm(() => {
5228
5596
  MINIMAX_REASONING_PRESET_HOSTS = new Set(["api.minimax.io", "api.minimaxi.com"]);
5229
5597
  DEEPSEEK_REASONING_PRESET_HOSTS = new Set(["api.deepseek.com"]);
5230
5598
  OPENAI_COMPAT_PROVIDERS = [
5599
+ { name: "openai", label: "OpenAI API", baseUrl: "https://api.openai.com", envKey: "OPENAI_API_KEY", models: [...OPENAI_MODELS], note: "Platform API key \xB7 separate API billing" },
5231
5600
  {
5232
5601
  name: "openrouter",
5233
5602
  label: "OpenRouter",
@@ -8796,7 +9165,7 @@ var init_redact = __esm(() => {
8796
9165
  });
8797
9166
 
8798
9167
  // src/security/output-validation.ts
8799
- function failure(reason) {
9168
+ function failure2(reason) {
8800
9169
  return {
8801
9170
  ok: false,
8802
9171
  text: FORMAT_UNSAFE_OUTPUT_TEXT,
@@ -9057,12 +9426,12 @@ function validateOutputForTransport(input) {
9057
9426
  try {
9058
9427
  if (input.format === "text") {
9059
9428
  if (typeof input.value !== "string") {
9060
- return failure("non-text-value");
9429
+ return failure2("non-text-value");
9061
9430
  }
9062
9431
  const safe = redactString(input.value, undefined, input.exemptExfil);
9063
9432
  const schemaReason = schemaFailure(safe.value, input.schema);
9064
9433
  if (schemaReason !== undefined)
9065
- return failure(schemaReason);
9434
+ return failure2(schemaReason);
9066
9435
  const reasons = uniqueSorted(safe.reasons);
9067
9436
  return {
9068
9437
  ok: true,
@@ -9076,10 +9445,10 @@ function validateOutputForTransport(input) {
9076
9445
  }
9077
9446
  const safe = sanitizeJsonValue(input.value, undefined, new WeakSet, input.exemptExfil);
9078
9447
  if (!safe.ok)
9079
- return failure(safe.reason);
9448
+ return failure2(safe.reason);
9080
9449
  const schemaReason = schemaFailure(safe.value, input.schema);
9081
9450
  if (schemaReason !== undefined)
9082
- return failure(schemaReason);
9451
+ return failure2(schemaReason);
9083
9452
  const reasons = uniqueSorted(safe.reasons);
9084
9453
  return {
9085
9454
  ok: true,
@@ -9091,7 +9460,7 @@ function validateOutputForTransport(input) {
9091
9460
  }
9092
9461
  };
9093
9462
  } catch {
9094
- return failure("validation.error");
9463
+ return failure2("validation.error");
9095
9464
  }
9096
9465
  }
9097
9466
  function skipWhitespace(scanner) {
@@ -17259,7 +17628,7 @@ var package_default;
17259
17628
  var init_package = __esm(() => {
17260
17629
  package_default = {
17261
17630
  name: "@mrciphersmith/keryx",
17262
- version: "0.3.5",
17631
+ version: "0.3.6",
17263
17632
  description: "Version-controlled project context for AI coding agents: code graph, architecture wiki, project memory, relevant tests, quality signals, and task flows.",
17264
17633
  private: false,
17265
17634
  publishConfig: {
@@ -21823,14 +22192,14 @@ var init_config2 = __esm(() => {
21823
22192
  // src/sync/provenance.ts
21824
22193
  import { mkdir as mkdir9, readFile as readFile20, writeFile as writeFile7 } from "fs/promises";
21825
22194
  import path52 from "path";
21826
- import { spawn } from "child_process";
22195
+ import { spawn as spawn2 } from "child_process";
21827
22196
  function provenancePath(cwd, module) {
21828
22197
  return path52.join(cwd, ".metaproject", "data", module, ".provenance.json");
21829
22198
  }
21830
22199
  function gitCmdResult(cwd, args) {
21831
22200
  return new Promise((resolve) => {
21832
22201
  try {
21833
- const child = spawn("git", args, { cwd, stdio: ["ignore", "pipe", "pipe"] });
22202
+ const child = spawn2("git", args, { cwd, stdio: ["ignore", "pipe", "pipe"] });
21834
22203
  let out = "";
21835
22204
  let err = "";
21836
22205
  child.stdout?.on("data", (chunk) => {
@@ -21940,13 +22309,13 @@ var init_provenance = __esm(() => {
21940
22309
  });
21941
22310
 
21942
22311
  // src/gdgraph/staleness.ts
21943
- import { spawn as spawn2 } from "child_process";
22312
+ import { spawn as spawn3 } from "child_process";
21944
22313
  import { stat as stat8 } from "fs/promises";
21945
22314
  import path53 from "path";
21946
22315
  function gitStatusPorcelain(cwd) {
21947
22316
  return new Promise((resolve) => {
21948
22317
  try {
21949
- const child = spawn2("git", ["status", "--porcelain=v1", "-z"], { cwd, stdio: ["ignore", "pipe", "ignore"] });
22318
+ const child = spawn3("git", ["status", "--porcelain=v1", "-z"], { cwd, stdio: ["ignore", "pipe", "ignore"] });
21950
22319
  let out = "";
21951
22320
  child.stdout?.on("data", (chunk) => {
21952
22321
  out += String(chunk);
@@ -30016,14 +30385,14 @@ function asNumber5(value) {
30016
30385
  function clampOpenAiReasoningEffort(effort) {
30017
30386
  return effort === "xhigh" || effort === "max" ? "high" : effort;
30018
30387
  }
30019
- function ownedReasoningItems(reasoning) {
30388
+ function ownedReasoningItems(reasoning, providerId) {
30020
30389
  const replay = reasoning?.replay;
30021
30390
  if (replay === undefined) {
30022
30391
  return [];
30023
30392
  }
30024
30393
  const items = [];
30025
30394
  for (const item of replay) {
30026
- if (item.providerId !== PROVIDER_ID2 || item.kind !== "reasoning_item") {
30395
+ if (item.providerId !== providerId || item.kind !== "reasoning_item") {
30027
30396
  continue;
30028
30397
  }
30029
30398
  if (isPlainObject7(item.data)) {
@@ -30032,7 +30401,7 @@ function ownedReasoningItems(reasoning) {
30032
30401
  }
30033
30402
  return items;
30034
30403
  }
30035
- function toResponsesInput(messages) {
30404
+ function toResponsesInput(messages, providerId) {
30036
30405
  const out = [];
30037
30406
  for (const linked of linkToolCalls(messages)) {
30038
30407
  const message = linked.message;
@@ -30048,7 +30417,7 @@ function toResponsesInput(messages) {
30048
30417
  continue;
30049
30418
  }
30050
30419
  if (message.role === "assistant") {
30051
- for (const item of ownedReasoningItems(message.reasoning)) {
30420
+ for (const item of ownedReasoningItems(message.reasoning, providerId)) {
30052
30421
  out.push(item);
30053
30422
  }
30054
30423
  }
@@ -30136,14 +30505,23 @@ function classifyHttpError3(status, headers, code) {
30136
30505
  }
30137
30506
  return { kind: "unknown", retryable: retryableFor4("unknown", false), message: "" };
30138
30507
  }
30139
- async function raceReadAgainstDeadline4(reader, ms) {
30508
+ async function raceReadAgainstDeadline4(reader, ms, signal) {
30509
+ if (signal?.aborted === true)
30510
+ return READ_CANCELLED;
30140
30511
  let timer;
30141
30512
  const deadline = new Promise((resolve) => {
30142
30513
  timer = setTimeout(() => resolve(READ_TIMED_OUT4), ms);
30143
30514
  });
30515
+ let onAbort;
30516
+ const aborted = new Promise((resolve) => {
30517
+ onAbort = () => resolve(READ_CANCELLED);
30518
+ signal?.addEventListener("abort", onAbort, { once: true });
30519
+ });
30144
30520
  try {
30145
- return await Promise.race([reader.read(), deadline]);
30521
+ return await Promise.race([reader.read(), deadline, aborted]);
30146
30522
  } finally {
30523
+ if (onAbort !== undefined)
30524
+ signal?.removeEventListener("abort", onAbort);
30147
30525
  if (timer !== undefined) {
30148
30526
  clearTimeout(timer);
30149
30527
  }
@@ -30165,6 +30543,9 @@ async function* drainAndCheckAbort4(queue, signal, stamp) {
30165
30543
 
30166
30544
  class OpenAiProvider {
30167
30545
  deps;
30546
+ get providerId() {
30547
+ return this.deps.codex === undefined ? PROVIDER_ID2 : "openai-codex";
30548
+ }
30168
30549
  constructor(deps) {
30169
30550
  this.deps = deps;
30170
30551
  }
@@ -30182,15 +30563,15 @@ class OpenAiProvider {
30182
30563
  };
30183
30564
  return {
30184
30565
  capabilities,
30185
- descriptor: { providerId: PROVIDER_ID2, providerRevision: PROVIDER_REVISION4 }
30566
+ descriptor: { providerId: this.providerId, providerRevision: PROVIDER_REVISION4 }
30186
30567
  };
30187
30568
  }
30188
30569
  descriptorDocument() {
30189
30570
  return {
30190
30571
  schemaVersion: 1,
30191
- providerId: PROVIDER_ID2,
30572
+ providerId: this.providerId,
30192
30573
  providerRevision: PROVIDER_REVISION4,
30193
- models: [{ modelId: DEFAULT_MODEL4.modelId, revision: DEFAULT_MODEL4.revision }],
30574
+ models: [{ modelId: this.deps.codex === undefined ? DEFAULT_MODEL4.modelId : "gpt-5.4", revision: DEFAULT_MODEL4.revision }],
30194
30575
  capabilities: {
30195
30576
  streaming: true,
30196
30577
  tools: true,
@@ -30215,7 +30596,8 @@ class OpenAiProvider {
30215
30596
  });
30216
30597
  return;
30217
30598
  }
30218
- const baseUrl = grant.baseUrl ?? DEFAULT_BASE_URL4;
30599
+ const codex = this.deps.codex;
30600
+ const baseUrl = codex === undefined ? grant.baseUrl ?? DEFAULT_BASE_URL4 : "https://chatgpt.com/backend-api/codex";
30219
30601
  let host;
30220
30602
  try {
30221
30603
  host = new URL(baseUrl).hostname;
@@ -30230,10 +30612,16 @@ class OpenAiProvider {
30230
30612
  });
30231
30613
  return;
30232
30614
  }
30233
- const url = `${baseUrl.replace(/\/+$/, "")}/v1/responses`;
30615
+ const url = `${baseUrl.replace(/\/+$/, "")}${codex === undefined ? "/v1/responses" : "/responses"}`;
30234
30616
  const headers = {
30235
30617
  authorization: `Bearer ${grant.apiKey}`,
30236
- "content-type": "application/json"
30618
+ "content-type": "application/json",
30619
+ ...codex === undefined ? {} : {
30620
+ "ChatGPT-Account-ID": codex.accountId,
30621
+ originator: "keryx",
30622
+ "User-Agent": "keryx",
30623
+ accept: "text/event-stream"
30624
+ }
30237
30625
  };
30238
30626
  const requestedReasoningEffort = request.options?.reasoning;
30239
30627
  const reasoningRequested = typeof requestedReasoningEffort === "string" && requestedReasoningEffort.length > 0 && requestedReasoningEffort !== "off";
@@ -30241,10 +30629,10 @@ class OpenAiProvider {
30241
30629
  const payload = {
30242
30630
  model: request.modelId,
30243
30631
  instructions: request.systemInstruction,
30244
- input: toResponsesInput(request.messages),
30632
+ input: toResponsesInput(request.messages, this.providerId),
30245
30633
  stream: true,
30246
- max_output_tokens: request.budget.maxOutputTokens,
30247
- ...request.options?.temperature !== undefined && !reasoningRequested ? { temperature: request.options.temperature } : {},
30634
+ ...codex === undefined ? { max_output_tokens: request.budget.maxOutputTokens } : { store: false, tool_choice: "auto", parallel_tool_calls: true, tools: [] },
30635
+ ...codex === undefined && request.options?.temperature !== undefined && !reasoningRequested ? { temperature: request.options.temperature } : {},
30248
30636
  ...reasoningRequested ? {
30249
30637
  reasoning: { effort: reasoningEffort, summary: "auto" },
30250
30638
  include: ["reasoning.encrypted_content"],
@@ -30328,7 +30716,7 @@ class OpenAiProvider {
30328
30716
  const timeoutMs = receivedAnyChunk ? idleTimeoutMs : firstByteTimeoutMs;
30329
30717
  let readResult;
30330
30718
  try {
30331
- readResult = await raceReadAgainstDeadline4(reader, timeoutMs);
30719
+ readResult = await raceReadAgainstDeadline4(reader, timeoutMs, opts.signal);
30332
30720
  } catch (cause) {
30333
30721
  cancelReader();
30334
30722
  const aborted = opts.signal?.aborted === true || typeof cause === "object" && cause !== null && cause.name === "AbortError";
@@ -30343,6 +30731,11 @@ class OpenAiProvider {
30343
30731
  });
30344
30732
  return;
30345
30733
  }
30734
+ if (readResult === READ_CANCELLED) {
30735
+ cancelReader();
30736
+ yield errorEvent({ kind: "cancelled", retryable: retryableFor4("cancelled", false), message: "attempt cancelled" });
30737
+ return;
30738
+ }
30346
30739
  if (readResult === READ_TIMED_OUT4) {
30347
30740
  cancelReader();
30348
30741
  yield errorEvent({
@@ -30471,7 +30864,7 @@ class OpenAiProvider {
30471
30864
  }
30472
30865
  bodies.push({
30473
30866
  kind: "reasoning_replay",
30474
- replay: { providerId: PROVIDER_ID2, kind: "reasoning_item", data: item }
30867
+ replay: { providerId: this.providerId, kind: "reasoning_item", data: item }
30475
30868
  });
30476
30869
  }
30477
30870
  break;
@@ -30591,7 +30984,7 @@ class OpenAiProvider {
30591
30984
  }
30592
30985
  }
30593
30986
  }
30594
- var DEFAULT_BASE_URL4 = "https://api.openai.com", PROVIDER_ID2 = "openai", PROVIDER_REVISION4 = "openai-responses-2026-08", DEFAULT_MODEL4, DEFAULT_STREAM_TIMEOUT_MS4 = 120000, READ_TIMED_OUT4;
30987
+ var DEFAULT_BASE_URL4 = "https://api.openai.com", PROVIDER_ID2 = "openai", PROVIDER_REVISION4 = "openai-responses-2026-08", DEFAULT_MODEL4, DEFAULT_STREAM_TIMEOUT_MS4 = 120000, READ_TIMED_OUT4, READ_CANCELLED;
30595
30988
  var init_openai_provider = __esm(() => {
30596
30989
  init_guard();
30597
30990
  init_provider_port();
@@ -30600,11 +30993,94 @@ var init_openai_provider = __esm(() => {
30600
30993
  revision: "2026-08"
30601
30994
  };
30602
30995
  READ_TIMED_OUT4 = Symbol("openai-read-timed-out");
30996
+ READ_CANCELLED = Symbol("openai-read-cancelled");
30997
+ });
30998
+
30999
+ // src/harness/provider/openai-codex/openai-codex-provider.ts
31000
+ class OpenAiCodexProvider {
31001
+ deps;
31002
+ constructor(deps) {
31003
+ this.deps = deps;
31004
+ }
31005
+ describe() {
31006
+ return new OpenAiProvider({ fetch: this.deps.fetch, codex: { accountId: "" } }).describe();
31007
+ }
31008
+ async* stream(request, opts) {
31009
+ const failure = (cancelled) => ({
31010
+ kind: "provider_error",
31011
+ sequence: 0,
31012
+ attemptId: opts.attemptId,
31013
+ error: {
31014
+ kind: cancelled ? "cancelled" : "authentication",
31015
+ retryable: false,
31016
+ message: cancelled ? "attempt cancelled" : `ChatGPT subscription authorization is unavailable. ${LOGIN_HINT}`
31017
+ }
31018
+ });
31019
+ if (isCancelled(opts.signal)) {
31020
+ yield failure(true);
31021
+ return;
31022
+ }
31023
+ if (this.deps.authorize === undefined) {
31024
+ yield failure(false);
31025
+ return;
31026
+ }
31027
+ for (let attempt = 0;attempt < 2; attempt++) {
31028
+ let grant;
31029
+ try {
31030
+ grant = await this.deps.authorize(opts.signal, attempt > 0);
31031
+ } catch {
31032
+ yield failure(isCancelled(opts.signal));
31033
+ return;
31034
+ }
31035
+ if (isCancelled(opts.signal)) {
31036
+ yield failure(true);
31037
+ return;
31038
+ }
31039
+ if (grant.access.length === 0 || grant.accountId.length === 0) {
31040
+ yield failure(false);
31041
+ return;
31042
+ }
31043
+ const provider = new OpenAiProvider({
31044
+ fetch: this.deps.fetch,
31045
+ grant: { network: true, apiKey: grant.access },
31046
+ codex: { accountId: grant.accountId }
31047
+ });
31048
+ let emitted = false;
31049
+ let retry = false;
31050
+ for await (const event of provider.stream(request, opts)) {
31051
+ if (event.error?.kind === "authentication") {
31052
+ if (attempt === 0 && !emitted) {
31053
+ retry = true;
31054
+ break;
31055
+ }
31056
+ yield { ...event, error: { ...event.error, message: `ChatGPT subscription authorization was rejected. ${LOGIN_HINT}` } };
31057
+ return;
31058
+ }
31059
+ emitted = true;
31060
+ yield event;
31061
+ }
31062
+ if (!retry)
31063
+ return;
31064
+ }
31065
+ }
31066
+ }
31067
+ var LOGIN_HINT = "Run `keryx auth login openai-codex`.", isCancelled = (signal) => signal?.aborted === true;
31068
+ var init_openai_codex_provider = __esm(() => {
31069
+ init_openai_provider();
30603
31070
  });
30604
31071
 
30605
31072
  // src/harness/provider/make-provider.ts
30606
31073
  function makeProvider(name, _model, opts) {
30607
31074
  const env = opts.credentials ?? opts.env ?? process.env;
31075
+ if (name === "openai-codex") {
31076
+ const authorize = opts.openAiCodexGrant ?? (opts.credentials === undefined ? (signal, forceRefresh) => ensureOpenAiCodexGrant({
31077
+ fetch: (url, init) => opts.fetch(url, init),
31078
+ ...opts.configDir === undefined ? {} : { configDir: opts.configDir },
31079
+ ...signal === undefined ? {} : { signal },
31080
+ ...forceRefresh === undefined ? {} : { forceRefresh }
31081
+ }) : undefined);
31082
+ return new OpenAiCodexProvider({ fetch: opts.fetch, ...authorize === undefined ? {} : { authorize } });
31083
+ }
30608
31084
  if (name === "anthropic") {
30609
31085
  const apiKey = env.ANTHROPIC_API_KEY;
30610
31086
  if (apiKey === undefined || apiKey.length === 0) {
@@ -30661,12 +31137,14 @@ var OLLAMA_COMPAT_IDENTITY;
30661
31137
  var init_make_provider = __esm(() => {
30662
31138
  init_providers();
30663
31139
  init_catalog();
31140
+ init_openai_subscription();
30664
31141
  init_anthropic_provider();
30665
31142
  init_openai_compat_provider();
30666
31143
  init_fake_provider();
30667
31144
  init_gemini_provider();
30668
31145
  init_ollama_provider();
30669
31146
  init_openai_provider();
31147
+ init_openai_codex_provider();
30670
31148
  OLLAMA_COMPAT_IDENTITY = {
30671
31149
  defaultBaseUrl: "http://localhost:11434",
30672
31150
  providerRevision: "ollama-2024-10-22",
@@ -36841,7 +37319,7 @@ var init_template_structure = __esm(() => {
36841
37319
  });
36842
37320
 
36843
37321
  // src/wiki/service.ts
36844
- import { spawn as spawn3 } from "child_process";
37322
+ import { spawn as spawn4 } from "child_process";
36845
37323
  import { createHash as createHash22 } from "crypto";
36846
37324
  import { mkdir as mkdir22, readFile as readFile55, readdir as readdir15, rm as rm8, writeFile as writeFile21 } from "fs/promises";
36847
37325
  import path118 from "path";
@@ -37836,7 +38314,7 @@ async function changedModules(cwd, since) {
37836
38314
  }
37837
38315
  function gitDiffNames(cwd, base) {
37838
38316
  return new Promise((resolve) => {
37839
- const child = spawn3("git", ["diff", "--name-only", base], { cwd });
38317
+ const child = spawn4("git", ["diff", "--name-only", base], { cwd });
37840
38318
  let out = "";
37841
38319
  child.stdout.on("data", (chunk) => {
37842
38320
  out += String(chunk);
@@ -39623,7 +40101,7 @@ var init_metaproject_state = __esm(() => {
39623
40101
 
39624
40102
  // src/ctx/orient.ts
39625
40103
  import path124 from "path";
39626
- import { spawn as spawn4 } from "child_process";
40104
+ import { spawn as spawn5 } from "child_process";
39627
40105
  import { readFile as readFile58 } from "fs/promises";
39628
40106
  function metaPath(cwd, parts) {
39629
40107
  return path124.join(cwd, ".metaproject", ...parts);
@@ -39701,7 +40179,7 @@ async function metaprojectIndexContext(cwd) {
39701
40179
  function gitOutput(cwd, args) {
39702
40180
  return new Promise((resolve) => {
39703
40181
  try {
39704
- const child = spawn4("git", args, { cwd, stdio: ["ignore", "pipe", "pipe"] });
40182
+ const child = spawn5("git", args, { cwd, stdio: ["ignore", "pipe", "pipe"] });
39705
40183
  let out = "";
39706
40184
  child.stdout?.on("data", (chunk) => {
39707
40185
  out += String(chunk);
@@ -39864,7 +40342,7 @@ var init_orient = __esm(() => {
39864
40342
  });
39865
40343
 
39866
40344
  // src/commands/gdgraph.ts
39867
- import { spawn as spawn5 } from "child_process";
40345
+ import { spawn as spawn6 } from "child_process";
39868
40346
  import { existsSync as existsSync19 } from "fs";
39869
40347
  import path125 from "path";
39870
40348
  async function gdgraphCommand(args) {
@@ -40506,7 +40984,7 @@ function collectSeeds(args) {
40506
40984
  async function changedFiles() {
40507
40985
  return new Promise((resolve) => {
40508
40986
  try {
40509
- const child = spawn5("git", ["diff", "--name-only", "HEAD"], {
40987
+ const child = spawn6("git", ["diff", "--name-only", "HEAD"], {
40510
40988
  cwd: process.cwd(),
40511
40989
  stdio: ["ignore", "pipe", "ignore"]
40512
40990
  });
@@ -40536,7 +41014,7 @@ async function delegateToLocalRunner(args) {
40536
41014
  return { delegated: false };
40537
41015
  }
40538
41016
  const exitCode = await new Promise((resolve, reject) => {
40539
- const child = spawn5(process.execPath, [...SAFE_BUN_SPAWN_ARGS, localRunner, ...args], {
41017
+ const child = spawn6(process.execPath, [...SAFE_BUN_SPAWN_ARGS, localRunner, ...args], {
40540
41018
  cwd: process.cwd(),
40541
41019
  stdio: "inherit",
40542
41020
  env: {
@@ -42464,7 +42942,7 @@ var init_profile = __esm(() => {
42464
42942
  });
42465
42943
 
42466
42944
  // src/harness/hooks/runner.ts
42467
- import { spawn as spawn6 } from "child_process";
42945
+ import { spawn as spawn7 } from "child_process";
42468
42946
  import path135 from "path";
42469
42947
  function buildHookEnv(opts) {
42470
42948
  const env = {};
@@ -42503,7 +42981,7 @@ function spawnAndCollect(cmdPath, args, opts) {
42503
42981
  const isPosix = process.platform !== "win32";
42504
42982
  let child;
42505
42983
  try {
42506
- child = spawn6(cmdPath, args, {
42984
+ child = spawn7(cmdPath, args, {
42507
42985
  cwd: opts.cwd,
42508
42986
  env: opts.env,
42509
42987
  stdio: ["pipe", "pipe", "pipe"],
@@ -59309,7 +59787,7 @@ var init_wiki_freshness = () => {};
59309
59787
 
59310
59788
  // src/harness/tool/builtin/interactive-tools.ts
59311
59789
  import { readdir as readdir28 } from "fs/promises";
59312
- import { basename, dirname, isAbsolute, join, relative, resolve, sep } from "path";
59790
+ import { basename, dirname, isAbsolute, join as join2, relative, resolve as resolve2, sep } from "path";
59313
59791
  import { lstatSync as lstatSync4, realpathSync as realpathSync7 } from "fs";
59314
59792
  async function readFromLine(file, startLine, cap) {
59315
59793
  const reader = file.stream().getReader();
@@ -59367,7 +59845,7 @@ async function readFromLine(file, startLine, cap) {
59367
59845
  }
59368
59846
  function confineToRoot(root, candidate) {
59369
59847
  const rootReal = realpathOr(root);
59370
- const target = resolve(rootReal, candidate ?? ".");
59848
+ const target = resolve2(rootReal, candidate ?? ".");
59371
59849
  const effective = realpathThroughAncestors(target);
59372
59850
  if (effective === undefined) {
59373
59851
  return null;
@@ -59385,7 +59863,7 @@ function realpathOr(candidate) {
59385
59863
  try {
59386
59864
  return realpathSync7(candidate);
59387
59865
  } catch {
59388
- return resolve(candidate);
59866
+ return resolve2(candidate);
59389
59867
  }
59390
59868
  }
59391
59869
  function realpathThroughAncestors(absolute) {
@@ -59394,7 +59872,7 @@ function realpathThroughAncestors(absolute) {
59394
59872
  for (;; ) {
59395
59873
  try {
59396
59874
  const real = realpathSync7(current);
59397
- return missing.length === 0 ? real : join(real, ...missing.reverse());
59875
+ return missing.length === 0 ? real : join2(real, ...missing.reverse());
59398
59876
  } catch {
59399
59877
  try {
59400
59878
  lstatSync4(current);
@@ -59402,7 +59880,7 @@ function realpathThroughAncestors(absolute) {
59402
59880
  } catch {}
59403
59881
  const parent = dirname(current);
59404
59882
  if (parent === current) {
59405
- return resolve(absolute);
59883
+ return resolve2(absolute);
59406
59884
  }
59407
59885
  missing.push(basename(current));
59408
59886
  current = parent;
@@ -65712,7 +66190,7 @@ var init_github = __esm(() => {
65712
66190
 
65713
66191
  // src/harness/tool/metaproject-adapter.ts
65714
66192
  import { lstat as lstat13, readdir as readdir33, readFile as readFile101, realpath as realpath15, stat as stat19 } from "fs/promises";
65715
- import { isAbsolute as isAbsolute2, join as join2, relative as relative2, resolve as resolve2 } from "path";
66193
+ import { isAbsolute as isAbsolute2, join as join3, relative as relative2, resolve as resolve3 } from "path";
65716
66194
  function isMissingExecutable(cause) {
65717
66195
  const message = cause instanceof Error ? cause.message : String(cause);
65718
66196
  return /Executable not found|\bENOENT\b|not found in \$?PATH/i.test(message);
@@ -65753,8 +66231,8 @@ function portableMemoryPath(candidate) {
65753
66231
  return !isAbsolute2(normalized) && normalized !== ".." && !normalized.startsWith("../") && !normalized.includes("/../") ? normalized : null;
65754
66232
  }
65755
66233
  function confineToWiki(cwd, candidate) {
65756
- const wikiRoot = join2(cwd, ".metaproject", "wiki");
65757
- const target = resolve2(wikiRoot, candidate);
66234
+ const wikiRoot = join3(cwd, ".metaproject", "wiki");
66235
+ const target = resolve3(wikiRoot, candidate);
65758
66236
  const rel = relative2(wikiRoot, target);
65759
66237
  if (rel === "") {
65760
66238
  return null;
@@ -65796,7 +66274,7 @@ async function projectRemovalTrail(cwd, query) {
65796
66274
  async function detectMemoryStoreUnreadable(cwd) {
65797
66275
  const root = memoryRoot(cwd);
65798
66276
  for (const { folder } of MEMORY_TYPES) {
65799
- const dir = join2(root, folder);
66277
+ const dir = join3(root, folder);
65800
66278
  try {
65801
66279
  await readdir33(dir);
65802
66280
  } catch (error) {
@@ -65810,8 +66288,8 @@ async function detectMemoryStoreUnreadable(cwd) {
65810
66288
  return null;
65811
66289
  }
65812
66290
  function confineToSkills(cwd, candidate) {
65813
- const skillsRoot = join2(cwd, ".metaproject", "skills", "gdskills");
65814
- const target = resolve2(cwd, candidate);
66291
+ const skillsRoot = join3(cwd, ".metaproject", "skills", "gdskills");
66292
+ const target = resolve3(cwd, candidate);
65815
66293
  const rel = relative2(skillsRoot, target);
65816
66294
  if (rel === "") {
65817
66295
  return null;
@@ -65825,7 +66303,7 @@ async function parseCatalogSummaries(cwd) {
65825
66303
  const summaries = new Map;
65826
66304
  let content;
65827
66305
  try {
65828
- const bytes = await readContainedFile(join2(cwd, ".metaproject", "skills"), join2(cwd, ".metaproject", "skills", "catalog.md"), { maxBytes: 512 * 1024, requireRegularFile: true });
66306
+ const bytes = await readContainedFile(join3(cwd, ".metaproject", "skills"), join3(cwd, ".metaproject", "skills", "catalog.md"), { maxBytes: 512 * 1024, requireRegularFile: true });
65829
66307
  content = bytes.toString("utf8");
65830
66308
  } catch {
65831
66309
  return summaries;
@@ -65846,7 +66324,7 @@ async function parseCatalogSummaries(cwd) {
65846
66324
  return summaries;
65847
66325
  }
65848
66326
  async function walkSkillCatalog(cwd) {
65849
- const root = join2(cwd, ".metaproject", "skills", "gdskills");
66327
+ const root = join3(cwd, ".metaproject", "skills", "gdskills");
65850
66328
  const entries = [];
65851
66329
  const rootInfo = await lstat13(root).catch(() => null);
65852
66330
  if (!rootInfo || !rootInfo.isDirectory() || rootInfo.isSymbolicLink())
@@ -65864,7 +66342,7 @@ async function walkSkillCatalog(cwd) {
65864
66342
  for (const categoryDir of categoryDirs) {
65865
66343
  if (!categoryDir.isDirectory() && !categoryDir.isSymbolicLink())
65866
66344
  continue;
65867
- const categoryPath = join2(root, categoryDir.name);
66345
+ const categoryPath = join3(root, categoryDir.name);
65868
66346
  const categoryReal = await realpath15(categoryPath).catch(() => null);
65869
66347
  const categoryInfo = await stat19(categoryPath).catch(() => null);
65870
66348
  if (!categoryReal || !categoryInfo?.isDirectory() || !isPathInside(rootReal, categoryReal))
@@ -65876,12 +66354,12 @@ async function walkSkillCatalog(cwd) {
65876
66354
  continue;
65877
66355
  }
65878
66356
  for (const skillDir of skillDirs) {
65879
- const skillPath = join2(categoryPath, skillDir.name);
66357
+ const skillPath = join3(categoryPath, skillDir.name);
65880
66358
  const skillReal = await realpath15(skillPath).catch(() => null);
65881
66359
  const skillInfo = await stat19(skillPath).catch(() => null);
65882
66360
  if (!skillReal || !skillInfo?.isDirectory() || !isPathInside(rootReal, skillReal))
65883
66361
  continue;
65884
- const skillMdPath = join2(skillPath, "SKILL.md");
66362
+ const skillMdPath = join3(skillPath, "SKILL.md");
65885
66363
  let content;
65886
66364
  try {
65887
66365
  const bytes = await readContainedFile(root, skillMdPath, { maxBytes: 512 * 1024, requireRegularFile: true });
@@ -66147,7 +66625,7 @@ function createMetaprojectAdapter(cwd, overrides = {}) {
66147
66625
  exists = false;
66148
66626
  }
66149
66627
  if (!exists) {
66150
- const relativePath = toPosix(relative2(join2(cwd, ".metaproject", "wiki"), target));
66628
+ const relativePath = toPosix(relative2(join3(cwd, ".metaproject", "wiki"), target));
66151
66629
  const registry = await readSectionRegistryState(cwd);
66152
66630
  if (registry.state === "unreadable") {
66153
66631
  return {
@@ -66188,7 +66666,7 @@ function createMetaprojectAdapter(cwd, overrides = {}) {
66188
66666
  };
66189
66667
  }
66190
66668
  try {
66191
- const content = (await readContainedFile(join2(cwd, ".metaproject", "wiki"), target, {
66669
+ const content = (await readContainedFile(join3(cwd, ".metaproject", "wiki"), target, {
66192
66670
  maxBytes: 8 * 1024 * 1024,
66193
66671
  requireRegularFile: true
66194
66672
  })).toString("utf8");
@@ -66232,7 +66710,7 @@ function createMetaprojectAdapter(cwd, overrides = {}) {
66232
66710
  }
66233
66711
  let hasWikiIndex;
66234
66712
  try {
66235
- await readFile101(join2(cwd, ".metaproject", "wiki", "index.md"), "utf8");
66713
+ await readFile101(join3(cwd, ".metaproject", "wiki", "index.md"), "utf8");
66236
66714
  hasWikiIndex = true;
66237
66715
  } catch {
66238
66716
  hasWikiIndex = false;
@@ -66507,7 +66985,7 @@ function createMetaprojectAdapter(cwd, overrides = {}) {
66507
66985
  const byName = catalog.find((entry) => entry.name === input.name);
66508
66986
  if (byName !== undefined) {
66509
66987
  try {
66510
- const content = (await readContainedFile(join2(cwd, ".metaproject", "skills", "gdskills"), join2(cwd, byName.path), {
66988
+ const content = (await readContainedFile(join3(cwd, ".metaproject", "skills", "gdskills"), join3(cwd, byName.path), {
66511
66989
  maxBytes: 512 * 1024,
66512
66990
  requireRegularFile: true
66513
66991
  })).toString("utf8");
@@ -66520,12 +66998,12 @@ function createMetaprojectAdapter(cwd, overrides = {}) {
66520
66998
  if (confined === null) {
66521
66999
  return { name: input.name, path: "", content: "", found: false };
66522
67000
  }
66523
- const byPath = catalog.find((entry) => join2(cwd, entry.path) === confined);
67001
+ const byPath = catalog.find((entry) => join3(cwd, entry.path) === confined);
66524
67002
  if (byPath === undefined) {
66525
67003
  return { name: input.name, path: "", content: "", found: false };
66526
67004
  }
66527
67005
  try {
66528
- const content = (await readContainedFile(join2(cwd, ".metaproject", "skills", "gdskills"), confined, {
67006
+ const content = (await readContainedFile(join3(cwd, ".metaproject", "skills", "gdskills"), confined, {
66529
67007
  maxBytes: 512 * 1024,
66530
67008
  requireRegularFile: true
66531
67009
  })).toString("utf8");
@@ -69660,7 +70138,7 @@ var init_narrate = __esm(() => {
69660
70138
  // src/commands/memory.ts
69661
70139
  import { randomUUID as randomUUID16 } from "crypto";
69662
70140
  import { readdir as readdir34 } from "fs/promises";
69663
- import { join as join3 } from "path";
70141
+ import { join as join4 } from "path";
69664
70142
  function getService() {
69665
70143
  service3 ??= createMemoryService();
69666
70144
  return service3;
@@ -69767,7 +70245,7 @@ async function runIndex2(args) {
69767
70245
  async function detectMemoryStoreUnreadable2(cwd) {
69768
70246
  const root = memoryRoot(cwd);
69769
70247
  for (const { folder } of MEMORY_TYPES) {
69770
- const dir = join3(root, folder);
70248
+ const dir = join4(root, folder);
69771
70249
  try {
69772
70250
  await readdir34(dir);
69773
70251
  } catch (error) {
@@ -90455,6 +90933,31 @@ var JEV_MODEL_1_13 = "jev-1.13";
90455
90933
  var DEFAULT_JEV_MODEL = JEV_MODEL_1_13;
90456
90934
  var JEV_TOKEN_BUDGET = 64000;
90457
90935
  var DEFAULT_JEV_TIMEOUT_MS = 30000;
90936
+ var JEV_MAX_ATTEMPTS = 3;
90937
+ var JEV_RETRY_BACKOFF_MS = [500, 1500];
90938
+ var JEV_RETRY_AFTER_CAP_MS = 5000;
90939
+ var JEV_RETRYABLE_STATUSES = new Set([429, 500, 502, 503, 504]);
90940
+ function defaultJevSleep(ms) {
90941
+ return new Promise((resolve) => setTimeout(resolve, ms));
90942
+ }
90943
+ function jevBackoffMs(attemptJustFailed) {
90944
+ return JEV_RETRY_BACKOFF_MS[attemptJustFailed - 1] ?? JEV_RETRY_BACKOFF_MS[JEV_RETRY_BACKOFF_MS.length - 1] ?? 500;
90945
+ }
90946
+ function retryAfterMsFromHeader(headerValue) {
90947
+ if (headerValue === null || headerValue.trim().length === 0)
90948
+ return;
90949
+ const seconds = Number(headerValue);
90950
+ if (Number.isFinite(seconds)) {
90951
+ return seconds > 0 ? Math.min(seconds * 1000, JEV_RETRY_AFTER_CAP_MS) : undefined;
90952
+ }
90953
+ const dateMs = Date.parse(headerValue);
90954
+ if (!Number.isNaN(dateMs)) {
90955
+ const deltaMs = dateMs - Date.now();
90956
+ if (deltaMs > 0)
90957
+ return Math.min(deltaMs, JEV_RETRY_AFTER_CAP_MS);
90958
+ }
90959
+ return;
90960
+ }
90458
90961
 
90459
90962
  class JevCredentialError extends Error {
90460
90963
  constructor(message = "OPENROUTER_API_KEY is not set, and no openrouterKey is saved in the keryx shell config: " + "Jev needs an OpenRouter credential and refuses to send an unauthenticated request.") {
@@ -90478,13 +90981,28 @@ class JevBudgetError extends Error {
90478
90981
 
90479
90982
  class JevRequestError extends Error {
90480
90983
  status;
90481
- constructor(status, body) {
90984
+ attempts;
90985
+ constructor(status, body, attempts = 1) {
90482
90986
  const redactedBody = redactSensitiveText(body);
90483
- super(`Jev request to ${JEV_ENDPOINT} failed: HTTP ${status}${redactedBody.length > 0 ? ` \u2014 ${redactedBody.slice(0, 500)}` : ""}`);
90987
+ const attemptsNote = attempts > 1 ? ` after ${attempts} attempts` : "";
90988
+ super(`Jev request to ${JEV_ENDPOINT} failed: HTTP ${status}${attemptsNote}${redactedBody.length > 0 ? ` \u2014 ${redactedBody.slice(0, 500)}` : ""}`);
90484
90989
  this.status = status;
90990
+ this.attempts = attempts;
90485
90991
  this.name = "JevRequestError";
90486
90992
  }
90487
90993
  }
90994
+
90995
+ class JevNetworkError extends Error {
90996
+ attempts;
90997
+ cause;
90998
+ constructor(attempts, cause) {
90999
+ const causeMessage = cause instanceof Error ? cause.message : String(cause);
91000
+ super(`Jev request to ${JEV_ENDPOINT} failed after ${attempts} attempt(s): ${causeMessage}`);
91001
+ this.attempts = attempts;
91002
+ this.cause = cause;
91003
+ this.name = "JevNetworkError";
91004
+ }
91005
+ }
90488
91006
  function sourceLabel(source) {
90489
91007
  if (source === "env")
90490
91008
  return "the OPENROUTER_API_KEY environment variable";
@@ -90591,22 +91109,9 @@ function validatedAnswer(key, question, raw) {
90591
91109
  }
90592
91110
  return { type: "choice", choice };
90593
91111
  }
90594
- async function callJevSystemOne(fetchFn = globalThis.fetch, input, opts) {
90595
- const keyResolution = resolveJevApiKeyResolution(opts?.env ?? process.env, opts?.dir);
90596
- const apiKey = keyResolution.key;
90597
- if (apiKey === undefined || apiKey.length === 0) {
90598
- throw new JevCredentialError;
90599
- }
90600
- preflightBudget(input.state, input.questions);
90601
- const body = {
90602
- model: input.model ?? DEFAULT_JEV_MODEL,
90603
- state: input.state,
90604
- questions: input.questions
90605
- };
90606
- const timeoutMs = opts?.timeoutMs ?? DEFAULT_JEV_TIMEOUT_MS;
91112
+ async function attemptJevRequest(fetchFn, apiKey, body, timeoutMs, external) {
90607
91113
  const controller = new AbortController;
90608
91114
  const timer = setTimeout(() => controller.abort(), timeoutMs);
90609
- const external = opts?.signal;
90610
91115
  let onExternalAbort;
90611
91116
  if (external !== undefined) {
90612
91117
  if (external.aborted) {
@@ -90616,9 +91121,6 @@ async function callJevSystemOne(fetchFn = globalThis.fetch, input, opts) {
90616
91121
  external.addEventListener("abort", onExternalAbort);
90617
91122
  }
90618
91123
  }
90619
- let text;
90620
- let status;
90621
- let ok;
90622
91124
  try {
90623
91125
  const res = await fetchFn(JEV_ENDPOINT, {
90624
91126
  method: "POST",
@@ -90626,25 +91128,65 @@ async function callJevSystemOne(fetchFn = globalThis.fetch, input, opts) {
90626
91128
  body: JSON.stringify(body),
90627
91129
  signal: controller.signal
90628
91130
  });
90629
- status = res.status;
90630
- ok = res.ok;
90631
- text = await res.text();
91131
+ const text = await res.text();
91132
+ return { kind: "response", status: res.status, ok: res.ok, text, retryAfterHeader: res.headers.get("retry-after") };
90632
91133
  } catch (cause) {
90633
91134
  if (controller.signal.aborted) {
90634
- throw new JevTimeoutError(timeoutMs);
91135
+ return { kind: "timeout" };
90635
91136
  }
90636
- throw cause;
91137
+ return { kind: "network-error", cause };
90637
91138
  } finally {
90638
91139
  clearTimeout(timer);
90639
91140
  if (onExternalAbort !== undefined) {
90640
91141
  external?.removeEventListener("abort", onExternalAbort);
90641
91142
  }
90642
91143
  }
90643
- if (!ok) {
91144
+ }
91145
+ async function callJevSystemOne(fetchFn = globalThis.fetch, input, opts) {
91146
+ const keyResolution = resolveJevApiKeyResolution(opts?.env ?? process.env, opts?.dir);
91147
+ const apiKey = keyResolution.key;
91148
+ if (apiKey === undefined || apiKey.length === 0) {
91149
+ throw new JevCredentialError;
91150
+ }
91151
+ preflightBudget(input.state, input.questions);
91152
+ const body = {
91153
+ model: input.model ?? DEFAULT_JEV_MODEL,
91154
+ state: input.state,
91155
+ questions: input.questions
91156
+ };
91157
+ const timeoutMs = opts?.timeoutMs ?? DEFAULT_JEV_TIMEOUT_MS;
91158
+ const external = opts?.signal;
91159
+ const sleepFn = opts?.sleepFn ?? defaultJevSleep;
91160
+ let text;
91161
+ let attempt = 0;
91162
+ for (;; ) {
91163
+ attempt += 1;
91164
+ const outcome = await attemptJevRequest(fetchFn, apiKey, body, timeoutMs, external);
91165
+ if (outcome.kind === "timeout") {
91166
+ throw new JevTimeoutError(timeoutMs);
91167
+ }
91168
+ if (outcome.kind === "network-error") {
91169
+ if (attempt >= JEV_MAX_ATTEMPTS) {
91170
+ throw new JevNetworkError(attempt, outcome.cause);
91171
+ }
91172
+ await sleepFn(jevBackoffMs(attempt));
91173
+ continue;
91174
+ }
91175
+ if (outcome.ok) {
91176
+ text = outcome.text;
91177
+ break;
91178
+ }
91179
+ const status = outcome.status;
91180
+ text = outcome.text;
91181
+ if (JEV_RETRYABLE_STATUSES.has(status) && attempt < JEV_MAX_ATTEMPTS) {
91182
+ const retryAfterMs = retryAfterMsFromHeader(outcome.retryAfterHeader);
91183
+ await sleepFn(retryAfterMs ?? jevBackoffMs(attempt));
91184
+ continue;
91185
+ }
90644
91186
  if (status === 401) {
90645
91187
  throw new JevAuthRejectedError(keyResolution.source, status, text, looksLikeOpenRouterKey(apiKey));
90646
91188
  }
90647
- throw new JevRequestError(status, text);
91189
+ throw new JevRequestError(status, text, attempt);
90648
91190
  }
90649
91191
  let parsed;
90650
91192
  try {
@@ -92435,7 +92977,7 @@ init_service4();
92435
92977
  init_learn();
92436
92978
  init_service9();
92437
92979
  import { mkdir as mkdir51, readdir as readdir47, readFile as readFile133 } from "fs/promises";
92438
- import path238, { join as join8 } from "path";
92980
+ import path238, { join as join9 } from "path";
92439
92981
  init_review_learning();
92440
92982
 
92441
92983
  // src/review/managed.ts
@@ -95480,7 +96022,7 @@ directly.
95480
96022
  // src/commands/review-jev-rules.ts
95481
96023
  init_args();
95482
96024
  import { readFile as readFile118, readdir as readdir44, stat as stat22 } from "fs/promises";
95483
- import { basename as basename2, dirname as dirname2, isAbsolute as isAbsolute3, join as join4, relative as relative3 } from "path";
96025
+ import { basename as basename2, dirname as dirname2, isAbsolute as isAbsolute3, join as join5, relative as relative3 } from "path";
95484
96026
 
95485
96027
  // src/review/conform-state.ts
95486
96028
  init_redact();
@@ -96647,7 +97189,7 @@ async function walkMatching(root, suffixes) {
96647
97189
  return;
96648
97190
  }
96649
97191
  for (const entry of entries) {
96650
- const full = join4(dir, entry.name);
97192
+ const full = join5(dir, entry.name);
96651
97193
  if (entry.isDirectory()) {
96652
97194
  await walk(full);
96653
97195
  continue;
@@ -96672,7 +97214,7 @@ function classify2(file) {
96672
97214
  }
96673
97215
  async function discoverRuleSources(cwd, explicitPaths) {
96674
97216
  const discovered = [];
96675
- for (const root of [join4(cwd, ".metaproject", "rules"), join4(cwd, "rules")]) {
97217
+ for (const root of [join5(cwd, ".metaproject", "rules"), join5(cwd, "rules")]) {
96676
97218
  for (const file of await walkMatching(root, [".md", ".mdc"])) {
96677
97219
  const text = await readIfExists(file);
96678
97220
  if (text === undefined)
@@ -96681,8 +97223,8 @@ async function discoverRuleSources(cwd, explicitPaths) {
96681
97223
  }
96682
97224
  }
96683
97225
  const skillRoots = [
96684
- [join4(cwd, ".metaproject", "project-skills"), "project-skill"],
96685
- [join4(cwd, ".metaproject", "skills", "gdskills"), "gdskill"]
97226
+ [join5(cwd, ".metaproject", "project-skills"), "project-skill"],
97227
+ [join5(cwd, ".metaproject", "skills", "gdskills"), "gdskill"]
96686
97228
  ];
96687
97229
  for (const [root, kind] of skillRoots) {
96688
97230
  for (const skillMd of await walkMatching(root, ["SKILL.md"])) {
@@ -96707,7 +97249,7 @@ async function discoverRuleSources(cwd, explicitPaths) {
96707
97249
  const explicitFiles = [];
96708
97250
  const explicitDirSources = [];
96709
97251
  for (const raw of explicitPaths) {
96710
- const resolved = isAbsolute3(raw) ? raw : join4(cwd, raw);
97252
+ const resolved = isAbsolute3(raw) ? raw : join5(cwd, raw);
96711
97253
  const stats = await stat22(resolved).catch(() => {
96712
97254
  return;
96713
97255
  });
@@ -96750,11 +97292,11 @@ async function discoverRuleSources(cwd, explicitPaths) {
96750
97292
  return { sources: [...byPath.values()].sort((a, b) => a.path.localeCompare(b.path)), excluded: excluded.sort((a, b) => a.path.localeCompare(b.path)) };
96751
97293
  }
96752
97294
  async function fixturePrPort(dir) {
96753
- const raw = await readFile118(join4(dir, "pr.json"), "utf8");
97295
+ const raw = await readFile118(join5(dir, "pr.json"), "utf8");
96754
97296
  return createFixtureConformPrPort({ pr: JSON.parse(raw) });
96755
97297
  }
96756
97298
  async function fixtureJevFetch(dir) {
96757
- const raw = await readFile118(join4(dir, "jev-responses.json"), "utf8");
97299
+ const raw = await readFile118(join5(dir, "jev-responses.json"), "utf8");
96758
97300
  const responses = JSON.parse(raw);
96759
97301
  let index = 0;
96760
97302
  const fn = async () => {
@@ -97557,7 +98099,7 @@ async function runJevRisk(args) {
97557
98099
  // src/commands/review-jev-scenarios.ts
97558
98100
  init_args();
97559
98101
  import { readFile as readFile122, readdir as readdir45 } from "fs/promises";
97560
- import { join as join5, relative as relative4 } from "path";
98102
+ import { join as join6, relative as relative4 } from "path";
97561
98103
 
97562
98104
  // src/review/jev-scenarios-config.ts
97563
98105
  init_review_gate();
@@ -97900,7 +98442,7 @@ async function walkMarkdown2(root) {
97900
98442
  return;
97901
98443
  }
97902
98444
  for (const entry of entries) {
97903
- const full = join5(dir, entry.name);
98445
+ const full = join6(dir, entry.name);
97904
98446
  if (entry.isDirectory()) {
97905
98447
  await walk(full);
97906
98448
  continue;
@@ -97914,19 +98456,19 @@ async function walkMarkdown2(root) {
97914
98456
  }
97915
98457
  async function discoverScenarioSources(cwd) {
97916
98458
  const scenarios = [];
97917
- for (const file of await walkMarkdown2(join5(cwd, ".metaproject", "wiki", "user-scenarios"))) {
98459
+ for (const file of await walkMarkdown2(join6(cwd, ".metaproject", "wiki", "user-scenarios"))) {
97918
98460
  const text = await readIfExists2(file);
97919
98461
  if (text === undefined)
97920
98462
  continue;
97921
98463
  scenarios.push(scenarioFromWikiPage(relative4(cwd, file), text));
97922
98464
  }
97923
- for (const file of await walkMarkdown2(join5(cwd, "docs", "requirements"))) {
98465
+ for (const file of await walkMarkdown2(join6(cwd, "docs", "requirements"))) {
97924
98466
  const text = await readIfExists2(file);
97925
98467
  if (text === undefined)
97926
98468
  continue;
97927
98469
  scenarios.push(...scenariosFromPrd(relative4(cwd, file), text));
97928
98470
  }
97929
- const readmeLikeFiles = [join5(cwd, "README.md"), ...await walkMarkdown2(join5(cwd, "docs", "docs"))];
98471
+ const readmeLikeFiles = [join6(cwd, "README.md"), ...await walkMarkdown2(join6(cwd, "docs", "docs"))];
97930
98472
  for (const file of readmeLikeFiles) {
97931
98473
  const text = await readIfExists2(file);
97932
98474
  if (text === undefined)
@@ -97936,11 +98478,11 @@ async function discoverScenarioSources(cwd) {
97936
98478
  return scenarios;
97937
98479
  }
97938
98480
  async function fixturePrPort3(dir) {
97939
- const raw = await readFile122(join5(dir, "pr.json"), "utf8");
98481
+ const raw = await readFile122(join6(dir, "pr.json"), "utf8");
97940
98482
  return createFixtureConformPrPort({ pr: JSON.parse(raw) });
97941
98483
  }
97942
98484
  async function fixtureJevFetch3(dir) {
97943
- const raw = await readFile122(join5(dir, "jev-responses.json"), "utf8");
98485
+ const raw = await readFile122(join6(dir, "jev-responses.json"), "utf8");
97944
98486
  const responses = JSON.parse(raw);
97945
98487
  let index = 0;
97946
98488
  const fn = async () => {
@@ -98096,7 +98638,7 @@ async function gitDiffForJevScenarios(ref) {
98096
98638
  // src/commands/review-jev-docs.ts
98097
98639
  init_args();
98098
98640
  import { readFile as readFile124, readdir as readdir46 } from "fs/promises";
98099
- import { join as join6, relative as relative5 } from "path";
98641
+ import { join as join7, relative as relative5 } from "path";
98100
98642
 
98101
98643
  // src/review/jev-docs-config.ts
98102
98644
  init_review_gate();
@@ -98576,7 +99118,7 @@ async function walkMatching2(root, suffixes) {
98576
99118
  return;
98577
99119
  }
98578
99120
  for (const entry of entries) {
98579
- const full = join6(dir, entry.name);
99121
+ const full = join7(dir, entry.name);
98580
99122
  if (entry.isDirectory()) {
98581
99123
  await walk(full);
98582
99124
  continue;
@@ -98637,7 +99179,7 @@ async function walkAllDocFiles(dir) {
98637
99179
  return out;
98638
99180
  }
98639
99181
  for (const entry of entries) {
98640
- const full = join6(dir, entry.name);
99182
+ const full = join7(dir, entry.name);
98641
99183
  if (entry.isDirectory()) {
98642
99184
  if (INCLUDE_WALK_PRUNE_DIRS.has(entry.name))
98643
99185
  continue;
@@ -98651,19 +99193,19 @@ async function walkAllDocFiles(dir) {
98651
99193
  async function resolveIncludeGlob(cwd, glob) {
98652
99194
  const rootDir = includeGlobRootDir(glob);
98653
99195
  const regex = includeGlobToRegExp(glob);
98654
- const candidates = await walkAllDocFiles(join6(cwd, rootDir));
99196
+ const candidates = await walkAllDocFiles(join7(cwd, rootDir));
98655
99197
  return candidates.filter((file) => regex.test(relative5(cwd, file)));
98656
99198
  }
98657
99199
  async function discoverDocFiles(cwd, includeGlobs = []) {
98658
99200
  const files = new Set;
98659
- for (const file of await walkMatching2(join6(cwd, "docs"), [".md"]))
99201
+ for (const file of await walkMatching2(join7(cwd, "docs"), [".md"]))
98660
99202
  files.add(file);
98661
99203
  for (const name of await matchingFileNames(cwd, /^readme/i)) {
98662
99204
  if (/^changelog/i.test(name))
98663
99205
  continue;
98664
- files.add(join6(cwd, name));
99206
+ files.add(join7(cwd, name));
98665
99207
  }
98666
- for (const file of await walkMatching2(join6(cwd, ".metaproject", "wiki"), [".md"]))
99208
+ for (const file of await walkMatching2(join7(cwd, ".metaproject", "wiki"), [".md"]))
98667
99209
  files.add(file);
98668
99210
  for (const glob of includeGlobs) {
98669
99211
  for (const file of await resolveIncludeGlob(cwd, glob))
@@ -98691,11 +99233,11 @@ function changedCommandNamesFromRegions(regions) {
98691
99233
  return names;
98692
99234
  }
98693
99235
  async function fixturePrPort4(dir) {
98694
- const raw = await readFile124(join6(dir, "pr.json"), "utf8");
99236
+ const raw = await readFile124(join7(dir, "pr.json"), "utf8");
98695
99237
  return createFixtureConformPrPort({ pr: JSON.parse(raw) });
98696
99238
  }
98697
99239
  async function fixtureJevFetch4(dir) {
98698
- const raw = await readFile124(join6(dir, "jev-responses.json"), "utf8");
99240
+ const raw = await readFile124(join7(dir, "jev-responses.json"), "utf8");
98699
99241
  const responses = JSON.parse(raw);
98700
99242
  let index = 0;
98701
99243
  const fn = async () => {
@@ -98869,7 +99411,7 @@ async function runJevDocs(args) {
98869
99411
  init_args();
98870
99412
  init_fs();
98871
99413
  import { readFile as readFile126 } from "fs/promises";
98872
- import { join as join7 } from "path";
99414
+ import { join as join8 } from "path";
98873
99415
  init_pr_comments();
98874
99416
 
98875
99417
  // src/review/jev-comments-config.ts
@@ -99163,15 +99705,15 @@ function createFixtureJevCommentsFactsPort(facts) {
99163
99705
  };
99164
99706
  }
99165
99707
  async function fixturePrPort5(dir) {
99166
- const raw = await readFile126(join7(dir, "pr.json"), "utf8");
99708
+ const raw = await readFile126(join8(dir, "pr.json"), "utf8");
99167
99709
  return createFixtureConformPrPort({ pr: JSON.parse(raw) });
99168
99710
  }
99169
99711
  async function fixtureFactsPort(dir) {
99170
- const raw = await readFile126(join7(dir, "git-facts.json"), "utf8").catch(() => "{}");
99712
+ const raw = await readFile126(join8(dir, "git-facts.json"), "utf8").catch(() => "{}");
99171
99713
  return createFixtureJevCommentsFactsPort(JSON.parse(raw));
99172
99714
  }
99173
99715
  async function fixtureJevFetch5(dir) {
99174
- const raw = await readFile126(join7(dir, "jev-responses.json"), "utf8");
99716
+ const raw = await readFile126(join8(dir, "jev-responses.json"), "utf8");
99175
99717
  const responses = JSON.parse(raw);
99176
99718
  let index = 0;
99177
99719
  const fn = async () => {
@@ -99185,7 +99727,7 @@ async function fixtureJevFetch5(dir) {
99185
99727
  return fn;
99186
99728
  }
99187
99729
  function advisoryLabelsPath(cwd, repo, number) {
99188
- return join7(cwd, ".metaproject", "data", "review-jev-comments", `labels__${repo.replace(/\//g, "__")}__${number}.json`);
99730
+ return join8(cwd, ".metaproject", "data", "review-jev-comments", `labels__${repo.replace(/\//g, "__")}__${number}.json`);
99189
99731
  }
99190
99732
  async function readCommentAdvisoryLabels(cwd, repo, number) {
99191
99733
  const file = advisoryLabelsPath(cwd, repo, number);
@@ -102579,7 +103121,7 @@ async function resolvePort(args) {
102579
103121
  }
102580
103122
  const files = {};
102581
103123
  for (const key of ["pull", "pull-comments", "pull-reviews", "issue-comments"]) {
102582
- const file = join8(fixtures, `${key}.json`);
103124
+ const file = join9(fixtures, `${key}.json`);
102583
103125
  try {
102584
103126
  files[key] = JSON.parse(await readFile133(file, "utf8"));
102585
103127
  } catch {
@@ -102589,13 +103131,13 @@ async function resolvePort(args) {
102589
103131
  return createFixturePort(files);
102590
103132
  }
102591
103133
  async function fixtureCiPort(dir) {
102592
- const runInfo = JSON.parse(await readFile133(join8(dir, "ci-run-info.json"), "utf8"));
102593
- const log = await readFile133(join8(dir, "ci-failed-log.txt"), "utf8").catch(() => "");
102594
- const history = await readFile133(join8(dir, "ci-history.json"), "utf8").then((raw) => JSON.parse(raw)).catch(() => ({}));
102595
- const attempts = await readFile133(join8(dir, "ci-attempts.json"), "utf8").then((raw) => JSON.parse(raw)).catch(() => ({}));
102596
- const changedFiles = await readFile133(join8(dir, "ci-changed-files.json"), "utf8").then((raw) => JSON.parse(raw)).catch(() => ({}));
102597
- const runsByHeadSha = await readFile133(join8(dir, "ci-runs-by-head-sha.json"), "utf8").then((raw) => JSON.parse(raw)).catch(() => ({}));
102598
- const related = await readFile133(join8(dir, "ci-related-runs.json"), "utf8").then((raw) => JSON.parse(raw)).catch(() => ({}));
103134
+ const runInfo = JSON.parse(await readFile133(join9(dir, "ci-run-info.json"), "utf8"));
103135
+ const log = await readFile133(join9(dir, "ci-failed-log.txt"), "utf8").catch(() => "");
103136
+ const history = await readFile133(join9(dir, "ci-history.json"), "utf8").then((raw) => JSON.parse(raw)).catch(() => ({}));
103137
+ const attempts = await readFile133(join9(dir, "ci-attempts.json"), "utf8").then((raw) => JSON.parse(raw)).catch(() => ({}));
103138
+ const changedFiles = await readFile133(join9(dir, "ci-changed-files.json"), "utf8").then((raw) => JSON.parse(raw)).catch(() => ({}));
103139
+ const runsByHeadSha = await readFile133(join9(dir, "ci-runs-by-head-sha.json"), "utf8").then((raw) => JSON.parse(raw)).catch(() => ({}));
103140
+ const related = await readFile133(join9(dir, "ci-related-runs.json"), "utf8").then((raw) => JSON.parse(raw)).catch(() => ({}));
102599
103141
  return createFixtureCiPort({
102600
103142
  runs: { [runInfo.runId]: runInfo, ...related.runs },
102601
103143
  logs: { [runInfo.runId]: log, ...related.logs },
@@ -102606,7 +103148,7 @@ async function fixtureCiPort(dir) {
102606
103148
  });
102607
103149
  }
102608
103150
  async function fixtureJevFetch8(dir) {
102609
- const body = await readFile133(join8(dir, "jev-response.json"), "utf8");
103151
+ const body = await readFile133(join9(dir, "jev-response.json"), "utf8");
102610
103152
  const fn = async () => new Response(body, { status: 200, headers: { "content-type": "application/json" } });
102611
103153
  return fn;
102612
103154
  }
@@ -102772,12 +103314,12 @@ async function runCiTriageEval(cwd, args, evalFile) {
102772
103314
  }
102773
103315
  }
102774
103316
  async function fixtureConformJevFetch(dir) {
102775
- const body = await readFile133(join8(dir, "jev-response.json"), "utf8");
103317
+ const body = await readFile133(join9(dir, "jev-response.json"), "utf8");
102776
103318
  const fn = async () => new Response(body, { status: 200, headers: { "content-type": "application/json" } });
102777
103319
  return fn;
102778
103320
  }
102779
103321
  async function fixtureConformPrPortFrom(dir) {
102780
- const pr = JSON.parse(await readFile133(join8(dir, "pr.json"), "utf8"));
103322
+ const pr = JSON.parse(await readFile133(join9(dir, "pr.json"), "utf8"));
102781
103323
  return createFixtureConformPrPort({ pr });
102782
103324
  }
102783
103325
  function accumulateJevUsage(acc, usage) {
@@ -102858,7 +103400,7 @@ async function explainConformVerdicts(cwd, refPath, verdicts, threshold, fixture
102858
103400
  return {};
102859
103401
  if (fixturesDir !== undefined) {
102860
103402
  try {
102861
- return JSON.parse(await readFile133(join8(fixturesDir, "explain-response.json"), "utf8"));
103403
+ return JSON.parse(await readFile133(join9(fixturesDir, "explain-response.json"), "utf8"));
102862
103404
  } catch {
102863
103405
  return {};
102864
103406
  }
@@ -102906,7 +103448,7 @@ function parsePositiveIntFlag(args, flag, fallback) {
102906
103448
  }
102907
103449
  async function readReportFindings(reportDir) {
102908
103450
  try {
102909
- const raw = JSON.parse(await readFile133(join8(reportDir, "findings.json"), "utf8"));
103451
+ const raw = JSON.parse(await readFile133(join9(reportDir, "findings.json"), "utf8"));
102910
103452
  return Array.isArray(raw) ? raw : [];
102911
103453
  } catch {
102912
103454
  return [];
@@ -102978,7 +103520,7 @@ async function runConform(args) {
102978
103520
  const fetchFn = fixturesDir === undefined ? globalThis.fetch : await fixtureConformJevFetch(fixturesDir);
102979
103521
  const acc = { jevCalls: 0, inputTokens: 0, outputTokens: 0, costUsd: 0, sawUsage: false };
102980
103522
  const clauses = await resolveConformClauseTags(cwd, refPath, docText, rawClauses, fetchFn, model, acc);
102981
- await writeFileAtomic(join8(cwd, CONFORM_RECENTS_PATH), `${JSON.stringify(withRecentDoc(await readRecentConformDocs(cwd), refPath), null, 2)}
103523
+ await writeFileAtomic(join9(cwd, CONFORM_RECENTS_PATH), `${JSON.stringify(withRecentDoc(await readRecentConformDocs(cwd), refPath), null, 2)}
102982
103524
  `, { mode: 384 }).catch(() => {});
102983
103525
  const notCheckable = clauses.filter((c) => !c.checkable);
102984
103526
  const checkable = clauses.filter((c) => c.checkable);
@@ -103010,7 +103552,7 @@ async function runConform(args) {
103010
103552
  hunkTotalRegions = hunkResult.totalRegions;
103011
103553
  } else if (reportDir !== undefined) {
103012
103554
  target = { kind: "report", label: reportDir };
103013
- const reportMarkdown = await readFile133(join8(reportDir, "report.md"), "utf8").catch(() => "");
103555
+ const reportMarkdown = await readFile133(join9(reportDir, "report.md"), "utf8").catch(() => "");
103014
103556
  const findings = await readReportFindings(reportDir);
103015
103557
  const facts = computeReportConformFacts(reportMarkdown, findings);
103016
103558
  const redacted = reportRedactedStateText(reportMarkdown);
@@ -103051,7 +103593,7 @@ async function runConform(args) {
103051
103593
  }
103052
103594
  async function readRecentConformDocs(cwd) {
103053
103595
  try {
103054
- const raw = JSON.parse(await readFile133(join8(cwd, CONFORM_RECENTS_PATH), "utf8"));
103596
+ const raw = JSON.parse(await readFile133(join9(cwd, CONFORM_RECENTS_PATH), "utf8"));
103055
103597
  return Array.isArray(raw) ? raw : [];
103056
103598
  } catch {
103057
103599
  return [];
@@ -103297,7 +103839,7 @@ async function runStack(args) {
103297
103839
  console.log(renderStackScopingMarkdown(detected, decisions));
103298
103840
  }
103299
103841
  async function stackScopingForInstalledReviewers(cwd, detected) {
103300
- const reviewRoot = join8(cwd, ".metaproject", "skills", "gdskills", "review");
103842
+ const reviewRoot = join9(cwd, ".metaproject", "skills", "gdskills", "review");
103301
103843
  let entries;
103302
103844
  try {
103303
103845
  entries = await readdir47(reviewRoot, { withFileTypes: true });
@@ -103309,7 +103851,7 @@ async function stackScopingForInstalledReviewers(cwd, detected) {
103309
103851
  if (!entry.isDirectory()) {
103310
103852
  continue;
103311
103853
  }
103312
- const skillMdPath = join8(reviewRoot, entry.name, "SKILL.md");
103854
+ const skillMdPath = join9(reviewRoot, entry.name, "SKILL.md");
103313
103855
  let content;
103314
103856
  try {
103315
103857
  content = await readFile133(skillMdPath, "utf8");
@@ -110424,38 +110966,8 @@ function formatDoctorReport(report) {
110424
110966
  `);
110425
110967
  }
110426
110968
 
110427
- // src/lib/oauth/open-url.ts
110428
- import { spawn as spawn7 } from "child_process";
110429
- function browserOpenPlan(url, platform = process.platform, env = process.env) {
110430
- if (url.length === 0) {
110431
- return;
110432
- }
110433
- if (platform === "darwin") {
110434
- return { cmd: "open", args: [url] };
110435
- }
110436
- if (platform === "win32") {
110437
- return { cmd: "cmd", args: ["/c", "start", "", url] };
110438
- }
110439
- if (typeof env.DISPLAY === "string" && env.DISPLAY.length > 0) {
110440
- return { cmd: "xdg-open", args: [url] };
110441
- }
110442
- if (typeof env.WAYLAND_DISPLAY === "string" && env.WAYLAND_DISPLAY.length > 0) {
110443
- return { cmd: "xdg-open", args: [url] };
110444
- }
110445
- return;
110446
- }
110447
- function openVerificationUrl(url, deps = {}) {
110448
- const plan = browserOpenPlan(url, deps.platform ?? process.platform, deps.env ?? process.env);
110449
- if (plan === undefined) {
110450
- return;
110451
- }
110452
- const spawnFn = deps.spawn ?? spawn7;
110453
- try {
110454
- const child = spawnFn(plan.cmd, plan.args, { stdio: "ignore", detached: true });
110455
- child.on("error", () => {});
110456
- child.unref();
110457
- } catch {}
110458
- }
110969
+ // src/commands/mcp-servers.ts
110970
+ init_open_url();
110459
110971
 
110460
110972
  // src/mcp-servers/oauth-callback.ts
110461
110973
  import { randomBytes as randomBytes8, timingSafeEqual as timingSafeEqual2 } from "crypto";
@@ -126678,7 +127190,7 @@ init_safe_exec();
126678
127190
  init_metaproject_operations();
126679
127191
  init_interactive_tools();
126680
127192
  import { existsSync as existsSync43 } from "fs";
126681
- import { resolve as resolve3 } from "path";
127193
+ import { resolve as resolve4 } from "path";
126682
127194
  var MAX_OUTPUT_BYTES3 = 20000;
126683
127195
  var RG_UNAVAILABLE_SIGNATURE = /ripgrep \(rg\) is not installed|Executable not found[^\n]*\brg\b|\brg\b[^\n]*\bENOENT\b|\bENOENT\b[^\n]*\brg\b/i;
126684
127196
  var SEARCH_CODE_RG_UNAVAILABLE_MESSAGE = "ripgrep (rg) is not installed or not on PATH, and search_code needs it. Install it " + "(`brew install ripgrep` / `apt install ripgrep`), or use read_file and list_dir to " + "inspect files directly instead of retrying search_code.";
@@ -126689,11 +127201,11 @@ function normalizeSearchResult(result) {
126689
127201
  return result;
126690
127202
  }
126691
127203
  var KERYX_ENTRIES = [
126692
- resolve3(import.meta.dir, "..", "..", "..", "cli.ts"),
126693
- resolve3(import.meta.dir, "cli.js")
127204
+ resolve4(import.meta.dir, "..", "..", "..", "cli.ts"),
127205
+ resolve4(import.meta.dir, "cli.js")
126694
127206
  ];
126695
127207
  function keryxSelfCommand(main = Bun.main, execPath = process.execPath, exists = existsSync43, entries = KERYX_ENTRIES) {
126696
- if (entries.includes(resolve3(main)) && exists(main)) {
127208
+ if (entries.includes(resolve4(main)) && exists(main)) {
126697
127209
  return [execPath, ...SAFE_BUN_SPAWN_ARGS, main];
126698
127210
  }
126699
127211
  if (isCompiledBinaryEntry(main)) {
@@ -132825,6 +133337,7 @@ ${lines.join(`
132825
133337
  }
132826
133338
 
132827
133339
  // src/tui/tui-shell.ts
133340
+ init_subscription_models();
132828
133341
  init_agent();
132829
133342
  init_single_turn();
132830
133343
 
@@ -146485,6 +146998,7 @@ init_patch_risk();
146485
146998
  init_catalog();
146486
146999
  init_grants();
146487
147000
  init_login();
147001
+ init_open_url();
146488
147002
  init_shell_config();
146489
147003
  init_provider_config();
146490
147004
 
@@ -151854,14 +152368,22 @@ async function filterConnectedDetectedProviders(detected, options = {}) {
151854
152368
  const env = options.env ?? process.env;
151855
152369
  const connected = [];
151856
152370
  for (const prov of detected) {
151857
- const registry = providerByName(prov.name);
152371
+ if (prov.name === "openai-codex") {
152372
+ if (oauthAccessToken(prov.name, options.configDir) === undefined)
152373
+ continue;
152374
+ const result = await fetchOpenAiCodexModels(fetchFn, { ...options.configDir !== undefined ? { configDir: options.configDir } : {} });
152375
+ if (result.source === "live" && result.models.length > 0)
152376
+ connected.push({ ...prov, models: result.models });
152377
+ continue;
152378
+ }
152379
+ const registry = providerByName(prov.name, options.configDir);
151858
152380
  if (registry === undefined) {
151859
152381
  connected.push(prov);
151860
152382
  continue;
151861
152383
  }
151862
152384
  const requiresApiKey = registry.requiresApiKey ?? true;
151863
152385
  const envKey = prov.envKey ?? registry.envKey;
151864
- const raw = envKey !== undefined ? env[envKey] : undefined;
152386
+ const raw = envKey !== undefined ? env[envKey] ?? loadShellConfig(options.configDir).apiKeys?.[envKey] : undefined;
151865
152387
  if (requiresApiKey && (raw === undefined || raw.length === 0)) {
151866
152388
  continue;
151867
152389
  }
@@ -152946,7 +153468,7 @@ function pickAuthMethodStep(otui, target, providerLabel, methods) {
152946
153468
  });
152947
153469
  });
152948
153470
  }
152949
- function runDeviceLoginInTui(otui, target, provider, dir) {
153471
+ function runDeviceLoginInTui(otui, target, provider, dir, fetchFn = globalThis.fetch) {
152950
153472
  const r = stepRenderer(target);
152951
153473
  return new Promise((resolve) => {
152952
153474
  const controller = new AbortController;
@@ -152969,7 +153491,7 @@ function runDeviceLoginInTui(otui, target, provider, dir) {
152969
153491
  };
152970
153492
  loginDeviceCode({
152971
153493
  provider,
152972
- fetch: (input, init) => globalThis.fetch(input, init),
153494
+ fetch: (input, init) => fetchFn(input, init),
152973
153495
  signal: controller.signal,
152974
153496
  ...dir !== undefined ? { dir } : {},
152975
153497
  onChallenge: (challenge) => {
@@ -152982,7 +153504,7 @@ function runDeviceLoginInTui(otui, target, provider, dir) {
152982
153504
  }
152983
153505
  if (result.ok) {
152984
153506
  cleanup();
152985
- applyOAuthAccessToEnv();
153507
+ applyOAuthAccessToEnv(dir);
152986
153508
  resolve(true);
152987
153509
  return;
152988
153510
  }
@@ -152997,7 +153519,7 @@ function runDeviceLoginInTui(otui, target, provider, dir) {
152997
153519
  });
152998
153520
  }
152999
153521
  async function modelsForPicker(prov, deps = {}) {
153000
- return await resolveModelsForPicker(deps.fetch ?? globalThis.fetch, prov, deps.env ?? process.env);
153522
+ return await resolveModelsForPicker(deps.fetch ?? globalThis.fetch, prov, deps.env ?? process.env, { ...deps.configDir !== undefined ? { configDir: deps.configDir } : {} });
153001
153523
  }
153002
153524
  function endpointMayBeAtFault(failure) {
153003
153525
  return failure?.kind === "unreachable" || failure?.kind === "http";
@@ -153141,7 +153663,7 @@ function pickConnectedProviderStep(otui, target, detected, opts) {
153141
153663
  }
153142
153664
  async function runTest(name) {
153143
153665
  const block = rowBlocks.find((b) => b.name === name);
153144
- const provider = providerByName(name, opts.configDir);
153666
+ const provider = connectionProviderByName(name, opts.configDir);
153145
153667
  if (block === undefined)
153146
153668
  return;
153147
153669
  if (provider === undefined) {
@@ -153305,7 +153827,8 @@ function selectProviderModelInTui(otui, rOrChrome, detected, options = {}) {
153305
153827
  }
153306
153828
  const candidatesPromise = options.onlyConnected ? filterConnectedDetectedProviders(detected, {
153307
153829
  ...options.fetch !== undefined ? { fetch: options.fetch } : {},
153308
- ...options.env !== undefined ? { env: options.env } : {}
153830
+ ...options.env !== undefined ? { env: options.env } : {},
153831
+ ...options.configDir !== undefined ? { configDir: options.configDir } : {}
153309
153832
  }) : Promise.resolve(detected);
153310
153833
  (async () => {
153311
153834
  const candidates = await candidatesPromise;
@@ -153349,7 +153872,7 @@ function selectProviderModelInTui(otui, rOrChrome, detected, options = {}) {
153349
153872
  }
153350
153873
  continue;
153351
153874
  }
153352
- const lockDiscoveredHost = prov.name === "github-copilot";
153875
+ const lockDiscoveredHost = ["github-copilot", "openai", "openai-codex"].includes(prov.name);
153353
153876
  let selectedBaseUrl = options.onlyConnected || prov.baseUrl === undefined || lockDiscoveredHost ? prov.baseUrl : await promptBaseUrlStep(otui, rOrChrome, prov.label ?? prov.name, prov.baseUrl);
153354
153877
  if (!options.onlyConnected && !lockDiscoveredHost && prov.baseUrl !== undefined && selectedBaseUrl === undefined) {
153355
153878
  continue;
@@ -153361,12 +153884,12 @@ function selectProviderModelInTui(otui, rOrChrome, detected, options = {}) {
153361
153884
  const envKey = prov.envKey;
153362
153885
  const label = prov.label ?? prov.name;
153363
153886
  const collectCredential = async (notice) => {
153364
- if (options.onlyConnected || envKey === undefined) {
153887
+ if (options.onlyConnected || envKey === undefined && !catalogAllows(prov.name, "device-code")) {
153365
153888
  return "ok";
153366
153889
  }
153367
153890
  if (notice === undefined) {
153368
- const existingKey = (options.env ?? process.env)[envKey];
153369
- const hasOauth = oauthAccessToken(prov.name, options.configDir) !== undefined;
153891
+ const existingKey = envKey === undefined ? undefined : (options.env ?? process.env)[envKey] ?? loadShellConfig(options.configDir).apiKeys?.[envKey];
153892
+ const hasOauth = prov.name !== "openai" && oauthAccessToken(prov.name, options.configDir) !== undefined;
153370
153893
  if (existingKey !== undefined && existingKey.length > 0 || hasOauth) {
153371
153894
  return "ok";
153372
153895
  }
@@ -153384,7 +153907,7 @@ function selectProviderModelInTui(otui, rOrChrome, detected, options = {}) {
153384
153907
  }
153385
153908
  }
153386
153909
  if (method === "device-code") {
153387
- if (!await runDeviceLoginInTui(otui, rOrChrome, prov.name, options.configDir)) {
153910
+ if (!await runDeviceLoginInTui(otui, rOrChrome, prov.name, options.configDir, options.fetch)) {
153388
153911
  return "back";
153389
153912
  }
153390
153913
  const saved = loadShellConfig(options.configDir).baseUrls?.[prov.name];
@@ -153394,8 +153917,10 @@ function selectProviderModelInTui(otui, rOrChrome, detected, options = {}) {
153394
153917
  }
153395
153918
  return "ok";
153396
153919
  }
153920
+ if (envKey === undefined)
153921
+ return "back";
153397
153922
  const kr = await promptApiKeyStep(otui, rOrChrome, {
153398
- label,
153923
+ label: prov.name === "openai" ? "OpenAI" : label,
153399
153924
  envKey,
153400
153925
  ...notice === undefined ? {} : { notice }
153401
153926
  });
@@ -153414,7 +153939,8 @@ function selectProviderModelInTui(otui, rOrChrome, detected, options = {}) {
153414
153939
  }
153415
153940
  const modelDeps = {
153416
153941
  ...options.fetch === undefined ? {} : { fetch: options.fetch },
153417
- ...options.env === undefined ? {} : { env: options.env }
153942
+ ...options.env === undefined ? {} : { env: options.env },
153943
+ ...options.configDir === undefined ? {} : { configDir: options.configDir }
153418
153944
  };
153419
153945
  let models = await modelsForPicker(selectedProvider, modelDeps);
153420
153946
  if (models.failure?.kind === "rejected") {
@@ -153423,7 +153949,7 @@ function selectProviderModelInTui(otui, rOrChrome, detected, options = {}) {
153423
153949
  }
153424
153950
  models = await modelsForPicker(selectedProvider, modelDeps);
153425
153951
  }
153426
- if (!options.onlyConnected && selectedProvider.baseUrl !== undefined && endpointMayBeAtFault(models.failure)) {
153952
+ if (!options.onlyConnected && !lockDiscoveredHost && selectedProvider.baseUrl !== undefined && endpointMayBeAtFault(models.failure)) {
153427
153953
  const corrected = await promptBaseUrlStep(otui, rOrChrome, label, selectedProvider.baseUrl, modelsFailureLine(label, models.failure));
153428
153954
  if (corrected === undefined) {
153429
153955
  continue;
@@ -156548,7 +157074,7 @@ Usage: /reasoning <${REASONING_EFFORT_LEVELS.join("|")}>
156548
157074
  io.onSystem?.(`Reasoning effort: ${wanted}
156549
157075
  `);
156550
157076
  const compatProvider = providerByName(currentSel.provider);
156551
- if (compatProvider !== undefined && compatProvider.reasoning === undefined) {
157077
+ if (currentSel.provider !== "openai" && compatProvider !== undefined && compatProvider.reasoning === undefined) {
156552
157078
  io.onSystem?.(`Note: ${currentSel.provider} is OpenAI-compatible with no "reasoning" entry \u2014 its reasoning ` + `is configured per-provider in llm-providers.json (reasoning.requestParams); this setting has no effect for it.
156553
157079
  `);
156554
157080
  }
@@ -156611,7 +157137,8 @@ Usage: /reasoning <${REASONING_EFFORT_LEVELS.join("|")}>
156611
157137
  `);
156612
157138
  }
156613
157139
  if (name === currentSel.provider) {
156614
- io.onSystem?.(`\u25C7 ${name} disconnected \u2014 this session keeps its already-loaded credential until you /connect another provider or restart.
157140
+ io.onSystem?.(name === "openai-codex" ? `\u25C7 ${name} disconnected \u2014 it stays selected, but the next turn requires a new login. Use /provider to reconnect or /connect to switch.
157141
+ ` : `\u25C7 ${name} disconnected \u2014 this session keeps its already-loaded credential until you /connect another provider or restart.
156615
157142
  `);
156616
157143
  }
156617
157144
  }
@@ -157954,6 +158481,17 @@ Starting a new session.
157954
158481
  modelParams = picked.modelParams ?? {};
157955
158482
  provider = makeActive();
157956
158483
  };
158484
+ const selectAndApply = async (options) => {
158485
+ if (deps.selectProviderModel === undefined)
158486
+ return;
158487
+ try {
158488
+ applySelection(await deps.selectProviderModel(io, options));
158489
+ } catch (cause) {
158490
+ system(`${cause instanceof Error ? cause.message : "Provider selection failed"}
158491
+ Kept the current session.
158492
+ `);
158493
+ }
158494
+ };
157957
158495
  for await (const line of io.lines) {
157958
158496
  io.onSafeBoundary?.();
157959
158497
  if (line.startsWith("/")) {
@@ -158091,12 +158629,19 @@ Kept the current session.
158091
158629
  `);
158092
158630
  continue;
158093
158631
  }
158094
- const picked = await deps.selectProviderModel(io, { onlyProvider: providerName });
158095
- applySelection(picked);
158632
+ await selectAndApply({ onlyProvider: providerName });
158096
158633
  continue;
158097
158634
  }
158098
158635
  if (command === "/provider") {
158099
158636
  if (argument.length > 0) {
158637
+ if (argument === "openai-codex") {
158638
+ if (deps.selectProviderModel === undefined) {
158639
+ system("Interactive subscription selection is not available. Run `keryx auth login openai-codex` and select an authorized model.\n");
158640
+ } else {
158641
+ await selectAndApply({ onlyProvider: "openai-codex" });
158642
+ }
158643
+ continue;
158644
+ }
158100
158645
  providerName = argument;
158101
158646
  provider = makeActive();
158102
158647
  continue;
@@ -158106,8 +158651,7 @@ Kept the current session.
158106
158651
  `);
158107
158652
  continue;
158108
158653
  }
158109
- const picked = await deps.selectProviderModel(io, {});
158110
- applySelection(picked);
158654
+ await selectAndApply({});
158111
158655
  continue;
158112
158656
  }
158113
158657
  if (command === "/connect") {
@@ -158115,8 +158659,7 @@ Kept the current session.
158115
158659
  system(CONNECT_GUIDANCE);
158116
158660
  continue;
158117
158661
  }
158118
- const picked = await deps.selectProviderModel(io, { onlyConnected: true });
158119
- applySelection(picked);
158662
+ await selectAndApply({ onlyConnected: true });
158120
158663
  continue;
158121
158664
  }
158122
158665
  const wrongMode = describeUnavailableCommand(command, "chat");
@@ -158224,6 +158767,8 @@ function realMakeProvider(write) {
158224
158767
  };
158225
158768
  }
158226
158769
  function oauthCredentialsFor(name) {
158770
+ if (name === "openai" || name === "openai-codex")
158771
+ return {};
158227
158772
  const definition = providerByName(name);
158228
158773
  const envKey = definition?.envKey;
158229
158774
  if (envKey === undefined)
@@ -169511,6 +170056,7 @@ async function routingCommand(args, deps = {}) {
169511
170056
  init_catalog();
169512
170057
  init_grants();
169513
170058
  init_login();
170059
+ init_open_url();
169514
170060
  function printAuthHelp() {
169515
170061
  console.log(`Usage:
169516
170062
  keryx auth list [--json]
@@ -169518,7 +170064,7 @@ function printAuthHelp() {
169518
170064
  keryx auth logout <provider>
169519
170065
  keryx auth status <provider> [--json]
169520
170066
 
169521
- Providers with a sanctioned subscription login: grok (SuperGrok), openai (ChatGPT Plus/Pro), github-copilot.
170067
+ Providers with a sanctioned subscription login: grok (SuperGrok), openai-codex (ChatGPT Plus/Pro), github-copilot.
169522
170068
  Claude Pro/Max, Gemini Google-account login, and DeepSeek OAuth are not offered.
169523
170069
  `);
169524
170070
  }
@@ -169574,7 +170120,9 @@ function runAuthList(args) {
169574
170120
  }
169575
170121
  }
169576
170122
  async function runAuthLogin(args) {
169577
- const provider = args[0];
170123
+ const provider = args[0] === "openai" ? "openai-codex" : args[0];
170124
+ if (args[0] === "openai")
170125
+ console.error("The OpenAI subscription login is now openai-codex; openai is API-key only.");
169578
170126
  if (provider === undefined || provider.length === 0) {
169579
170127
  console.error("Usage: keryx auth login <provider>");
169580
170128
  process.exitCode = 1;
@@ -169586,14 +170134,18 @@ async function runAuthLogin(args) {
169586
170134
  process.exitCode = 1;
169587
170135
  return;
169588
170136
  }
170137
+ const controller = new AbortController;
170138
+ const cancel = () => controller.abort();
170139
+ process.on("SIGINT", cancel);
169589
170140
  const result = await loginDeviceCode({
169590
170141
  provider,
170142
+ signal: controller.signal,
169591
170143
  fetch: (input, init) => globalThis.fetch(input, init),
169592
170144
  onChallenge: (challenge) => {
169593
170145
  console.log(challenge.instructions);
169594
170146
  openVerificationUrl(challenge.verificationUriComplete ?? challenge.verificationUri);
169595
170147
  }
169596
- });
170148
+ }).finally(() => process.removeListener("SIGINT", cancel));
169597
170149
  if (!result.ok) {
169598
170150
  console.error(result.error);
169599
170151
  process.exitCode = 1;
@@ -169603,7 +170155,9 @@ async function runAuthLogin(args) {
169603
170155
  console.log(`Authorized ${provider} via ${deviceCodeMethodLabel(provider)}.`);
169604
170156
  }
169605
170157
  function runAuthLogout(args) {
169606
- const provider = args[0];
170158
+ const provider = args[0] === "openai" ? "openai-codex" : args[0];
170159
+ if (args[0] === "openai")
170160
+ console.error("The OpenAI subscription login is now openai-codex; openai is API-key only.");
169607
170161
  if (provider === undefined || provider.length === 0) {
169608
170162
  console.error("Usage: keryx auth logout <provider>");
169609
170163
  process.exitCode = 1;
@@ -169613,7 +170167,9 @@ function runAuthLogout(args) {
169613
170167
  console.log(`Logged out ${provider}.`);
169614
170168
  }
169615
170169
  function runAuthStatus(args) {
169616
- const provider = args[0];
170170
+ const provider = args[0] === "openai" ? "openai-codex" : args[0];
170171
+ if (args[0] === "openai")
170172
+ console.error("The OpenAI subscription login is now openai-codex; openai is API-key only.");
169617
170173
  if (provider === undefined || provider.length === 0) {
169618
170174
  console.error("Usage: keryx auth status <provider>");
169619
170175
  process.exitCode = 1;