@bitkyc08/opencodex 2.52.0-preview.20260911 → 2.52.0

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 (166) hide show
  1. package/gui/dist/assets/index-CWXut3rG.js +115 -0
  2. package/gui/dist/assets/index-EdoPnm9_.css +1 -0
  3. package/gui/dist/index.html +2 -2
  4. package/gui/dist/provider-icons/devin.svg +49 -0
  5. package/gui/dist/provider-icons/omo.svg +42 -0
  6. package/package.json +3 -1
  7. package/src/AGENTS.md +1 -1
  8. package/src/adapters/cline-pass-deepseek-v4-tool-replay.ts +0 -1
  9. package/src/adapters/command-code.ts +0 -1
  10. package/src/adapters/cursor/checkpoint-store.ts +37 -0
  11. package/src/adapters/cursor/live-transport.ts +74 -2
  12. package/src/adapters/cursor.ts +6 -0
  13. package/src/adapters/devin/cloud-direct/auth.ts +264 -0
  14. package/src/adapters/devin/cloud-direct/catalog.ts +306 -0
  15. package/src/adapters/devin/cloud-direct/chat.ts +1274 -0
  16. package/src/adapters/devin/cloud-direct/index.ts +65 -0
  17. package/src/adapters/devin/cloud-direct/metadata.ts +134 -0
  18. package/src/adapters/devin/cloud-direct/wire.ts +206 -0
  19. package/src/adapters/devin/live-models.ts +133 -0
  20. package/src/adapters/devin-cli/acp.ts +204 -0
  21. package/src/adapters/devin-cli/adapter.ts +345 -0
  22. package/src/adapters/devin-cli/binary.ts +69 -0
  23. package/src/adapters/devin-cli/models.ts +57 -0
  24. package/src/adapters/devin.ts +326 -0
  25. package/src/adapters/google.ts +1 -1
  26. package/src/adapters/openai-chat.ts +31 -10
  27. package/src/adapters/openai-responses.ts +16 -1
  28. package/src/adapters/registry.ts +25 -1
  29. package/src/bridge.ts +8 -2
  30. package/src/claude/inbound-cache-stabilize.ts +130 -0
  31. package/src/claude/inbound.ts +45 -5
  32. package/src/cli/account-auth.ts +60 -1
  33. package/src/cli/account-extended.ts +23 -30
  34. package/src/cli/account.ts +2 -1
  35. package/src/cli/capabilities.ts +50 -8
  36. package/src/cli/config-command.ts +2 -2
  37. package/src/cli/dispatch.ts +14 -2
  38. package/src/cli/export-command.ts +11 -25
  39. package/src/cli/help.ts +2 -2
  40. package/src/cli/opencode.ts +5 -0
  41. package/src/cli/registry.ts +15 -4
  42. package/src/clients/config-export/cline.ts +71 -0
  43. package/src/clients/config-export/contracts.ts +10 -1
  44. package/src/clients/config-export/model-metadata.ts +33 -0
  45. package/src/clients/config-export/zcode.ts +23 -12
  46. package/src/clients/config-export.ts +156 -4
  47. package/src/codex/account-store.ts +7 -2
  48. package/src/codex/auth-context.ts +1 -1
  49. package/src/codex/catalog/effort.ts +4 -3
  50. package/src/codex/catalog/metadata.ts +8 -4
  51. package/src/codex/catalog/native-models.ts +24 -4
  52. package/src/codex/catalog/parsing.ts +19 -1
  53. package/src/codex/catalog/provider-fetch.ts +57 -0
  54. package/src/codex/catalog/sync.ts +1 -1
  55. package/src/codex/context-compat.ts +97 -0
  56. package/src/codex/context-owner.ts +201 -0
  57. package/src/codex/history-provider.ts +161 -14
  58. package/src/codex/inject-coordination.ts +3 -2
  59. package/src/codex/inject.ts +128 -9
  60. package/src/codex/pool-rotation.ts +8 -292
  61. package/src/codex/quota.ts +41 -12
  62. package/src/codex/retired-model-migration.ts +41 -0
  63. package/src/codex/routing.ts +327 -22
  64. package/src/codex/warmup.ts +2 -2
  65. package/src/combos/failover.ts +5 -0
  66. package/src/combos/resolve.ts +11 -15
  67. package/src/config.ts +46 -14
  68. package/src/generated/compatibility-version.json +293 -113
  69. package/src/grok/grpc-web.ts +120 -0
  70. package/src/grok/reset-coupon-ledger.ts +139 -0
  71. package/src/grok/reset-coupons.ts +278 -0
  72. package/src/integrations/catalog-refresh.ts +1 -1
  73. package/src/integrations/cline-document.ts +73 -0
  74. package/src/integrations/cline-io.ts +149 -0
  75. package/src/integrations/cline-transaction.ts +42 -0
  76. package/src/integrations/config-io.ts +21 -2
  77. package/src/integrations/journal.ts +43 -1
  78. package/src/integrations/omp-yaml-source.ts +123 -4
  79. package/src/integrations/ownership-policy.ts +7 -0
  80. package/src/integrations/ownership.ts +36 -0
  81. package/src/integrations/registry.ts +27 -0
  82. package/src/integrations/state.ts +5 -2
  83. package/src/integrations/store.ts +6 -0
  84. package/src/integrations/writer.ts +53 -9
  85. package/src/lab/subject/behavior-fingerprint.ts +1 -1
  86. package/src/lib/abort.ts +36 -0
  87. package/src/lib/local-destinations.ts +1 -1
  88. package/src/lib/upstream-retry.ts +36 -1
  89. package/src/oauth/account-quota-rank.ts +11 -0
  90. package/src/oauth/callback-server.ts +10 -5
  91. package/src/oauth/devin/api-base.ts +63 -0
  92. package/src/oauth/devin/login.ts +1 -0
  93. package/src/oauth/devin/register-user.ts +186 -0
  94. package/src/oauth/devin/types.ts +71 -0
  95. package/src/oauth/devin-cli.ts +149 -0
  96. package/src/oauth/devin.ts +170 -0
  97. package/src/oauth/generic-account-failover.ts +169 -1
  98. package/src/oauth/index.ts +20 -1
  99. package/src/oauth/login-cli.ts +19 -5
  100. package/src/oauth/pool-kernel.ts +321 -0
  101. package/src/oauth/pool-settings-capability.ts +127 -9
  102. package/src/oauth/store.ts +7 -3
  103. package/src/oauth/token-guardian.ts +1 -1
  104. package/src/providers/codebuddy-models.ts +0 -3
  105. package/src/providers/command-code-efforts.ts +23 -4
  106. package/src/providers/default-aliases.ts +4 -0
  107. package/src/providers/derive.ts +8 -0
  108. package/src/providers/devin-cli-authmode-migration.ts +57 -0
  109. package/src/providers/key-failover.ts +175 -0
  110. package/src/providers/model-rename-startup.ts +21 -1
  111. package/src/providers/qoder-models.ts +0 -1
  112. package/src/providers/quota-key-accounts.ts +50 -0
  113. package/src/providers/quota-routing-cache.ts +65 -6
  114. package/src/providers/quota-types.ts +7 -0
  115. package/src/providers/quota.ts +113 -30
  116. package/src/providers/registry.ts +237 -77
  117. package/src/providers/stale-context-window-migration.ts +92 -0
  118. package/src/providers/zai-responses-migration.ts +45 -0
  119. package/src/quota/reset-observer.ts +2 -1
  120. package/src/quota/reset-seen-store.ts +9 -1
  121. package/src/remote-control/crypto.ts +442 -0
  122. package/src/remote-control/host.ts +175 -0
  123. package/src/remote-control/index.ts +100 -0
  124. package/src/remote-control/protocol.ts +200 -0
  125. package/src/remote-control/relay.ts +162 -0
  126. package/src/remote-control/workspace-agent-protocol.ts +246 -0
  127. package/src/remote-control/workspace-rpc-framing.ts +128 -0
  128. package/src/remote-control/workspace-tools.ts +237 -0
  129. package/src/remote-control/workspace-utf8.ts +24 -0
  130. package/src/responses/custom-tool-compat.ts +23 -0
  131. package/src/router.ts +6 -1
  132. package/src/routing/compatibility/behavior.ts +3 -0
  133. package/src/server/adapter-resolve.ts +6 -2
  134. package/src/server/auth-cors.ts +71 -6
  135. package/src/server/chat-completions.ts +5 -3
  136. package/src/server/chat-native.ts +9 -0
  137. package/src/server/claude-messages.ts +25 -30
  138. package/src/server/context-history.ts +207 -0
  139. package/src/server/images.ts +28 -1
  140. package/src/server/index.ts +42 -22
  141. package/src/server/live.ts +2 -1
  142. package/src/server/management/agent-settings-routes.ts +7 -1
  143. package/src/server/management/config-routes.ts +3 -3
  144. package/src/server/management/grok-coupon-routes.ts +287 -0
  145. package/src/server/management/integration-routes.ts +6 -1
  146. package/src/server/management/oauth-account-routes.ts +124 -7
  147. package/src/server/management/provider-routes.ts +77 -2
  148. package/src/server/management/route-registry.ts +6 -1
  149. package/src/server/management-api.ts +7 -0
  150. package/src/server/request-log.ts +4 -0
  151. package/src/server/responses/codex-ws-exchange.ts +90 -14
  152. package/src/server/responses/codex-ws-wire.ts +51 -1
  153. package/src/server/responses/compact.ts +8 -1
  154. package/src/server/responses/core.ts +188 -22
  155. package/src/server/responses/ws-upstream.ts +1 -1
  156. package/src/server/responses-undeclared-tool-guard.ts +261 -12
  157. package/src/server/zai-responses-startup.ts +21 -0
  158. package/src/types/config.ts +33 -3
  159. package/src/types/provider.ts +54 -4
  160. package/src/types/request.ts +1 -1
  161. package/src/types/tools.ts +36 -6
  162. package/src/update/job.ts +33 -11
  163. package/src/vision/plan.ts +40 -37
  164. package/src/web-search/index.ts +1 -0
  165. package/gui/dist/assets/index-BoBRSehJ.css +0 -1
  166. package/gui/dist/assets/index-Dx0xv2EA.js +0 -115
