@mrciphersmith/keryx 0.3.3 → 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 (140) hide show
  1. package/dist/cli.js +996 -366
  2. package/docs/README.md +2 -0
  3. package/package.json +1 -1
  4. package/src/gdskills/bundled/install-manifest.json +520 -48
  5. package/src/gdskills/bundled/stacks/csharp-dotnet/agent-refs.json +4 -0
  6. package/src/gdskills/bundled/stacks/csharp-dotnet/governance/eval.json +1881 -0
  7. package/src/gdskills/bundled/stacks/csharp-dotnet/governance/scout.json +33 -0
  8. package/src/gdskills/bundled/stacks/csharp-dotnet/pack.json +38 -0
  9. package/src/gdskills/bundled/stacks/csharp-dotnet/rules/coding-style.mdc +100 -0
  10. package/src/gdskills/bundled/stacks/csharp-dotnet/rules/patterns.mdc +107 -0
  11. package/src/gdskills/bundled/stacks/csharp-dotnet/rules/security.mdc +86 -0
  12. package/src/gdskills/bundled/stacks/csharp-dotnet/rules/testing.mdc +89 -0
  13. package/src/gdskills/bundled/stacks/csharp-dotnet/skills/dotnet-build-fix/SKILL.md +143 -0
  14. package/src/gdskills/bundled/stacks/csharp-dotnet/skills/dotnet-build-fix/evals.json +77 -0
  15. package/src/gdskills/bundled/stacks/csharp-dotnet/skills/dotnet-code-review/SKILL.md +121 -0
  16. package/src/gdskills/bundled/stacks/csharp-dotnet/skills/dotnet-code-review/evals.json +77 -0
  17. package/src/gdskills/bundled/stacks/csharp-dotnet/skills/dotnet-implementation/SKILL.md +134 -0
  18. package/src/gdskills/bundled/stacks/csharp-dotnet/skills/dotnet-implementation/evals.json +76 -0
  19. package/src/gdskills/bundled/stacks/csharp-dotnet/skills/dotnet-testing/SKILL.md +130 -0
  20. package/src/gdskills/bundled/stacks/csharp-dotnet/skills/dotnet-testing/evals.json +77 -0
  21. package/src/gdskills/bundled/stacks/django/agent-refs.json +3 -0
  22. package/src/gdskills/bundled/stacks/django/governance/eval.json +1763 -0
  23. package/src/gdskills/bundled/stacks/django/governance/scout.json +40 -0
  24. package/src/gdskills/bundled/stacks/django/pack.json +43 -0
  25. package/src/gdskills/bundled/stacks/django/rules/coding-style.mdc +80 -0
  26. package/src/gdskills/bundled/stacks/django/rules/patterns.mdc +92 -0
  27. package/src/gdskills/bundled/stacks/django/rules/security.mdc +92 -0
  28. package/src/gdskills/bundled/stacks/django/rules/testing.mdc +89 -0
  29. package/src/gdskills/bundled/stacks/django/skills/django-build-fix/SKILL.md +149 -0
  30. package/src/gdskills/bundled/stacks/django/skills/django-build-fix/evals.json +49 -0
  31. package/src/gdskills/bundled/stacks/django/skills/django-code-review/SKILL.md +137 -0
  32. package/src/gdskills/bundled/stacks/django/skills/django-code-review/evals.json +48 -0
  33. package/src/gdskills/bundled/stacks/django/skills/django-implementation/SKILL.md +147 -0
  34. package/src/gdskills/bundled/stacks/django/skills/django-implementation/evals.json +75 -0
  35. package/src/gdskills/bundled/stacks/django/skills/django-migrate/SKILL.md +166 -0
  36. package/src/gdskills/bundled/stacks/django/skills/django-migrate/evals.json +49 -0
  37. package/src/gdskills/bundled/stacks/django/skills/django-testing/SKILL.md +130 -0
  38. package/src/gdskills/bundled/stacks/django/skills/django-testing/evals.json +48 -0
  39. package/src/gdskills/bundled/stacks/fastapi/agent-refs.json +3 -0
  40. package/src/gdskills/bundled/stacks/fastapi/governance/eval.json +1777 -0
  41. package/src/gdskills/bundled/stacks/fastapi/governance/scout.json +34 -0
  42. package/src/gdskills/bundled/stacks/fastapi/pack.json +43 -0
  43. package/src/gdskills/bundled/stacks/fastapi/rules/coding-style.mdc +68 -0
  44. package/src/gdskills/bundled/stacks/fastapi/rules/patterns.mdc +108 -0
  45. package/src/gdskills/bundled/stacks/fastapi/rules/security.mdc +99 -0
  46. package/src/gdskills/bundled/stacks/fastapi/rules/testing.mdc +85 -0
  47. package/src/gdskills/bundled/stacks/fastapi/skills/fastapi-build-fix/SKILL.md +157 -0
  48. package/src/gdskills/bundled/stacks/fastapi/skills/fastapi-build-fix/evals.json +76 -0
  49. package/src/gdskills/bundled/stacks/fastapi/skills/fastapi-code-review/SKILL.md +150 -0
  50. package/src/gdskills/bundled/stacks/fastapi/skills/fastapi-code-review/evals.json +74 -0
  51. package/src/gdskills/bundled/stacks/fastapi/skills/fastapi-implementation/SKILL.md +158 -0
  52. package/src/gdskills/bundled/stacks/fastapi/skills/fastapi-implementation/evals.json +75 -0
  53. package/src/gdskills/bundled/stacks/fastapi/skills/fastapi-testing/SKILL.md +146 -0
  54. package/src/gdskills/bundled/stacks/fastapi/skills/fastapi-testing/evals.json +74 -0
  55. package/src/gdskills/bundled/stacks/flutter-dart/agent-refs.json +4 -0
  56. package/src/gdskills/bundled/stacks/flutter-dart/governance/eval.json +1849 -0
  57. package/src/gdskills/bundled/stacks/flutter-dart/governance/scout.json +33 -0
  58. package/src/gdskills/bundled/stacks/flutter-dart/pack.json +41 -0
  59. package/src/gdskills/bundled/stacks/flutter-dart/rules/coding-style.mdc +98 -0
  60. package/src/gdskills/bundled/stacks/flutter-dart/rules/patterns.mdc +88 -0
  61. package/src/gdskills/bundled/stacks/flutter-dart/rules/security.mdc +91 -0
  62. package/src/gdskills/bundled/stacks/flutter-dart/rules/testing.mdc +101 -0
  63. package/src/gdskills/bundled/stacks/flutter-dart/skills/flutter-build-fix/SKILL.md +134 -0
  64. package/src/gdskills/bundled/stacks/flutter-dart/skills/flutter-build-fix/evals.json +79 -0
  65. package/src/gdskills/bundled/stacks/flutter-dart/skills/flutter-code-review/SKILL.md +124 -0
  66. package/src/gdskills/bundled/stacks/flutter-dart/skills/flutter-code-review/evals.json +74 -0
  67. package/src/gdskills/bundled/stacks/flutter-dart/skills/flutter-implementation/SKILL.md +139 -0
  68. package/src/gdskills/bundled/stacks/flutter-dart/skills/flutter-implementation/evals.json +77 -0
  69. package/src/gdskills/bundled/stacks/flutter-dart/skills/flutter-testing/SKILL.md +134 -0
  70. package/src/gdskills/bundled/stacks/flutter-dart/skills/flutter-testing/evals.json +74 -0
  71. package/src/gdskills/bundled/stacks/java-kotlin-spring/agent-refs.json +3 -0
  72. package/src/gdskills/bundled/stacks/java-kotlin-spring/governance/eval.json +2194 -0
  73. package/src/gdskills/bundled/stacks/java-kotlin-spring/governance/scout.json +39 -0
  74. package/src/gdskills/bundled/stacks/java-kotlin-spring/pack.json +40 -0
  75. package/src/gdskills/bundled/stacks/java-kotlin-spring/rules/coding-style.mdc +67 -0
  76. package/src/gdskills/bundled/stacks/java-kotlin-spring/rules/patterns.mdc +65 -0
  77. package/src/gdskills/bundled/stacks/java-kotlin-spring/rules/security.mdc +69 -0
  78. package/src/gdskills/bundled/stacks/java-kotlin-spring/rules/testing.mdc +80 -0
  79. package/src/gdskills/bundled/stacks/java-kotlin-spring/skills/java-kotlin-spring-build-fix/SKILL.md +144 -0
  80. package/src/gdskills/bundled/stacks/java-kotlin-spring/skills/java-kotlin-spring-build-fix/evals.json +74 -0
  81. package/src/gdskills/bundled/stacks/java-kotlin-spring/skills/java-kotlin-spring-code-review/SKILL.md +129 -0
  82. package/src/gdskills/bundled/stacks/java-kotlin-spring/skills/java-kotlin-spring-code-review/evals.json +74 -0
  83. package/src/gdskills/bundled/stacks/java-kotlin-spring/skills/java-kotlin-spring-implementation/SKILL.md +147 -0
  84. package/src/gdskills/bundled/stacks/java-kotlin-spring/skills/java-kotlin-spring-implementation/evals.json +75 -0
  85. package/src/gdskills/bundled/stacks/java-kotlin-spring/skills/java-kotlin-spring-migrate/SKILL.md +139 -0
  86. package/src/gdskills/bundled/stacks/java-kotlin-spring/skills/java-kotlin-spring-migrate/evals.json +74 -0
  87. package/src/gdskills/bundled/stacks/java-kotlin-spring/skills/java-kotlin-spring-testing/SKILL.md +128 -0
  88. package/src/gdskills/bundled/stacks/java-kotlin-spring/skills/java-kotlin-spring-testing/evals.json +73 -0
  89. package/src/gdskills/bundled/stacks/kotlin-android/agent-refs.json +4 -0
  90. package/src/gdskills/bundled/stacks/kotlin-android/governance/eval.json +1889 -0
  91. package/src/gdskills/bundled/stacks/kotlin-android/governance/scout.json +34 -0
  92. package/src/gdskills/bundled/stacks/kotlin-android/pack.json +38 -0
  93. package/src/gdskills/bundled/stacks/kotlin-android/rules/coding-style.mdc +89 -0
  94. package/src/gdskills/bundled/stacks/kotlin-android/rules/patterns.mdc +96 -0
  95. package/src/gdskills/bundled/stacks/kotlin-android/rules/security.mdc +90 -0
  96. package/src/gdskills/bundled/stacks/kotlin-android/rules/testing.mdc +89 -0
  97. package/src/gdskills/bundled/stacks/kotlin-android/skills/compose-implementation/SKILL.md +150 -0
  98. package/src/gdskills/bundled/stacks/kotlin-android/skills/compose-implementation/evals.json +77 -0
  99. package/src/gdskills/bundled/stacks/kotlin-android/skills/kotlin-android-build-fix/SKILL.md +151 -0
  100. package/src/gdskills/bundled/stacks/kotlin-android/skills/kotlin-android-build-fix/evals.json +76 -0
  101. package/src/gdskills/bundled/stacks/kotlin-android/skills/kotlin-android-code-review/SKILL.md +139 -0
  102. package/src/gdskills/bundled/stacks/kotlin-android/skills/kotlin-android-code-review/evals.json +78 -0
  103. package/src/gdskills/bundled/stacks/kotlin-android/skills/kotlin-android-testing/SKILL.md +131 -0
  104. package/src/gdskills/bundled/stacks/kotlin-android/skills/kotlin-android-testing/evals.json +77 -0
  105. package/src/gdskills/bundled/stacks/python/agent-refs.json +2 -1
  106. package/src/gdskills/bundled/stacks/python/pack.json +1 -1
  107. package/src/gdskills/bundled/stacks/rust/agent-refs.json +3 -0
  108. package/src/gdskills/bundled/stacks/rust/governance/eval.json +1823 -0
  109. package/src/gdskills/bundled/stacks/rust/governance/scout.json +32 -0
  110. package/src/gdskills/bundled/stacks/rust/pack.json +42 -0
  111. package/src/gdskills/bundled/stacks/rust/rules/coding-style.mdc +93 -0
  112. package/src/gdskills/bundled/stacks/rust/rules/patterns.mdc +85 -0
  113. package/src/gdskills/bundled/stacks/rust/rules/security.mdc +85 -0
  114. package/src/gdskills/bundled/stacks/rust/rules/testing.mdc +82 -0
  115. package/src/gdskills/bundled/stacks/rust/skills/rust-build-fix/SKILL.md +141 -0
  116. package/src/gdskills/bundled/stacks/rust/skills/rust-build-fix/evals.json +78 -0
  117. package/src/gdskills/bundled/stacks/rust/skills/rust-code-review/SKILL.md +127 -0
  118. package/src/gdskills/bundled/stacks/rust/skills/rust-code-review/evals.json +72 -0
  119. package/src/gdskills/bundled/stacks/rust/skills/rust-implementation/SKILL.md +133 -0
  120. package/src/gdskills/bundled/stacks/rust/skills/rust-implementation/evals.json +79 -0
  121. package/src/gdskills/bundled/stacks/rust/skills/rust-testing/SKILL.md +130 -0
  122. package/src/gdskills/bundled/stacks/rust/skills/rust-testing/evals.json +75 -0
  123. package/src/gdskills/bundled/stacks/swift-ios/agent-refs.json +4 -0
  124. package/src/gdskills/bundled/stacks/swift-ios/governance/eval.json +1803 -0
  125. package/src/gdskills/bundled/stacks/swift-ios/governance/scout.json +32 -0
  126. package/src/gdskills/bundled/stacks/swift-ios/pack.json +38 -0
  127. package/src/gdskills/bundled/stacks/swift-ios/rules/coding-style.mdc +92 -0
  128. package/src/gdskills/bundled/stacks/swift-ios/rules/patterns.mdc +112 -0
  129. package/src/gdskills/bundled/stacks/swift-ios/rules/security.mdc +78 -0
  130. package/src/gdskills/bundled/stacks/swift-ios/rules/testing.mdc +90 -0
  131. package/src/gdskills/bundled/stacks/swift-ios/skills/swift-build-fix/SKILL.md +144 -0
  132. package/src/gdskills/bundled/stacks/swift-ios/skills/swift-build-fix/evals.json +75 -0
  133. package/src/gdskills/bundled/stacks/swift-ios/skills/swift-code-review/SKILL.md +122 -0
  134. package/src/gdskills/bundled/stacks/swift-ios/skills/swift-code-review/evals.json +75 -0
  135. package/src/gdskills/bundled/stacks/swift-ios/skills/swift-testing/SKILL.md +131 -0
  136. package/src/gdskills/bundled/stacks/swift-ios/skills/swift-testing/evals.json +75 -0
  137. package/src/gdskills/bundled/stacks/swift-ios/skills/swiftui-implementation/SKILL.md +149 -0
  138. package/src/gdskills/bundled/stacks/swift-ios/skills/swiftui-implementation/evals.json +76 -0
  139. package/src/gdskills/bundled/agents/python-build-fixer.md +0 -52
  140. 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.3",
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();
@@ -96156,6 +96698,28 @@ function clauseFileKindApplicability(sourceCategory, clauseText, changedFileKind
96156
96698
  }