@@ -24,6 +24,7 @@ import { grokDefaultReasoningEffort } from "../grok/effort";
24
24
  import { flushConfigDirHardening } from "../config/paths";
25
25
  import { migrateStartupSubagentModels } from "./subagent-models-startup";
26
26
  import { migrateStartupXaiResponses } from "./xai-responses-startup";
27
+ import { migrateStartupZaiResponses } from "./zai-responses-startup";
27
28
  import { reconcileOAuthProviders } from "../oauth";
28
29
  import { withCatalogWriteSerialization } from "../codex/catalog-write-serialization";
29
30
  import { invalidateCodexModelsCacheWithPermit } from "../codex/catalog/sync";
@@ -192,6 +193,7 @@ import { anthropicErrorResponse } from "../claude/outbound";
192
193
  import { buildDesktop3pRegistry, generateDesktop3pModels } from "../claude/desktop-3p";
193
194
  import { buildDesktopDiscoveryInputs } from "../claude/desktop-discovery-inputs";
194
195
  import { runClaudeAuthModeMigration } from "../claude/auth-mode-migration";
196
+ import { runRetiredCodexModelMigration } from "../codex/retired-model-migration";
195
197
  import {
196
198
  bindNativeMainStartupLifecycle,
197
199
  blockNativeMainStartupForUnownedServiceHome,
@@ -203,6 +205,8 @@ import {
203
205
  import { handleImages } from "./images";
204
206
  import { handleLive, logLiveSidebandFrame, parseLiveSidebandTarget, resolveLiveSidebandUpgrade } from "./live";
205
207
  import { handleSearch } from "./search";
208
+ import { handleContextHistory } from "./context-history";
209
+ import { codexCompatibleUrl, contextEndpoint, contextRelayActivated } from "../codex/context-compat";
206
210
  import { fetchAllModels, handleManagementAPI, VERSION, type ManagementApiDeps } from "./management-api";
207
211
  import {
208
212
  createManagementSessionControl,
@@ -666,7 +670,7 @@ export function startServer(port?: number, deps: StartServerDeps = {}): Server<W
666
670
  // Reconcile disk-backed presets first: it replaces provider rows and must not undo
667
671
  // an in-memory wire upgrade when that upgrade's persistence is temporarily unavailable.
668
672
  reconcileOAuthProviders(startupConfig);
669
- const config = migrateStartupXaiResponses(startupConfig);
673
+ const config = migrateStartupZaiResponses(migrateStartupXaiResponses(startupConfig));
670
674
  warnAgentTaskRecoveryStartup(config);
671
675
  setLiveStateStoreConfig(config);
672
676
  applyProxyEnv(config);
@@ -682,24 +686,11 @@ export function startServer(port?: number, deps: StartServerDeps = {}): Server<W
682
686
  // indistinguishable from "never chose". Pin those to subscription once so an upgrade
683
687
  // never silently moves a deliberate subscriber onto proxy.
684
688
  if (runClaudeAuthModeMigration(config)) saveConfig(config);
685
- // Sidecar model migration (KST 2026-07-10 06:00 = UTC 2026-07-09 21:00): auto-migrate the old
686
- // gpt-5.4-mini default to gpt-5.6-luna for both search and vision sidecars. Only touches configs
687
- // still on the old default — explicit user choices are preserved.
688
- {
689
- const SIDECAR_MIGRATION_CUTOFF = Date.UTC(2026, 6, 9, 21, 0); // July 9 21:00 UTC = KST July 10 06:00
690
- if (Date.now() >= SIDECAR_MIGRATION_CUTOFF) {
691
- let migrated = false;
692
- if (config.webSearchSidecar?.model === "gpt-5.4-mini") {
693
- config.webSearchSidecar = { ...config.webSearchSidecar, model: "gpt-5.6-luna" };
694
- migrated = true;
695
- }
696
- if (config.visionSidecar?.model === "gpt-5.4-mini") {
697
- config.visionSidecar = { ...config.visionSidecar, model: "gpt-5.6-luna" };
698
- migrated = true;
699
- }
700
- if (migrated) saveConfig(config);
701
- }
702
- }
689
+ // Retired Codex-login models: a stored gpt-5.4-mini is a guaranteed 404 for the search and
690
+ // vision sidecars and for pool warmup, so it moves to gpt-5.6-luna. Extracted so the rule is
691
+ // testable on its own; see src/codex/retired-model-migration.ts for why exact equality also
692
+ // rewrites an explicit choice.
693
+ if (runRetiredCodexModelMigration(config)) saveConfig(config);
703
694
  // Resolve unattended service-home authority before any Codex lock, cache, owner,
704
695
  // journal, or credential path. Both positive foreign evidence and an unprovable
705
696
  // ownership state are non-authority.
@@ -856,6 +847,7 @@ export function startServer(port?: number, deps: StartServerDeps = {}): Server<W
856
847
  if (path === "/v1/messages" || path === "/v1/chat/completions") return req.method === "POST";
857
848
  if (path === "/v1/messages/count_tokens") return req.method === "POST";
858
849
  if (path === "/v1/alpha/search") return req.method === "POST";
850
+ if (contextEndpoint(path)) return req.method === "POST";
859
851
  if (path === "/v1/images/generations" || path === "/v1/images/edits") {
860
852
  return req.method === "POST";
861
853
  }
@@ -1102,7 +1094,7 @@ export function startServer(port?: number, deps: StartServerDeps = {}): Server<W
1102
1094
  // The unauthenticated loopback listener (#1102) serves a fixed allowlist and nothing
1103
1095
  // else. Rejecting here, before any handler runs, is what keeps the surface from growing
1104
1096
  // silently when a route is added below.
1105
- if (ingress === "unauthenticated-loopback" && !loopbackRouteAllowed(new URL(req.url), req)) {
1097
+ if (ingress === "unauthenticated-loopback" && !loopbackRouteAllowed(codexCompatibleUrl(req.url), req)) {
1106
1098
  return withCors(
1107
1099
  formatErrorResponse(404, "not_found", `Unknown endpoint: ${req.method} ${new URL(req.url).pathname}`),
1108
1100
  req,
@@ -1111,7 +1103,7 @@ export function startServer(port?: number, deps: StartServerDeps = {}): Server<W
1111
1103
  }
1112
1104
  // Tailscale Serve terminates only on this separately bound loopback socket. Reject before
1113
1105
  // dispatch so no data, readiness, health, WebSocket, or unknown-static handler can run.
1114
- if (ingress === "hub-management" && !managementIngressRouteAllowed(new URL(req.url), req)) {
1106
+ if (ingress === "hub-management" && !managementIngressRouteAllowed(codexCompatibleUrl(req.url), req)) {
1115
1107
  return withCors(
1116
1108
  formatErrorResponse(404, "not_found", `Unknown endpoint: ${req.method} ${new URL(req.url).pathname}`),
1117
1109
  req,
@@ -1124,7 +1116,7 @@ export function startServer(port?: number, deps: StartServerDeps = {}): Server<W
1124
1116
  // same code path a plain loopback bind has always taken — Host-header check included.
1125
1117
  // Routing, provider selection and response bodies keep using `config`.
1126
1118
  const policy: RequestPolicyView = ingress === "unauthenticated-loopback" ? loopbackPolicy() : config;
1127
- const url = new URL(req.url);
1119
+ const url = codexCompatibleUrl(req.url);
1128
1120
  markActivity(`${req.method} ${url.pathname}`);
1129
1121
 
1130
1122
  // Readiness is exact-GET on the literal /readyz path. Compare the DECODED
@@ -1966,6 +1958,34 @@ export function startServer(port?: number, deps: StartServerDeps = {}): Server<W
1966
1958
  }), req, policy);
1967
1959
  }
1968
1960
 
1961
+ if (contextEndpoint(url.pathname) !== undefined && req.method === "POST" && contextRelayActivated()) {
1962
+ // No timeout disable here. The relay is a bounded JSON round trip that owns one deadline
1963
+ // from entry; removing the idle timeout first would let an unfinished body hold an
1964
+ // admitted turn slot indefinitely, before that deadline ever starts.
1965
+ if (isDraining()) {
1966
+ return drainingResponse(req, policy);
1967
+ }
1968
+ const admission = resolveApiAuth(req, policy);
1969
+ if (!admission) return withCors(formatErrorResponse(401, "authentication_error", "opencodex API key required"), req, policy);
1970
+ if (!isAllowedRequestOrigin(req, policy)) {
1971
+ return withCors(formatErrorResponse(403, "origin_rejected", "cross-origin data-plane request blocked"), req, policy);
1972
+ }
1973
+ const start = Date.now();
1974
+ const requestId = nextRequestLogId(start);
1975
+ const logCtx: RequestLogContext = {
1976
+ model: "context_history",
1977
+ provider: "unknown",
1978
+ ...admissionFields(admission),
1979
+ };
1980
+ return runAdmittedHttpTurn(req, policy, async turnAdmissionLease => {
1981
+ const response = await handleContextHistory(req, config, logCtx, contextEndpoint(url.pathname)!,
1982
+ turnAdmissionLease, admission, () => resolveApiAuth(req, policy));
1983
+ addFinalRequestLog(requestId, start, logCtx, response.status,
1984
+ response.status === 499 ? { closeReason: "client_cancel" } : undefined);
1985
+ return withCors(response, req, policy);
1986
+ });
1987
+ }
1988
+
1969
1989
  if (url.pathname === "/v1/alpha/search" && req.method === "POST") {
1970
1990
  disableResponsesRequestTimeout(req, requestServer);
1971
1991
  if (isDraining()) {
@@ -1,3 +1,4 @@
1
+ import { codexCompatibleUrl } from "../codex/context-compat";
1
2
  /**
2
3
  * /v1/live and /v1/realtime/calls relay (issue #371).
3
4
  *
@@ -635,7 +636,7 @@ export async function handleLive(
635
636
  // Frameless API-shape call-create posts to `{base}/live` without the AVAS
636
637
  // query (openai/codex RealtimeCallClient, realtime_call.rs); only the
637
638
  // realtime/calls inbound shape keeps the legacy keyed AVAS endpoint.
638
- url = new URL(req.url).pathname === "/v1/live"
639
+ url = codexCompatibleUrl(req.url).pathname === "/v1/live"
639
640
  ? forwardLiveUrl(relay.providerBaseUrl, /* usesBackendShape */ false)
640
641
  : keyedLiveUrl(relay.providerBaseUrl);
641
642
  }
@@ -948,10 +948,16 @@ export async function handleAgentSettingsRoutes(ctx: ManagementContext): Promise
948
948
  const { parseDesktopProfile, reconcileDesktopProfile } = await import("../../claude/desktop-profile");
949
949
  const parsed = parseDesktopProfile(body.profile);
950
950
  const current = await buildClaudeDesktopState(config);
951
+ const availableRoutes = new Set(current.models.filter(item => item.available).map(item => item.route));
952
+ for (const route of Object.keys(parsed.assignments)) {
953
+ if (!current.profile.assignments[route] && !availableRoutes.has(route)) {
954
+ throw new Error(`현재 사용할 수 없는 모델은 추가할 수 없습니다: ${route}`);
955
+ }
956
+ }
951
957
  for (const model of current.models.filter(item => !item.available)) {
952
958
  const before = current.profile.assignments[model.route];
953
959
  const after = parsed.assignments[model.route];
954
- if (JSON.stringify(before) !== JSON.stringify(after)) {
960
+ if (after !== undefined && JSON.stringify(before) !== JSON.stringify(after)) {
955
961
  throw new Error(`현재 사용할 수 없는 모델은 옮길 수 없습니다: ${model.route}`);
956
962
  }
957
963
  }
@@ -237,7 +237,7 @@ export async function syncEnabledClientIntegrations(
237
237
  },
238
238
  config,
239
239
  port,
240
- }, ["mcode", "pi", "aside", "raycast"]));
240
+ }, ["mcode", "pi", "aside", "raycast", "omo", "cline"]));
241
241
 
242
242
  return out;
243
243
  }
@@ -820,8 +820,8 @@ export async function handleConfigRoutes(ctx: ManagementContext): Promise<Respon
820
820
  if (body.vision && (body.vision.model !== undefined || body.vision.reasoning !== undefined)) {
821
821
  visionReasoningTouched = true;
822
822
  const model = typeof body.vision.model === "string"
823
- ? (body.vision.model === "" ? "gpt-5.4-mini" : body.vision.model)
824
- : (config.visionSidecar?.model || "gpt-5.4-mini");
823
+ ? (body.vision.model === "" ? "gpt-5.6-luna" : body.vision.model)
824
+ : (config.visionSidecar?.model || "gpt-5.6-luna");
825
825
  const sourceReasoning = body.vision.reasoning ?? config.visionSidecar?.reasoning;
826
826
  normalizedVisionReasoning = sourceReasoning === undefined
827
827
  ? undefined
@@ -0,0 +1,287 @@
1
+ /**
2
+ * Management API handlers for Grok quota reset coupons.
3
+ *
4
+ * Exposes inspection and consumption of Grok billing reset coupons via gRPC-Web
5
+ * to Grok ConsumerUiSvc upstream endpoints.
6
+ *
7
+ * Inherits management authentication from requireManagementAuth in management-api.ts.
8
+ * Lazy-loaded by handleGrokCouponRoutesOnDemand to keep startup fast and honor the
9
+ * core-lab boundary contract.
10
+ */
11
+
12
+ import { jsonResponse } from "../auth-cors";
13
+ import type { ManagementContext } from "./context";
14
+ import { isCodexResetCreditOperationId } from "../../codex/reset-credit-recovery";
15
+ import { getValidAccessSnapshotForAccount } from "../../oauth";
16
+ import { listAccounts, captureOAuthAccountSelection } from "../../oauth/store";
17
+ import {
18
+ getGrokRemainingResets,
19
+ redeemGrokResetCoupon,
20
+ type GrokResetCoupon,
21
+ } from "../../grok/reset-coupons";
22
+ import {
23
+ openGrokResetCouponOperation,
24
+ recordGrokResetCouponSettlement,
25
+ type GrokResetCouponOperationRecord,
26
+ } from "../../grok/reset-coupon-ledger";
27
+
28
+ export interface GrokResetCouponsResponse {
29
+ accountId: string;
30
+ tokens: Array<{
31
+ tokenId: string;
32
+ validityStart: string;
33
+ validityEnd: string;
34
+ }>;
35
+ remaining: number;
36
+ }
37
+
38
+ export interface GrokConsumeCouponRequestBody {
39
+ accountId?: string;
40
+ tokenId?: string;
41
+ operationId?: string;
42
+ }
43
+
44
+ function resolveTargetAccountId(requestedAccountId?: string): string {
45
+ if (requestedAccountId && requestedAccountId.trim() !== "") {
46
+ return requestedAccountId.trim();
47
+ }
48
+ const selection = captureOAuthAccountSelection("xai");
49
+ if (selection?.accountId) {
50
+ return selection.accountId;
51
+ }
52
+ const accounts = listAccounts("xai");
53
+ if (accounts.length > 0) {
54
+ return accounts[0].id;
55
+ }
56
+ throw new Error("No xAI account found or active");
57
+ }
58
+
59
+ export async function handleGrokCouponRoutes(ctx: ManagementContext): Promise<Response | null> {
60
+ const { url, req, config } = ctx;
61
+ const { pathname } = url;
62
+
63
+ if (pathname === "/api/grok/reset-coupons") {
64
+ if (req.method !== "GET") {
65
+ return jsonResponse({ error: "Method not allowed" }, 405, req, config);
66
+ }
67
+
68
+ const queryAccountId = url.searchParams.get("accountId") ?? undefined;
69
+ let accountId: string;
70
+ try {
71
+ accountId = resolveTargetAccountId(queryAccountId);
72
+ } catch (err) {
73
+ return jsonResponse(
74
+ { error: { code: "no_account", message: err instanceof Error ? err.message : String(err) } },
75
+ 400,
76
+ req,
77
+ config,
78
+ );
79
+ }
80
+
81
+ let tokenSnapshot;
82
+ try {
83
+ tokenSnapshot = await getValidAccessSnapshotForAccount("xai", accountId, { requireUsableAccount: true });
84
+ } catch (err) {
85
+ return jsonResponse(
86
+ { error: { code: "auth_failed", message: "Failed to resolve valid xAI credentials for account" } },
87
+ 401,
88
+ req,
89
+ config,
90
+ );
91
+ }
92
+
93
+ try {
94
+ const remainingResult = await getGrokRemainingResets({
95
+ accessToken: tokenSnapshot.accessToken,
96
+ });
97
+
98
+ const payload: GrokResetCouponsResponse = {
99
+ accountId,
100
+ tokens: remainingResult.tokens.map((t) => ({
101
+ tokenId: t.tokenId,
102
+ validityStart: t.validityStart,
103
+ validityEnd: t.validityEnd,
104
+ })),
105
+ remaining: remainingResult.tokens.length,
106
+ };
107
+
108
+ return jsonResponse(payload, 200, req, config);
109
+ } catch (err) {
110
+ return jsonResponse(
111
+ { error: { code: "upstream_error", message: err instanceof Error ? err.message : String(err) } },
112
+ 502,
113
+ req,
114
+ config,
115
+ );
116
+ }
117
+ }
118
+
119
+ if (pathname === "/api/grok/reset-coupons/consume") {
120
+ if (req.method !== "POST") {
121
+ return jsonResponse({ error: "Method not allowed" }, 405, req, config);
122
+ }
123
+
124
+ let body: GrokConsumeCouponRequestBody;
125
+ try {
126
+ body = (await req.json()) as GrokConsumeCouponRequestBody;
127
+ } catch {
128
+ return jsonResponse({ error: { code: "invalid_json", message: "Invalid JSON body" } }, 400, req, config);
129
+ }
130
+
131
+ const { accountId: rawAccountId, tokenId: requestedTokenId, operationId } = body;
132
+
133
+ if (operationId !== undefined && !isCodexResetCreditOperationId(operationId)) {
134
+ return jsonResponse(
135
+ { error: { code: "invalid_operation_id", message: "operationId must be a valid UUIDv4" } },
136
+ 400,
137
+ req,
138
+ config,
139
+ );
140
+ }
141
+
142
+ let accountId: string;
143
+ try {
144
+ accountId = resolveTargetAccountId(rawAccountId);
145
+ } catch (err) {
146
+ return jsonResponse(
147
+ { error: { code: "no_account", message: err instanceof Error ? err.message : String(err) } },
148
+ 400,
149
+ req,
150
+ config,
151
+ );
152
+ }
153
+
154
+ let tokenSnapshot;
155
+ try {
156
+ tokenSnapshot = await getValidAccessSnapshotForAccount("xai", accountId, { requireUsableAccount: true });
157
+ } catch (err) {
158
+ return jsonResponse(
159
+ { error: { code: "auth_failed", message: "Failed to resolve valid xAI credentials for account" } },
160
+ 401,
161
+ req,
162
+ config,
163
+ );
164
+ }
165
+
166
+ // Journaling and Idempotency settlement check
167
+ const effectiveOpId = operationId ?? crypto.randomUUID();
168
+ const opRecord = openGrokResetCouponOperation({
169
+ accountId,
170
+ tokenId: requestedTokenId,
171
+ operationId: effectiveOpId,
172
+ });
173
+
174
+ if (opRecord.kind === "replay") {
175
+ return jsonResponse(
176
+ {
177
+ code: opRecord.code,
178
+ replayed: true,
179
+ tokenId: opRecord.tokenId,
180
+ settledAt: opRecord.settledAt,
181
+ },
182
+ 200,
183
+ req,
184
+ config,
185
+ );
186
+ }
187
+
188
+ if (opRecord.kind === "identity-mismatch") {
189
+ return jsonResponse(
190
+ {
191
+ error: {
192
+ code: "operation_id_owned_by_another_account",
193
+ message: "Operation ID was previously registered with a different account or token",
194
+ },
195
+ },
196
+ 409,
197
+ req,
198
+ config,
199
+ );
200
+ }
201
+
202
+ if (opRecord.kind !== "execute") {
203
+ return jsonResponse(
204
+ {
205
+ error: {
206
+ code: opRecord.kind,
207
+ message: "Coupon ledger capacity or unavailable failure",
208
+ },
209
+ },
210
+ 503,
211
+ req,
212
+ config,
213
+ );
214
+ }
215
+
216
+ let resolvedTokenId = requestedTokenId;
217
+ if (!resolvedTokenId) {
218
+ try {
219
+ const remaining = await getGrokRemainingResets({ accessToken: tokenSnapshot.accessToken });
220
+ if (!remaining.tokens || remaining.tokens.length === 0) {
221
+ recordGrokResetCouponSettlement({
222
+ operationId: effectiveOpId,
223
+ code: "no_coupons_available",
224
+ status: "failed",
225
+ });
226
+ return jsonResponse(
227
+ { error: { code: "no_coupons_available", message: "No reset coupons available to redeem" } },
228
+ 400,
229
+ req,
230
+ config,
231
+ );
232
+ }
233
+ resolvedTokenId = remaining.tokens[0].tokenId;
234
+ } catch (err) {
235
+ return jsonResponse(
236
+ { error: { code: "fetch_resets_failed", message: err instanceof Error ? err.message : String(err) } },
237
+ 502,
238
+ req,
239
+ config,
240
+ );
241
+ }
242
+ }
243
+
244
+ try {
245
+ const redeemResult = await redeemGrokResetCoupon({
246
+ accessToken: tokenSnapshot.accessToken,
247
+ tokenId: resolvedTokenId,
248
+ });
249
+
250
+ recordGrokResetCouponSettlement({
251
+ operationId: effectiveOpId,
252
+ tokenId: resolvedTokenId,
253
+ code: "redeemed",
254
+ status: "success",
255
+ });
256
+
257
+ return jsonResponse(
258
+ {
259
+ success: true,
260
+ code: "redeemed",
261
+ replayed: false,
262
+ tokenId: resolvedTokenId,
263
+ accountId,
264
+ operationId: effectiveOpId,
265
+ },
266
+ 200,
267
+ req,
268
+ config,
269
+ );
270
+ } catch (err) {
271
+ recordGrokResetCouponSettlement({
272
+ operationId: effectiveOpId,
273
+ tokenId: resolvedTokenId,
274
+ code: "redeem_failed",
275
+ status: "failed",
276
+ });
277
+ return jsonResponse(
278
+ { error: { code: "redeem_failed", message: err instanceof Error ? err.message : String(err) } },
279
+ 502,
280
+ req,
281
+ config,
282
+ );
283
+ }
284
+ }
285
+
286
+ return null;
287
+ }
@@ -16,6 +16,7 @@ import {
16
16
  asideJournalDeleteResponse, type AsideProfileRouteOptions,
17
17
  } from "./aside-profile-routes";
18
18
  import type { IntegrationIO } from "../../integrations/config-io";
19
+ import { createClineIO } from "../../integrations/cline-io";
19
20
  import { matchesOperationResult } from "../../integrations/journal";
20
21
  import {
21
22
  INTEGRATION_CLIENT_IDS,
@@ -534,7 +535,11 @@ export async function handleIntegrationRoutes(ctx: ManagementContext): Promise<R
534
535
  undoable: (() => {
535
536
  if (snapshot === "expired") return false;
536
537
  if (newestByClient.get(operation.clientId) !== operation.opId) return false;
537
- const current = currentConfigText(operation.configPath);
538
+ const current = operation.clientId === "cline" ? (() => {
539
+ const io = createClineIO(integrationMutationTestHooks?.io ?? store.io(), operation.configPath, store);
540
+ const read = io.readText(operation.configPath);
541
+ return read.kind === "text" ? read.text : read.kind === "missing" ? null : undefined;
542
+ })() : currentConfigText(operation.configPath);
538
543
  return current === undefined ? false : matchesOperationResult(operation, current);
539
544
  })(),
540
545
  /*