96157
96699
  return { applicable: true, reason: "hunk is a code file; rule source is not categorised docs" };
96158
96700
  }
96701
+ var API_SCOPE_TERMS = ["api", "endpoint", "rest", "http", "openapi"];
96702
+ function isApiScopedRuleSource(source) {
96703
+ const haystack = `${(source.declaredPaths ?? []).join(" ")} ${ruleSourceTitle(source.text)} ${source.path}`.toLowerCase();
96704
+ return API_SCOPE_TERMS.some((term) => new RegExp(`\\b${term}\\b`).test(haystack));
96705
+ }
96706
+ var API_SURFACE_DIR_RE = /(^|\/)(api|routes|controllers|handlers|server)\//i;
96707
+ var HTTP_FRAMEWORK_IMPORT_RE = /\bfrom\s*["'](?:express|fastify|koa|hono|@hono\/[\w-]+|hapi|@nestjs\/(?:common|core)|next\/server|h3)["']/;
96708
+ function looksLikeApiSurfaceHunk(region) {
96709
+ return API_SURFACE_DIR_RE.test(region.path) || HTTP_FRAMEWORK_IMPORT_RE.test(region.text);
96710
+ }
96711
+ function clauseApiScopeApplicability(source, region) {
96712
+ if (!isApiScopedRuleSource(source)) {
96713
+ return { applicable: true, reason: "rule source is not API/endpoint-scoped \u2014 this gate does not apply" };
96714
+ }
96715
+ if (looksLikeApiSurfaceHunk(region)) {
96716
+ return { applicable: true, reason: `rule source is API/endpoint-scoped and ${region.path} looks like an API surface` };
96717
+ }
96718
+ return {
96719
+ applicable: false,
96720
+ reason: `rule source is API/endpoint-scoped (its declared paths/description/title name one of: ${API_SCOPE_TERMS.join(", ")}) but ${region.path} ` + "does not look like an API surface (no api/routes/controllers/handlers/server path segment, and this hunk imports no known HTTP framework)"
96721
+ };
96722
+ }
96159
96723
  var DEFAULT_MAX_JEV_RULE_CALLS = 150;
96160
96724
  function isHunkCheckableClause(clause) {
96161
96725
  return clause.state_kind === "hunk" && clause.checkable;
@@ -96199,6 +96763,11 @@ function selectRuleHunkPairs(regions, taggedSources, detectedStack, maxCalls = D
96199
96763
  notApplicable.push({ region, ruleId: source.path, reason: decision.reason });
96200
96764
  continue;
96201
96765
  }
96766
+ const apiScopeDecision = clauseApiScopeApplicability(source, region);
96767
+ if (!apiScopeDecision.applicable) {
96768
+ notApplicable.push({ region, ruleId: source.path, reason: apiScopeDecision.reason });
96769
+ continue;
96770
+ }
96202
96771
  let sawHunkCheckable = false;
96203
96772
  let anyFileKindApplicable = false;
96204
96773
  for (const clause of clauses) {
@@ -96620,7 +97189,7 @@ async function walkMatching(root, suffixes) {
96620
97189
  return;
96621
97190
  }
96622
97191
  for (const entry of entries) {
96623
- const full = join4(dir, entry.name);
97192
+ const full = join5(dir, entry.name);
96624
97193
  if (entry.isDirectory()) {
96625
97194
  await walk(full);
96626
97195
  continue;
@@ -96645,7 +97214,7 @@ function classify2(file) {
96645
97214
  }
96646
97215
  async function discoverRuleSources(cwd, explicitPaths) {
96647
97216
  const discovered = [];
96648
- for (const root of [join4(cwd, ".metaproject", "rules"), join4(cwd, "rules")]) {
97217
+ for (const root of [join5(cwd, ".metaproject", "rules"), join5(cwd, "rules")]) {
96649
97218
  for (const file of await walkMatching(root, [".md", ".mdc"])) {
96650
97219
  const text = await readIfExists(file);
96651
97220
  if (text === undefined)
@@ -96654,8 +97223,8 @@ async function discoverRuleSources(cwd, explicitPaths) {
96654
97223
  }
96655
97224
  }
96656
97225
  const skillRoots = [
96657
- [join4(cwd, ".metaproject", "project-skills"), "project-skill"],
96658
- [join4(cwd, ".metaproject", "skills", "gdskills"), "gdskill"]
97226
+ [join5(cwd, ".metaproject", "project-skills"), "project-skill"],
97227
+ [join5(cwd, ".metaproject", "skills", "gdskills"), "gdskill"]
96659
97228
  ];
96660
97229
  for (const [root, kind] of skillRoots) {
96661
97230
  for (const skillMd of await walkMatching(root, ["SKILL.md"])) {
@@ -96680,7 +97249,7 @@ async function discoverRuleSources(cwd, explicitPaths) {
96680
97249
  const explicitFiles = [];
96681
97250
  const explicitDirSources = [];
96682
97251
  for (const raw of explicitPaths) {
96683
- const resolved = isAbsolute3(raw) ? raw : join4(cwd, raw);
97252
+ const resolved = isAbsolute3(raw) ? raw : join5(cwd, raw);
96684
97253
  const stats = await stat22(resolved).catch(() => {
96685
97254
  return;
96686
97255
  });
@@ -96723,11 +97292,11 @@ async function discoverRuleSources(cwd, explicitPaths) {
96723
97292
  return { sources: [...byPath.values()].sort((a, b) => a.path.localeCompare(b.path)), excluded: excluded.sort((a, b) => a.path.localeCompare(b.path)) };
96724
97293
  }
96725
97294
  async function fixturePrPort(dir) {
96726
- const raw = await readFile118(join4(dir, "pr.json"), "utf8");
97295
+ const raw = await readFile118(join5(dir, "pr.json"), "utf8");
96727
97296
  return createFixtureConformPrPort({ pr: JSON.parse(raw) });
96728
97297
  }
96729
97298
  async function fixtureJevFetch(dir) {
96730
- const raw = await readFile118(join4(dir, "jev-responses.json"), "utf8");
97299
+ const raw = await readFile118(join5(dir, "jev-responses.json"), "utf8");
96731
97300
  const responses = JSON.parse(raw);
96732
97301
  let index = 0;
96733
97302
  const fn = async () => {
@@ -97042,6 +97611,10 @@ function touchedExportedSymbols(regionText) {
97042
97611
  return [...symbols];
97043
97612
  }
97044
97613
  var TEST_INFIX_RE = /\.(test|spec)\./;
97614
+ var TEST_DIR_RE = /(^|\/)(__tests__|tests?)\//;
97615
+ function isTestFilePath(path) {
97616
+ return TEST_INFIX_RE.test(path) || TEST_DIR_RE.test(path);
97617
+ }
97045
97618
  function stemOf(filePath) {
97046
97619
  const base = filePath.split("/").at(-1) ?? filePath;
97047
97620
  return base.replace(/\.(test|spec)\.[jt]sx?$/, "").replace(/\.[jt]sx?$/, "");
@@ -97056,7 +97629,7 @@ function testFilesTouchedNearby(regionPath, allChangedFiles) {
97056
97629
  return allChangedFiles.filter((file) => {
97057
97630
  if (file === regionPath)
97058
97631
  return false;
97059
- if (!TEST_INFIX_RE.test(file) && !/(^|\/)(__tests__|tests?)\//.test(file))
97632
+ if (!isTestFilePath(file))
97060
97633
  return false;
97061
97634
  return stemOf(file) === stem || dirOf(file) === dir;
97062
97635
  });
@@ -97163,7 +97736,10 @@ function batchRiskQuestionsForHunk(facts) {
97163
97736
  return batches;
97164
97737
  }
97165
97738
  var NON_CODE_HUNK_RE = /\.(md|txt)$/i;
97739
+ var GENERATED_METADATA_PATH_RE = /(^|\/)\.metaproject\/(flows|data|reviews)\//;
97166
97740
  function isNonCodeHunk(path) {
97741
+ if (GENERATED_METADATA_PATH_RE.test(path))
97742
+ return true;
97167
97743
  if (/(^|\/)(__fixtures__|fixtures|testdata|__snapshots__|snapshots)\//.test(path))
97168
97744
  return false;
97169
97745
  return NON_CODE_HUNK_RE.test(path);
@@ -97237,6 +97813,8 @@ function synthesizeRiskFindings(scored, threshold = DEFAULT_JEV_RISK_THRESHOLD)
97237
97813
  for (const hunk of rankHunksByRisk(scored)) {
97238
97814
  if (hunk.combinedRisk < threshold || hunk.facts.hasNearbyTest)
97239
97815
  continue;
97816
+ if (isTestFilePath(hunk.facts.region.path))
97817
+ continue;
97240
97818
  const { region } = hunk.facts;
97241
97819
  const key = `jev-risk::${region.path}:${region.startLine}-${region.endLine}`;
97242
97820
  const severity = hunk.combinedRisk >= 0.85 ? "minor" : "info";
@@ -97267,6 +97845,8 @@ function riskFindingStats(findings) {
97267
97845
  function computeRiskRoutingHints(scored, threshold = DEFAULT_JEV_RISK_THRESHOLD) {
97268
97846
  const hints = [];
97269
97847
  for (const hunk of rankHunksByRisk(scored)) {
97848
+ if (isTestFilePath(hunk.facts.region.path))
97849
+ continue;
97270
97850
  const { region } = hunk.facts;
97271
97851
  if (hunk.probabilities.security >= threshold) {
97272
97852
  hints.push({ file: region.path, startLine: region.startLine, endLine: region.endLine, dimension: "security", probability: hunk.probabilities.security, suggestedReviewer: "review-security-code" });
@@ -97420,7 +98000,7 @@ async function computeJevRiskResult(options) {
97420
98000
  topDimension: hunk.topDimension
97421
98001
  }));
97422
98002
  const status = findings.length > 0 ? "DONE_WITH_CONCERNS" : "DONE";
97423
- const summary = `Scored ${scored.length} hunk(s) (of ${selection.selected.length + selection.skipped.length} retained) across 5 risk dimensions against ${targetLabel}; ` + `${findings.length} finding(s) at/above threshold ${threshold} with no nearby test. ` + `${selection.skipped.length} hunk(s) skipped by --max-calls ${selection.maxCalls} (${selection.pairsSkipped} pair(s)); ` + `${selection.notCode.length} hunk(s) skipped as not a code hunk (docs/.md/.txt).`;
98003
+ const summary = `Scored ${scored.length} hunk(s) (of ${selection.selected.length + selection.skipped.length} retained) across 5 risk dimensions against ${targetLabel}; ` + `${findings.length} finding(s) at/above threshold ${threshold} with no nearby test. ` + `${selection.skipped.length} hunk(s) skipped by --max-calls ${selection.maxCalls} (${selection.pairsSkipped} pair(s)); ` + `${selection.notCode.length} hunk(s) skipped as not a code hunk (docs/.md/.txt, or .metaproject/{flows,data,reviews} bookkeeping).`;
97424
98004
  return {
97425
98005
  status,
97426
98006
  reviewer: "review-jev-risk",
@@ -97519,7 +98099,7 @@ async function runJevRisk(args) {
97519
98099
  // src/commands/review-jev-scenarios.ts
97520
98100
  init_args();
97521
98101
  import { readFile as readFile122, readdir as readdir45 } from "fs/promises";
97522
- import { join as join5, relative as relative4 } from "path";
98102
+ import { join as join6, relative as relative4 } from "path";
97523
98103
 
97524
98104
  // src/review/jev-scenarios-config.ts
97525
98105
  init_review_gate();
@@ -97862,7 +98442,7 @@ async function walkMarkdown2(root) {
97862
98442
  return;
97863
98443
  }
97864
98444
  for (const entry of entries) {
97865
- const full = join5(dir, entry.name);
98445
+ const full = join6(dir, entry.name);
97866
98446
  if (entry.isDirectory()) {
97867
98447
  await walk(full);
97868
98448
  continue;
@@ -97876,19 +98456,19 @@ async function walkMarkdown2(root) {
97876
98456
  }
97877
98457
  async function discoverScenarioSources(cwd) {
97878
98458
  const scenarios = [];
97879
- 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"))) {
97880
98460
  const text = await readIfExists2(file);
97881
98461
  if (text === undefined)
97882
98462
  continue;
97883
98463
  scenarios.push(scenarioFromWikiPage(relative4(cwd, file), text));
97884
98464
  }
97885
- for (const file of await walkMarkdown2(join5(cwd, "docs", "requirements"))) {
98465
+ for (const file of await walkMarkdown2(join6(cwd, "docs", "requirements"))) {
97886
98466
  const text = await readIfExists2(file);
97887
98467
  if (text === undefined)
97888
98468
  continue;
97889
98469
  scenarios.push(...scenariosFromPrd(relative4(cwd, file), text));
97890
98470
  }
97891
- 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"))];
97892
98472
  for (const file of readmeLikeFiles) {
97893
98473
  const text = await readIfExists2(file);
97894
98474
  if (text === undefined)
@@ -97898,11 +98478,11 @@ async function discoverScenarioSources(cwd) {
97898
98478
  return scenarios;
97899
98479
  }
97900
98480
  async function fixturePrPort3(dir) {
97901
- const raw = await readFile122(join5(dir, "pr.json"), "utf8");
98481
+ const raw = await readFile122(join6(dir, "pr.json"), "utf8");
97902
98482
  return createFixtureConformPrPort({ pr: JSON.parse(raw) });
97903
98483
  }
97904
98484
  async function fixtureJevFetch3(dir) {
97905
- const raw = await readFile122(join5(dir, "jev-responses.json"), "utf8");
98485
+ const raw = await readFile122(join6(dir, "jev-responses.json"), "utf8");
97906
98486
  const responses = JSON.parse(raw);
97907
98487
  let index = 0;
97908
98488
  const fn = async () => {
@@ -98058,7 +98638,7 @@ async function gitDiffForJevScenarios(ref) {
98058
98638
  // src/commands/review-jev-docs.ts
98059
98639
  init_args();
98060
98640
  import { readFile as readFile124, readdir as readdir46 } from "fs/promises";
98061
- import { join as join6, relative as relative5 } from "path";
98641
+ import { join as join7, relative as relative5 } from "path";
98062
98642
 
98063
98643
  // src/review/jev-docs-config.ts
98064
98644
  init_review_gate();
@@ -98266,7 +98846,6 @@ function boundLinkedSections(linked, maxCalls = DEFAULT_MAX_JEV_DOCS_CALLS, maxP
98266
98846
  };
98267
98847
  }
98268
98848
  var JEV_DOCS_TOKEN_BUDGET = 64000;
98269
- var QUESTIONS_BUDGET_FRACTION4 = 0.5;
98270
98849
  var HUNK_TEXT_CHARS = 2000;
98271
98850
  var SECTION_TEXT_CHARS = 4000;
98272
98851
  function docsQuestionKey(linked) {
@@ -98300,19 +98879,25 @@ ${bounded}`;
98300
98879
  ].join(`
98301
98880
  `);
98302
98881
  }
98882
+ function buildDocsBatch(items) {
98883
+ return {
98884
+ items,
98885
+ state: items.map(renderSectionFacts).join(`
98886
+
98887
+ `),
98888
+ questions: Object.fromEntries(items.map((item) => [docsQuestionKey(item), questionFor4(item)]))
98889
+ };
98890
+ }
98891
+ var DOCS_BATCH_BUDGET_FRACTION = 0.4;
98892
+ var MAX_SECTIONS_PER_BATCH = 3;
98303
98893
  function batchDocsSections(items) {
98304
98894
  const batches = [];
98305
98895
  let current = [];
98896
+ const effectiveBudget = JEV_DOCS_TOKEN_BUDGET * DOCS_BATCH_BUDGET_FRACTION;
98306
98897
  const flush = () => {
98307
98898
  if (current.length === 0)
98308
98899
  return;
98309
- batches.push({
98310
- items: current,
98311
- state: current.map(renderSectionFacts).join(`
98312
-
98313
- `),
98314
- questions: Object.fromEntries(current.map((item) => [docsQuestionKey(item), questionFor4(item)]))
98315
- });
98900
+ batches.push(buildDocsBatch(current));
98316
98901
  current = [];
98317
98902
  };
98318
98903
  for (const item of items) {
@@ -98322,8 +98907,7 @@ function batchDocsSections(items) {
98322
98907
  `));
98323
98908
  const questionsTokens = estimateTokens2(attempt.map((linked) => `${docsQuestionKey(linked)}:noul:${questionFor4(linked).instructions}`).join(`
98324
98909
  `));
98325
- const withinQuestionsShare = questionsTokens <= JEV_DOCS_TOKEN_BUDGET * QUESTIONS_BUDGET_FRACTION4;
98326
- if (current.length > 0 && (stateTokens + questionsTokens > JEV_DOCS_TOKEN_BUDGET || !withinQuestionsShare)) {
98910
+ if (current.length > 0 && (attempt.length > MAX_SECTIONS_PER_BATCH || stateTokens + questionsTokens > effectiveBudget)) {
98327
98911
  flush();
98328
98912
  current = [item];
98329
98913
  } else {
@@ -98472,6 +99056,12 @@ function renderJevDocsMarkdown(result) {
98472
99056
  }
98473
99057
 
98474
99058
  // src/commands/review-jev-docs.ts
99059
+ function messageOf(error) {
99060
+ return error instanceof Error ? error.message : String(error);
99061
+ }
99062
+ function isMaxTokensExceededError(error) {
99063
+ return error instanceof JevRequestError && error.status === 400 && /max_tokens/i.test(error.message);
99064
+ }
98475
99065
  var JEV_DOCS_FLAGS = ["--diff", "--pr", "--max-calls", "--threshold", "--model", "--repo", "--fixtures", "--include", "--json"];
98476
99066
  function rejectUnknownJevDocsFlags(args) {
98477
99067
  const unknown = args.filter((arg) => arg.startsWith("--") && !JEV_DOCS_FLAGS.includes(arg.split("=")[0]));
@@ -98528,7 +99118,7 @@ async function walkMatching2(root, suffixes) {
98528
99118
  return;
98529
99119
  }
98530
99120
  for (const entry of entries) {
98531
- const full = join6(dir, entry.name);
99121
+ const full = join7(dir, entry.name);
98532
99122
  if (entry.isDirectory()) {
98533
99123
  await walk(full);
98534
99124
  continue;
@@ -98589,7 +99179,7 @@ async function walkAllDocFiles(dir) {
98589
99179
  return out;
98590
99180
  }
98591
99181
  for (const entry of entries) {
98592
- const full = join6(dir, entry.name);
99182
+ const full = join7(dir, entry.name);
98593
99183
  if (entry.isDirectory()) {
98594
99184
  if (INCLUDE_WALK_PRUNE_DIRS.has(entry.name))
98595
99185
  continue;
@@ -98603,19 +99193,19 @@ async function walkAllDocFiles(dir) {
98603
99193
  async function resolveIncludeGlob(cwd, glob) {
98604
99194
  const rootDir = includeGlobRootDir(glob);
98605
99195
  const regex = includeGlobToRegExp(glob);
98606
- const candidates = await walkAllDocFiles(join6(cwd, rootDir));
99196
+ const candidates = await walkAllDocFiles(join7(cwd, rootDir));
98607
99197
  return candidates.filter((file) => regex.test(relative5(cwd, file)));
98608
99198
  }
98609
99199
  async function discoverDocFiles(cwd, includeGlobs = []) {
98610
99200
  const files = new Set;
98611
- for (const file of await walkMatching2(join6(cwd, "docs"), [".md"]))
99201
+ for (const file of await walkMatching2(join7(cwd, "docs"), [".md"]))
98612
99202
  files.add(file);
98613
99203
  for (const name of await matchingFileNames(cwd, /^readme/i)) {
98614
99204
  if (/^changelog/i.test(name))
98615
99205
  continue;
98616
- files.add(join6(cwd, name));
99206
+ files.add(join7(cwd, name));
98617
99207
  }
98618
- for (const file of await walkMatching2(join6(cwd, ".metaproject", "wiki"), [".md"]))
99208
+ for (const file of await walkMatching2(join7(cwd, ".metaproject", "wiki"), [".md"]))
98619
99209
  files.add(file);
98620
99210
  for (const glob of includeGlobs) {
98621
99211
  for (const file of await resolveIncludeGlob(cwd, glob))
@@ -98643,11 +99233,11 @@ function changedCommandNamesFromRegions(regions) {
98643
99233
  return names;
98644
99234
  }
98645
99235
  async function fixturePrPort4(dir) {
98646
- const raw = await readFile124(join6(dir, "pr.json"), "utf8");
99236
+ const raw = await readFile124(join7(dir, "pr.json"), "utf8");
98647
99237
  return createFixtureConformPrPort({ pr: JSON.parse(raw) });
98648
99238
  }
98649
99239
  async function fixtureJevFetch4(dir) {
98650
- const raw = await readFile124(join6(dir, "jev-responses.json"), "utf8");
99240
+ const raw = await readFile124(join7(dir, "jev-responses.json"), "utf8");
98651
99241
  const responses = JSON.parse(raw);
98652
99242
  let index = 0;
98653
99243
  const fn = async () => {
@@ -98681,29 +99271,54 @@ async function computeJevDocsResult(options) {
98681
99271
  let outputTokens = 0;
98682
99272
  let costUsd = 0;
98683
99273
  let sawUsage = false;
98684
- for (const batch of batches) {
98685
- const result = await callJevSystemOne(fetchFn, { model, state: batch.state, questions: batch.questions }, { env: process.env });
98686
- jevCalls += 1;
98687
- if (result.usage.input_tokens !== undefined) {
98688
- inputTokens += result.usage.input_tokens;
98689
- sawUsage = true;
98690
- }
98691
- if (result.usage.output_tokens !== undefined) {
98692
- outputTokens += result.usage.output_tokens;
98693
- sawUsage = true;
99274
+ let jevError;
99275
+ const attemptBatch = async (batch) => {
99276
+ try {
99277
+ const result = await callJevSystemOne(fetchFn, { model, state: batch.state, questions: batch.questions }, { env: options.env ?? process.env });
99278
+ jevCalls += 1;
99279
+ if (result.usage.input_tokens !== undefined) {
99280
+ inputTokens += result.usage.input_tokens;
99281
+ sawUsage = true;
99282
+ }
99283
+ if (result.usage.output_tokens !== undefined) {
99284
+ outputTokens += result.usage.output_tokens;
99285
+ sawUsage = true;
99286
+ }
99287
+ if (result.usage.cost !== undefined) {
99288
+ costUsd += result.usage.cost;
99289
+ sawUsage = true;
99290
+ }
99291
+ for (const item of batch.items) {
99292
+ const key = `${item.section.file}::${item.section.line}`;
99293
+ const answer = result.answers[key];
99294
+ if (answer === undefined || answer.type !== "noul")
99295
+ continue;
99296
+ const finding = synthesizeDocsFinding(item, answer.noul, threshold);
99297
+ if (finding !== undefined)
99298
+ jevFindings.push(finding);
99299
+ }
99300
+ return { ok: true };
99301
+ } catch (error) {
99302
+ return { ok: false, error };
98694
99303
  }
98695
- if (result.usage.cost !== undefined) {
98696
- costUsd += result.usage.cost;
98697
- sawUsage = true;
99304
+ };
99305
+ for (const batch of batches) {
99306
+ const attempt = await attemptBatch(batch);
99307
+ if (attempt.ok)
99308
+ continue;
99309
+ if (isMaxTokensExceededError(attempt.error) && batch.items.length > 1) {
99310
+ const mid = Math.ceil(batch.items.length / 2);
99311
+ const halves = [buildDocsBatch(batch.items.slice(0, mid)), buildDocsBatch(batch.items.slice(mid))];
99312
+ for (const half of halves) {
99313
+ const halfAttempt = await attemptBatch(half);
99314
+ if (!halfAttempt.ok && jevError === undefined) {
99315
+ jevError = messageOf(halfAttempt.error);
99316
+ }
99317
+ }
99318
+ continue;
98698
99319
  }
98699
- for (const item of batch.items) {
98700
- const key = `${item.section.file}::${item.section.line}`;
98701
- const answer = result.answers[key];
98702
- if (answer === undefined || answer.type !== "noul")
98703
- continue;
98704
- const finding = synthesizeDocsFinding(item, answer.noul, threshold);
98705
- if (finding !== undefined)
98706
- jevFindings.push(finding);
99320
+ if (jevError === undefined) {
99321
+ jevError = messageOf(attempt.error);
98707
99322
  }
98708
99323
  }
98709
99324
  const findings = [...flagFindings, ...jevFindings].sort((a, b) => a.file.localeCompare(b.file) || a.line - b.line);
@@ -98717,6 +99332,7 @@ async function computeJevDocsResult(options) {
98717
99332
  findings,
98718
99333
  stats,
98719
99334
  tokens: sawUsage ? { jevCalls, inputTokens, outputTokens, costUsd } : { jevCalls },
99335
+ ...jevError !== undefined ? { jevError } : {},
98720
99336
  selection: {
98721
99337
  maxCalls,
98722
99338
  maxPerFile,
@@ -98795,7 +99411,7 @@ async function runJevDocs(args) {
98795
99411
  init_args();
98796
99412
  init_fs();
98797
99413
  import { readFile as readFile126 } from "fs/promises";
98798
- import { join as join7 } from "path";
99414
+ import { join as join8 } from "path";
98799
99415
  init_pr_comments();
98800
99416
 
98801
99417
  // src/review/jev-comments-config.ts
@@ -98838,7 +99454,7 @@ function commentFactLines(facts) {
98838
99454
  ];
98839
99455
  }
98840
99456
  var COMMENT_TOKEN_BUDGET = 64000;
98841
- var QUESTIONS_BUDGET_FRACTION5 = 0.5;
99457
+ var QUESTIONS_BUDGET_FRACTION4 = 0.5;
98842
99458
  var REPLY_BODY_CHARS = 500;
98843
99459
  var HUNK_TEXT_CHARS2 = 1500;
98844
99460
  var COMMENT_BODY_CHARS = 1500;
@@ -98904,7 +99520,7 @@ function batchOpenComments(items) {
98904
99520
  `));
98905
99521
  const questionsTokens = estimateTokens2(attempt.map((f) => `${commentQuestionKey(f)}:choice`).join(`
98906
99522
  `));
98907
- const withinQuestionsShare = questionsTokens <= COMMENT_TOKEN_BUDGET * QUESTIONS_BUDGET_FRACTION5;
99523
+ const withinQuestionsShare = questionsTokens <= COMMENT_TOKEN_BUDGET * QUESTIONS_BUDGET_FRACTION4;
98908
99524
  if (current.length > 0 && (stateTokens + questionsTokens > COMMENT_TOKEN_BUDGET || !withinQuestionsShare)) {
98909
99525
  flush();
98910
99526
  current = [item];
@@ -99089,15 +99705,15 @@ function createFixtureJevCommentsFactsPort(facts) {
99089
99705
  };
99090
99706
  }
99091
99707
  async function fixturePrPort5(dir) {
99092
- const raw = await readFile126(join7(dir, "pr.json"), "utf8");
99708
+ const raw = await readFile126(join8(dir, "pr.json"), "utf8");
99093
99709
  return createFixtureConformPrPort({ pr: JSON.parse(raw) });
99094
99710
  }
99095
99711
  async function fixtureFactsPort(dir) {
99096
- const raw = await readFile126(join7(dir, "git-facts.json"), "utf8").catch(() => "{}");
99712
+ const raw = await readFile126(join8(dir, "git-facts.json"), "utf8").catch(() => "{}");
99097
99713
  return createFixtureJevCommentsFactsPort(JSON.parse(raw));
99098
99714
  }
99099
99715
  async function fixtureJevFetch5(dir) {
99100
- const raw = await readFile126(join7(dir, "jev-responses.json"), "utf8");
99716
+ const raw = await readFile126(join8(dir, "jev-responses.json"), "utf8");
99101
99717
  const responses = JSON.parse(raw);
99102
99718
  let index = 0;
99103
99719
  const fn = async () => {
@@ -99111,7 +99727,7 @@ async function fixtureJevFetch5(dir) {
99111
99727
  return fn;
99112
99728
  }
99113
99729
  function advisoryLabelsPath(cwd, repo, number) {
99114
- 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`);
99115
99731
  }
99116
99732
  async function readCommentAdvisoryLabels(cwd, repo, number) {
99117
99733
  const file = advisoryLabelsPath(cwd, repo, number);
@@ -99651,10 +100267,10 @@ function parseThreshold5(raw) {
99651
100267
  }
99652
100268
  return value;
99653
100269
  }
99654
- function messageOf(error) {
100270
+ function messageOf2(error) {
99655
100271
  return error instanceof Error ? error.message : String(error);
99656
100272
  }
99657
- function isMaxTokensExceededError(error) {
100273
+ function isMaxTokensExceededError2(error) {
99658
100274
  return error instanceof JevRequestError && error.status === 400 && /max_tokens/i.test(error.message);
99659
100275
  }
99660
100276
  async function fixturePrPort6(dir) {
@@ -99725,19 +100341,19 @@ async function computeJevContractResult(options) {
99725
100341
  const attempt = await attemptBatch(batch);
99726
100342
  if (attempt.ok)
99727
100343
  continue;
99728
- if (isMaxTokensExceededError(attempt.error) && batch.items.length > 1) {
100344
+ if (isMaxTokensExceededError2(attempt.error) && batch.items.length > 1) {
99729
100345
  const mid = Math.ceil(batch.items.length / 2);
99730
100346
  const halves = [buildContractClaimBatch(batch.items.slice(0, mid)), buildContractClaimBatch(batch.items.slice(mid))];
99731
100347
  for (const half of halves) {
99732
100348
  const halfAttempt = await attemptBatch(half);
99733
100349
  if (!halfAttempt.ok && jevError === undefined) {
99734
- jevError = messageOf(halfAttempt.error);
100350
+ jevError = messageOf2(halfAttempt.error);
99735
100351
  }
99736
100352
  }
99737
100353
  continue;
99738
100354
  }
99739
100355
  if (jevError === undefined) {
99740
- jevError = messageOf(attempt.error);
100356
+ jevError = messageOf2(attempt.error);
99741
100357
  }
99742
100358
  }
99743
100359
  for (const item of items) {
@@ -100149,10 +100765,10 @@ function parseThreshold6(raw) {
100149
100765
  return value;
100150
100766
  }
100151
100767
  var LIKELY_DUPLICATE_THRESHOLD = 0.5;
100152
- function messageOf2(error) {
100768
+ function messageOf3(error) {
100153
100769
  return error instanceof Error ? error.message : String(error);
100154
100770
  }
100155
- function isMaxTokensExceededError2(error) {
100771
+ function isMaxTokensExceededError3(error) {
100156
100772
  return error instanceof JevRequestError && error.status === 400 && /max_tokens/i.test(error.message);
100157
100773
  }
100158
100774
  async function loadReportRaw(reportPath) {
@@ -100226,19 +100842,19 @@ async function computeJevTriageResult(options) {
100226
100842
  const attempt = await attemptBatch(batch);
100227
100843
  if (attempt.ok)
100228
100844
  continue;
100229
- if (isMaxTokensExceededError2(attempt.error) && batch.items.length > 1) {
100845
+ if (isMaxTokensExceededError3(attempt.error) && batch.items.length > 1) {
100230
100846
  const mid = Math.ceil(batch.items.length / 2);
100231
100847
  const halves = [buildTriageBatch(batch.items.slice(0, mid)), buildTriageBatch(batch.items.slice(mid))];
100232
100848
  for (const half of halves) {
100233
100849
  const halfAttempt = await attemptBatch(half);
100234
100850
  if (!halfAttempt.ok && jevError === undefined) {
100235
- jevError = messageOf2(halfAttempt.error);
100851
+ jevError = messageOf3(halfAttempt.error);
100236
100852
  }
100237
100853
  }
100238
100854
  continue;
100239
100855
  }
100240
100856
  if (jevError === undefined) {
100241
- jevError = messageOf2(attempt.error);
100857
+ jevError = messageOf3(attempt.error);
100242
100858
  }
100243
100859
  }
100244
100860
  }
@@ -101421,7 +102037,7 @@ function applyDeterministicOverride(verdict, signals) {
101421
102037
  // src/review/conform-jev.ts
101422
102038
  init_service4();
101423
102039
  var CONFORM_TOKEN_BUDGET = 64000;
101424
- var QUESTIONS_BUDGET_FRACTION6 = 0.5;
102040
+ var QUESTIONS_BUDGET_FRACTION5 = 0.5;
101425
102041
  function questionFor6(clause) {
101426
102042
  return {
101427
102043
  type: "noul",
@@ -101461,7 +102077,7 @@ function batchConformItems(items, sharedRedactedText) {
101461
102077
  const factsTokens = estimateTokens2(renderFacts(attempt));
101462
102078
  const questionsTokens = estimateQuestionsTokens(attempt);
101463
102079
  const total = sharedTokens + factsTokens + questionsTokens;
101464
- const withinQuestionsShare = questionsTokens <= CONFORM_TOKEN_BUDGET * QUESTIONS_BUDGET_FRACTION6;
102080
+ const withinQuestionsShare = questionsTokens <= CONFORM_TOKEN_BUDGET * QUESTIONS_BUDGET_FRACTION5;
101465
102081
  if (current.length > 0 && (total > CONFORM_TOKEN_BUDGET || !withinQuestionsShare)) {
101466
102082
  flush();
101467
102083
  current = [item];
@@ -102505,7 +103121,7 @@ async function resolvePort(args) {
102505
103121
  }
102506
103122
  const files = {};
102507
103123
  for (const key of ["pull", "pull-comments", "pull-reviews", "issue-comments"]) {
102508
- const file = join8(fixtures, `${key}.json`);
103124
+ const file = join9(fixtures, `${key}.json`);
102509
103125
  try {
102510
103126
  files[key] = JSON.parse(await readFile133(file, "utf8"));
102511
103127
  } catch {
@@ -102515,13 +103131,13 @@ async function resolvePort(args) {
102515
103131
  return createFixturePort(files);
102516
103132
  }
102517
103133
  async function fixtureCiPort(dir) {
102518
- const runInfo = JSON.parse(await readFile133(join8(dir, "ci-run-info.json"), "utf8"));
102519
- const log = await readFile133(join8(dir, "ci-failed-log.txt"), "utf8").catch(() => "");
102520
- const history = await readFile133(join8(dir, "ci-history.json"), "utf8").then((raw) => JSON.parse(raw)).catch(() => ({}));
102521
- const attempts = await readFile133(join8(dir, "ci-attempts.json"), "utf8").then((raw) => JSON.parse(raw)).catch(() => ({}));
102522
- const changedFiles = await readFile133(join8(dir, "ci-changed-files.json"), "utf8").then((raw) => JSON.parse(raw)).catch(() => ({}));
102523
- const runsByHeadSha = await readFile133(join8(dir, "ci-runs-by-head-sha.json"), "utf8").then((raw) => JSON.parse(raw)).catch(() => ({}));
102524
- 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(() => ({}));
102525
103141
  return createFixtureCiPort({
102526
103142
  runs: { [runInfo.runId]: runInfo, ...related.runs },
102527
103143
  logs: { [runInfo.runId]: log, ...related.logs },
@@ -102532,7 +103148,7 @@ async function fixtureCiPort(dir) {
102532
103148
  });
102533
103149
  }
102534
103150
  async function fixtureJevFetch8(dir) {
102535
- const body = await readFile133(join8(dir, "jev-response.json"), "utf8");
103151
+ const body = await readFile133(join9(dir, "jev-response.json"), "utf8");
102536
103152
  const fn = async () => new Response(body, { status: 200, headers: { "content-type": "application/json" } });
102537
103153
  return fn;
102538
103154
  }
@@ -102698,12 +103314,12 @@ async function runCiTriageEval(cwd, args, evalFile) {
102698
103314
  }
102699
103315
  }
102700
103316
  async function fixtureConformJevFetch(dir) {
102701
- const body = await readFile133(join8(dir, "jev-response.json"), "utf8");
103317
+ const body = await readFile133(join9(dir, "jev-response.json"), "utf8");
102702
103318
  const fn = async () => new Response(body, { status: 200, headers: { "content-type": "application/json" } });
102703
103319
  return fn;
102704
103320
  }
102705
103321
  async function fixtureConformPrPortFrom(dir) {
102706
- const pr = JSON.parse(await readFile133(join8(dir, "pr.json"), "utf8"));
103322
+ const pr = JSON.parse(await readFile133(join9(dir, "pr.json"), "utf8"));
102707
103323
  return createFixtureConformPrPort({ pr });
102708
103324
  }
102709
103325
  function accumulateJevUsage(acc, usage) {
@@ -102784,7 +103400,7 @@ async function explainConformVerdicts(cwd, refPath, verdicts, threshold, fixture
102784
103400
  return {};
102785
103401
  if (fixturesDir !== undefined) {
102786
103402
  try {
102787
- return JSON.parse(await readFile133(join8(fixturesDir, "explain-response.json"), "utf8"));
103403
+ return JSON.parse(await readFile133(join9(fixturesDir, "explain-response.json"), "utf8"));
102788
103404
  } catch {
102789
103405
  return {};
102790
103406
  }
@@ -102832,7 +103448,7 @@ function parsePositiveIntFlag(args, flag, fallback) {
102832
103448
  }
102833
103449
  async function readReportFindings(reportDir) {
102834
103450
  try {
102835
- const raw = JSON.parse(await readFile133(join8(reportDir, "findings.json"), "utf8"));
103451
+ const raw = JSON.parse(await readFile133(join9(reportDir, "findings.json"), "utf8"));
102836
103452
  return Array.isArray(raw) ? raw : [];
102837
103453
  } catch {
102838
103454
  return [];
@@ -102904,7 +103520,7 @@ async function runConform(args) {
102904
103520
  const fetchFn = fixturesDir === undefined ? globalThis.fetch : await fixtureConformJevFetch(fixturesDir);
102905
103521
  const acc = { jevCalls: 0, inputTokens: 0, outputTokens: 0, costUsd: 0, sawUsage: false };
102906
103522
  const clauses = await resolveConformClauseTags(cwd, refPath, docText, rawClauses, fetchFn, model, acc);
102907
- 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)}
102908
103524
  `, { mode: 384 }).catch(() => {});
102909
103525
  const notCheckable = clauses.filter((c) => !c.checkable);
102910
103526
  const checkable = clauses.filter((c) => c.checkable);
@@ -102936,7 +103552,7 @@ async function runConform(args) {
102936
103552
  hunkTotalRegions = hunkResult.totalRegions;
102937
103553
  } else if (reportDir !== undefined) {
102938
103554
  target = { kind: "report", label: reportDir };
102939
- const reportMarkdown = await readFile133(join8(reportDir, "report.md"), "utf8").catch(() => "");
103555
+ const reportMarkdown = await readFile133(join9(reportDir, "report.md"), "utf8").catch(() => "");
102940
103556
  const findings = await readReportFindings(reportDir);
102941
103557
  const facts = computeReportConformFacts(reportMarkdown, findings);
102942
103558
  const redacted = reportRedactedStateText(reportMarkdown);
@@ -102977,7 +103593,7 @@ async function runConform(args) {
102977
103593
  }
102978
103594
  async function readRecentConformDocs(cwd) {
102979
103595
  try {
102980
- 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"));
102981
103597
  return Array.isArray(raw) ? raw : [];
102982
103598
  } catch {
102983
103599
  return [];
@@ -103223,7 +103839,7 @@ async function runStack(args) {
103223
103839
  console.log(renderStackScopingMarkdown(detected, decisions));
103224
103840
  }
103225
103841
  async function stackScopingForInstalledReviewers(cwd, detected) {
103226
- const reviewRoot = join8(cwd, ".metaproject", "skills", "gdskills", "review");
103842
+ const reviewRoot = join9(cwd, ".metaproject", "skills", "gdskills", "review");
103227
103843
  let entries;
103228
103844
  try {
103229
103845
  entries = await readdir47(reviewRoot, { withFileTypes: true });
@@ -103235,7 +103851,7 @@ async function stackScopingForInstalledReviewers(cwd, detected) {
103235
103851
  if (!entry.isDirectory()) {
103236
103852
  continue;
103237
103853
  }
103238
- const skillMdPath = join8(reviewRoot, entry.name, "SKILL.md");
103854
+ const skillMdPath = join9(reviewRoot, entry.name, "SKILL.md");
103239
103855
  let content;
103240
103856
  try {
103241
103857
  content = await readFile133(skillMdPath, "utf8");
@@ -110205,7 +110821,7 @@ async function diagnose(server, options) {
110205
110821
  skipped: catalog.skipped
110206
110822
  };
110207
110823
  } catch (error) {
110208
- return { ...base, status: "failed", toolCount: 0, skipped: [], detail: messageOf3(error) };
110824
+ return { ...base, status: "failed", toolCount: 0, skipped: [], detail: messageOf4(error) };
110209
110825
  } finally {
110210
110826
  try {
110211
110827
  await connection.close();
@@ -110227,7 +110843,7 @@ function oauthErrorCodeOf(error) {
110227
110843
  return typeof code === "string" ? code : "";
110228
110844
  }
110229
110845
  function explainConnectFailure(transport, server, error) {
110230
- const message = sanitiseForDisplay(messageOf3(error));
110846
+ const message = sanitiseForDisplay(messageOf4(error));
110231
110847
  if (transport !== "http")
110232
110848
  return message;
110233
110849
  const url = displayUrl(server.raw.url);
@@ -110300,7 +110916,7 @@ function syscallCodeOf(error) {
110300
110916
  }
110301
110917
  return;
110302
110918
  }
110303
- function messageOf3(error) {
110919
+ function messageOf4(error) {
110304
110920
  return error instanceof Error ? error.message : String(error);
110305
110921
  }
110306
110922
  async function withTimeout(work, ms, name) {
@@ -110350,38 +110966,8 @@ function formatDoctorReport(report) {
110350
110966
  `);
110351
110967
  }
110352
110968
 
110353
- // src/lib/oauth/open-url.ts
110354
- import { spawn as spawn7 } from "child_process";
110355
- function browserOpenPlan(url, platform = process.platform, env = process.env) {
110356
- if (url.length === 0) {
110357
- return;
110358
- }
110359
- if (platform === "darwin") {
110360
- return { cmd: "open", args: [url] };
110361
- }
110362
- if (platform === "win32") {
110363
- return { cmd: "cmd", args: ["/c", "start", "", url] };
110364
- }
110365
- if (typeof env.DISPLAY === "string" && env.DISPLAY.length > 0) {
110366
- return { cmd: "xdg-open", args: [url] };
110367
- }
110368
- if (typeof env.WAYLAND_DISPLAY === "string" && env.WAYLAND_DISPLAY.length > 0) {
110369
- return { cmd: "xdg-open", args: [url] };
110370
- }
110371
- return;
110372
- }
110373
- function openVerificationUrl(url, deps = {}) {
110374
- const plan = browserOpenPlan(url, deps.platform ?? process.platform, deps.env ?? process.env);
110375
- if (plan === undefined) {
110376
- return;
110377
- }
110378
- const spawnFn = deps.spawn ?? spawn7;
110379
- try {
110380
- const child = spawnFn(plan.cmd, plan.args, { stdio: "ignore", detached: true });
110381
- child.on("error", () => {});
110382
- child.unref();
110383
- } catch {}
110384
- }
110969
+ // src/commands/mcp-servers.ts
110970
+ init_open_url();
110385
110971
 
110386
110972
  // src/mcp-servers/oauth-callback.ts
110387
110973
  import { randomBytes as randomBytes8, timingSafeEqual as timingSafeEqual2 } from "crypto";
@@ -126604,7 +127190,7 @@ init_safe_exec();
126604
127190
  init_metaproject_operations();
126605
127191
  init_interactive_tools();
126606
127192
  import { existsSync as existsSync43 } from "fs";
126607
- import { resolve as resolve3 } from "path";
127193
+ import { resolve as resolve4 } from "path";
126608
127194
  var MAX_OUTPUT_BYTES3 = 20000;
126609
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;
126610
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.";
@@ -126615,11 +127201,11 @@ function normalizeSearchResult(result) {
126615
127201
  return result;
126616
127202
  }
126617
127203
  var KERYX_ENTRIES = [
126618
- resolve3(import.meta.dir, "..", "..", "..", "cli.ts"),
126619
- resolve3(import.meta.dir, "cli.js")
127204
+ resolve4(import.meta.dir, "..", "..", "..", "cli.ts"),
127205
+ resolve4(import.meta.dir, "cli.js")
126620
127206
  ];
126621
127207
  function keryxSelfCommand(main = Bun.main, execPath = process.execPath, exists = existsSync43, entries = KERYX_ENTRIES) {
126622
- if (entries.includes(resolve3(main)) && exists(main)) {
127208
+ if (entries.includes(resolve4(main)) && exists(main)) {
126623
127209
  return [execPath, ...SAFE_BUN_SPAWN_ARGS, main];
126624
127210
  }
126625
127211
  if (isCompiledBinaryEntry(main)) {
@@ -132751,6 +133337,7 @@ ${lines.join(`
132751
133337
  }
132752
133338
 
132753
133339
  // src/tui/tui-shell.ts
133340
+ init_subscription_models();
132754
133341
  init_agent();
132755
133342
  init_single_turn();
132756
133343
 
@@ -146411,6 +146998,7 @@ init_patch_risk();
146411
146998
  init_catalog();
146412
146999
  init_grants();
146413
147000
  init_login();
147001
+ init_open_url();
146414
147002
  init_shell_config();
146415
147003
  init_provider_config();
146416
147004
 
@@ -151780,14 +152368,22 @@ async function filterConnectedDetectedProviders(detected, options = {}) {
151780
152368
  const env = options.env ?? process.env;
151781
152369
  const connected = [];
151782
152370
  for (const prov of detected) {
151783
- 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);
151784
152380
  if (registry === undefined) {
151785
152381
  connected.push(prov);
151786
152382
  continue;
151787
152383
  }
151788
152384
  const requiresApiKey = registry.requiresApiKey ?? true;
151789
152385
  const envKey = prov.envKey ?? registry.envKey;
151790
- const raw = envKey !== undefined ? env[envKey] : undefined;
152386
+ const raw = envKey !== undefined ? env[envKey] ?? loadShellConfig(options.configDir).apiKeys?.[envKey] : undefined;
151791
152387
  if (requiresApiKey && (raw === undefined || raw.length === 0)) {
151792
152388
  continue;
151793
152389
  }
@@ -152872,7 +153468,7 @@ function pickAuthMethodStep(otui, target, providerLabel, methods) {
152872
153468
  });
152873
153469
  });
152874
153470
  }
152875
- function runDeviceLoginInTui(otui, target, provider, dir) {
153471
+ function runDeviceLoginInTui(otui, target, provider, dir, fetchFn = globalThis.fetch) {
152876
153472
  const r = stepRenderer(target);
152877
153473
  return new Promise((resolve) => {
152878
153474
  const controller = new AbortController;
@@ -152895,7 +153491,7 @@ function runDeviceLoginInTui(otui, target, provider, dir) {
152895
153491
  };
152896
153492
  loginDeviceCode({
152897
153493
  provider,
152898
- fetch: (input, init) => globalThis.fetch(input, init),
153494
+ fetch: (input, init) => fetchFn(input, init),
152899
153495
  signal: controller.signal,
152900
153496
  ...dir !== undefined ? { dir } : {},
152901
153497
  onChallenge: (challenge) => {
@@ -152908,7 +153504,7 @@ function runDeviceLoginInTui(otui, target, provider, dir) {
152908
153504
  }
152909
153505
  if (result.ok) {
152910
153506
  cleanup();
152911
- applyOAuthAccessToEnv();
153507
+ applyOAuthAccessToEnv(dir);
152912
153508
  resolve(true);
152913
153509
  return;
152914
153510
  }
@@ -152923,7 +153519,7 @@ function runDeviceLoginInTui(otui, target, provider, dir) {
152923
153519
  });
152924
153520
  }
152925
153521
  async function modelsForPicker(prov, deps = {}) {
152926
- 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 } : {} });
152927
153523
  }
152928
153524
  function endpointMayBeAtFault(failure) {
152929
153525
  return failure?.kind === "unreachable" || failure?.kind === "http";
@@ -153067,7 +153663,7 @@ function pickConnectedProviderStep(otui, target, detected, opts) {
153067
153663
  }
153068
153664
  async function runTest(name) {
153069
153665
  const block = rowBlocks.find((b) => b.name === name);
153070
- const provider = providerByName(name, opts.configDir);
153666
+ const provider = connectionProviderByName(name, opts.configDir);
153071
153667
  if (block === undefined)
153072
153668
  return;
153073
153669
  if (provider === undefined) {
@@ -153231,7 +153827,8 @@ function selectProviderModelInTui(otui, rOrChrome, detected, options = {}) {
153231
153827
  }
153232
153828
  const candidatesPromise = options.onlyConnected ? filterConnectedDetectedProviders(detected, {
153233
153829
  ...options.fetch !== undefined ? { fetch: options.fetch } : {},
153234
- ...options.env !== undefined ? { env: options.env } : {}
153830
+ ...options.env !== undefined ? { env: options.env } : {},
153831
+ ...options.configDir !== undefined ? { configDir: options.configDir } : {}
153235
153832
  }) : Promise.resolve(detected);
153236
153833
  (async () => {
153237
153834
  const candidates = await candidatesPromise;
@@ -153275,7 +153872,7 @@ function selectProviderModelInTui(otui, rOrChrome, detected, options = {}) {
153275
153872
  }
153276
153873
  continue;
153277
153874
  }
153278
- const lockDiscoveredHost = prov.name === "github-copilot";
153875
+ const lockDiscoveredHost = ["github-copilot", "openai", "openai-codex"].includes(prov.name);
153279
153876
  let selectedBaseUrl = options.onlyConnected || prov.baseUrl === undefined || lockDiscoveredHost ? prov.baseUrl : await promptBaseUrlStep(otui, rOrChrome, prov.label ?? prov.name, prov.baseUrl);
153280
153877
  if (!options.onlyConnected && !lockDiscoveredHost && prov.baseUrl !== undefined && selectedBaseUrl === undefined) {
153281
153878
  continue;
@@ -153287,12 +153884,12 @@ function selectProviderModelInTui(otui, rOrChrome, detected, options = {}) {
153287
153884
  const envKey = prov.envKey;
153288
153885
  const label = prov.label ?? prov.name;
153289
153886
  const collectCredential = async (notice) => {
153290
- if (options.onlyConnected || envKey === undefined) {
153887
+ if (options.onlyConnected || envKey === undefined && !catalogAllows(prov.name, "device-code")) {
153291
153888
  return "ok";
153292
153889
  }
153293
153890
  if (notice === undefined) {
153294
- const existingKey = (options.env ?? process.env)[envKey];
153295
- 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;
153296
153893
  if (existingKey !== undefined && existingKey.length > 0 || hasOauth) {
153297
153894
  return "ok";
153298
153895
  }
@@ -153310,7 +153907,7 @@ function selectProviderModelInTui(otui, rOrChrome, detected, options = {}) {
153310
153907
  }
153311
153908
  }
153312
153909
  if (method === "device-code") {
153313
- if (!await runDeviceLoginInTui(otui, rOrChrome, prov.name, options.configDir)) {
153910
+ if (!await runDeviceLoginInTui(otui, rOrChrome, prov.name, options.configDir, options.fetch)) {
153314
153911
  return "back";
153315
153912
  }
153316
153913
  const saved = loadShellConfig(options.configDir).baseUrls?.[prov.name];
@@ -153320,8 +153917,10 @@ function selectProviderModelInTui(otui, rOrChrome, detected, options = {}) {
153320
153917
  }
153321
153918
  return "ok";
153322
153919
  }
153920
+ if (envKey === undefined)
153921
+ return "back";
153323
153922
  const kr = await promptApiKeyStep(otui, rOrChrome, {
153324
- label,
153923
+ label: prov.name === "openai" ? "OpenAI" : label,
153325
153924
  envKey,
153326
153925
  ...notice === undefined ? {} : { notice }
153327
153926
  });
@@ -153340,7 +153939,8 @@ function selectProviderModelInTui(otui, rOrChrome, detected, options = {}) {
153340
153939
  }
153341
153940
  const modelDeps = {
153342
153941
  ...options.fetch === undefined ? {} : { fetch: options.fetch },
153343
- ...options.env === undefined ? {} : { env: options.env }
153942
+ ...options.env === undefined ? {} : { env: options.env },
153943
+ ...options.configDir === undefined ? {} : { configDir: options.configDir }
153344
153944
  };
153345
153945
  let models = await modelsForPicker(selectedProvider, modelDeps);
153346
153946
  if (models.failure?.kind === "rejected") {
@@ -153349,7 +153949,7 @@ function selectProviderModelInTui(otui, rOrChrome, detected, options = {}) {
153349
153949
  }
153350
153950
  models = await modelsForPicker(selectedProvider, modelDeps);
153351
153951
  }
153352
- if (!options.onlyConnected && selectedProvider.baseUrl !== undefined && endpointMayBeAtFault(models.failure)) {
153952
+ if (!options.onlyConnected && !lockDiscoveredHost && selectedProvider.baseUrl !== undefined && endpointMayBeAtFault(models.failure)) {
153353
153953
  const corrected = await promptBaseUrlStep(otui, rOrChrome, label, selectedProvider.baseUrl, modelsFailureLine(label, models.failure));
153354
153954
  if (corrected === undefined) {
153355
153955
  continue;
@@ -156474,7 +157074,7 @@ Usage: /reasoning <${REASONING_EFFORT_LEVELS.join("|")}>
156474
157074
  io.onSystem?.(`Reasoning effort: ${wanted}
156475
157075
  `);
156476
157076
  const compatProvider = providerByName(currentSel.provider);
156477
- if (compatProvider !== undefined && compatProvider.reasoning === undefined) {
157077
+ if (currentSel.provider !== "openai" && compatProvider !== undefined && compatProvider.reasoning === undefined) {
156478
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.
156479
157079
  `);
156480
157080
  }
@@ -156537,7 +157137,8 @@ Usage: /reasoning <${REASONING_EFFORT_LEVELS.join("|")}>
156537
157137
  `);
156538
157138
  }
156539
157139
  if (name === currentSel.provider) {
156540
- 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.
156541
157142
  `);
156542
157143
  }
156543
157144
  }
@@ -157880,6 +158481,17 @@ Starting a new session.
157880
158481
  modelParams = picked.modelParams ?? {};
157881
158482
  provider = makeActive();
157882
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
+ };
157883
158495
  for await (const line of io.lines) {
157884
158496
  io.onSafeBoundary?.();
157885
158497
  if (line.startsWith("/")) {
@@ -158017,12 +158629,19 @@ Kept the current session.
158017
158629
  `);
158018
158630
  continue;
158019
158631
  }
158020
- const picked = await deps.selectProviderModel(io, { onlyProvider: providerName });
158021
- applySelection(picked);
158632
+ await selectAndApply({ onlyProvider: providerName });
158022
158633
  continue;
158023
158634
  }
158024
158635
  if (command === "/provider") {
158025
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
+ }
158026
158645
  providerName = argument;
158027
158646
  provider = makeActive();
158028
158647
  continue;
@@ -158032,8 +158651,7 @@ Kept the current session.
158032
158651
  `);
158033
158652
  continue;
158034
158653
  }
158035
- const picked = await deps.selectProviderModel(io, {});
158036
- applySelection(picked);
158654
+ await selectAndApply({});
158037
158655
  continue;
158038
158656
  }
158039
158657
  if (command === "/connect") {
@@ -158041,8 +158659,7 @@ Kept the current session.
158041
158659
  system(CONNECT_GUIDANCE);
158042
158660
  continue;
158043
158661
  }
158044
- const picked = await deps.selectProviderModel(io, { onlyConnected: true });
158045
- applySelection(picked);
158662
+ await selectAndApply({ onlyConnected: true });
158046
158663
  continue;
158047
158664
  }
158048
158665
  const wrongMode = describeUnavailableCommand(command, "chat");
@@ -158150,6 +158767,8 @@ function realMakeProvider(write) {
158150
158767
  };
158151
158768
  }
158152
158769
  function oauthCredentialsFor(name) {
158770
+ if (name === "openai" || name === "openai-codex")
158771
+ return {};
158153
158772
  const definition = providerByName(name);
158154
158773
  const envKey = definition?.envKey;
158155
158774
  if (envKey === undefined)
@@ -169437,6 +170056,7 @@ async function routingCommand(args, deps = {}) {
169437
170056
  init_catalog();
169438
170057
  init_grants();
169439
170058
  init_login();
170059
+ init_open_url();
169440
170060
  function printAuthHelp() {
169441
170061
  console.log(`Usage:
169442
170062
  keryx auth list [--json]
@@ -169444,7 +170064,7 @@ function printAuthHelp() {
169444
170064
  keryx auth logout <provider>
169445
170065
  keryx auth status <provider> [--json]
169446
170066
 
169447
- 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.
169448
170068
  Claude Pro/Max, Gemini Google-account login, and DeepSeek OAuth are not offered.
169449
170069
  `);
169450
170070
  }
@@ -169500,7 +170120,9 @@ function runAuthList(args) {
169500
170120
  }
169501
170121
  }
169502
170122
  async function runAuthLogin(args) {
169503
- 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.");
169504
170126
  if (provider === undefined || provider.length === 0) {
169505
170127
  console.error("Usage: keryx auth login <provider>");
169506
170128
  process.exitCode = 1;
@@ -169512,14 +170134,18 @@ async function runAuthLogin(args) {
169512
170134
  process.exitCode = 1;
169513
170135
  return;
169514
170136
  }
170137
+ const controller = new AbortController;
170138
+ const cancel = () => controller.abort();
170139
+ process.on("SIGINT", cancel);
169515
170140
  const result = await loginDeviceCode({
169516
170141
  provider,
170142
+ signal: controller.signal,
169517
170143
  fetch: (input, init) => globalThis.fetch(input, init),
169518
170144
  onChallenge: (challenge) => {
169519
170145
  console.log(challenge.instructions);
169520
170146
  openVerificationUrl(challenge.verificationUriComplete ?? challenge.verificationUri);
169521
170147
  }
169522
- });
170148
+ }).finally(() => process.removeListener("SIGINT", cancel));
169523
170149
  if (!result.ok) {
169524
170150
  console.error(result.error);
169525
170151
  process.exitCode = 1;
@@ -169529,7 +170155,9 @@ async function runAuthLogin(args) {
169529
170155
  console.log(`Authorized ${provider} via ${deviceCodeMethodLabel(provider)}.`);
169530
170156
  }
169531
170157
  function runAuthLogout(args) {
169532
- 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.");
169533
170161
  if (provider === undefined || provider.length === 0) {
169534
170162
  console.error("Usage: keryx auth logout <provider>");
169535
170163
  process.exitCode = 1;
@@ -169539,7 +170167,9 @@ function runAuthLogout(args) {
169539
170167
  console.log(`Logged out ${provider}.`);
169540
170168
  }
169541
170169
  function runAuthStatus(args) {
169542
- 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.");
169543
170173
  if (provider === undefined || provider.length === 0) {
169544
170174
  console.error("Usage: keryx auth status <provider>");
169545
170175
  process.exitCode = 1;