@iislee/opencodex 2.31.0 → 2.35.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 (261) hide show
  1. package/README.md +3 -3
  2. package/bin/ocx.mjs +99 -70
  3. package/gui/dist/assets/index-Be1sDpWw.css +1 -0
  4. package/gui/dist/assets/index-D4OKgUV4.js +114 -0
  5. package/gui/dist/index.html +2 -2
  6. package/package.json +3 -1
  7. package/src/adapters/anthropic.ts +104 -15
  8. package/src/adapters/base.ts +29 -1
  9. package/src/adapters/command-code.ts +42 -6
  10. package/src/adapters/cursor/call-id.ts +44 -0
  11. package/src/adapters/cursor/catalog.ts +541 -0
  12. package/src/adapters/cursor/checkpoint-store.ts +15 -10
  13. package/src/adapters/cursor/cursor-errors.ts +15 -0
  14. package/src/adapters/cursor/discovery.ts +69 -18
  15. package/src/adapters/cursor/effort-map.ts +79 -1
  16. package/src/adapters/cursor/envelope-echo.ts +290 -0
  17. package/src/adapters/cursor/live-models.ts +7 -2
  18. package/src/adapters/cursor/live-transport.ts +14 -0
  19. package/src/adapters/cursor/message-mapper.ts +4 -1
  20. package/src/adapters/cursor/native-exec-desktop.ts +23 -0
  21. package/src/adapters/cursor/native-exec-fs.ts +2 -2
  22. package/src/adapters/cursor/native-exec-network.ts +1 -1
  23. package/src/adapters/cursor/native-exec-shell.ts +5 -2
  24. package/src/adapters/cursor/native-exec.ts +20 -0
  25. package/src/adapters/cursor/protobuf-request.ts +68 -14
  26. package/src/adapters/cursor/request-builder.ts +40 -10
  27. package/src/adapters/cursor/tool-definitions.ts +14 -4
  28. package/src/adapters/cursor/tool-result-normalize.ts +28 -5
  29. package/src/adapters/cursor/types.ts +20 -0
  30. package/src/adapters/cursor.ts +209 -38
  31. package/src/adapters/exec-tool-result-normalize.ts +99 -0
  32. package/src/adapters/google-antigravity-replay.ts +173 -25
  33. package/src/adapters/google-antigravity-wire.ts +5 -0
  34. package/src/adapters/google-errors.ts +44 -12
  35. package/src/adapters/google.ts +136 -24
  36. package/src/adapters/image.ts +1 -1
  37. package/src/adapters/kiro-constants.ts +27 -0
  38. package/src/adapters/kiro-tools.ts +55 -16
  39. package/src/adapters/kiro.ts +182 -23
  40. package/src/adapters/openai-chat.ts +300 -242
  41. package/src/adapters/openai-responses.ts +353 -27
  42. package/src/adapters/run-turn-queue.ts +36 -1
  43. package/src/adapters/tool-catalog-nudge.ts +4 -3
  44. package/src/adapters/xai-tool-schema.ts +436 -0
  45. package/src/adapters/xai-web-search.ts +17 -16
  46. package/src/bridge.ts +90 -37
  47. package/src/chat/inbound.ts +14 -1
  48. package/src/chat/outbound.ts +15 -7
  49. package/src/claude/agents-inject.ts +8 -1
  50. package/src/claude/context-windows.ts +16 -9
  51. package/src/claude/outbound.ts +24 -11
  52. package/src/cli/access.ts +46 -3
  53. package/src/cli/account-api.ts +116 -19
  54. package/src/cli/account-extended.ts +262 -29
  55. package/src/cli/account-main.ts +12 -12
  56. package/src/cli/account.ts +69 -15
  57. package/src/cli/agent.ts +8 -1
  58. package/src/cli/alias.ts +66 -0
  59. package/src/cli/capabilities-command.ts +94 -0
  60. package/src/cli/capabilities.ts +496 -0
  61. package/src/cli/claude-desktop.ts +31 -11
  62. package/src/cli/claude.ts +26 -1
  63. package/src/cli/dispatch.ts +256 -28
  64. package/src/cli/doctor.ts +126 -14
  65. package/src/cli/help.ts +17 -6
  66. package/src/cli/index.ts +39 -9
  67. package/src/cli/init.ts +1 -0
  68. package/src/cli/inspect.ts +230 -0
  69. package/src/cli/models-runtime.ts +95 -0
  70. package/src/cli/models.ts +26 -10
  71. package/src/cli/observe.ts +31 -8
  72. package/src/cli/provider-runtime.ts +16 -2
  73. package/src/cli/provider.ts +2 -1
  74. package/src/cli/registry.ts +49 -10
  75. package/src/cli/runtime-api.ts +51 -7
  76. package/src/cli/status.ts +41 -1
  77. package/src/cli/storage.ts +234 -0
  78. package/src/cli/system-command.ts +16 -0
  79. package/src/cli/system-restart-client.ts +1 -1
  80. package/src/cli/usage-report.ts +184 -0
  81. package/src/cli/v2.ts +34 -10
  82. package/src/cli/version-skew.ts +46 -0
  83. package/src/clients/config-export.ts +85 -3
  84. package/src/clients/effective-status.ts +30 -2
  85. package/src/clients/probes/cline.ts +81 -0
  86. package/src/codex/account-label.ts +21 -0
  87. package/src/codex/account-pause.ts +2 -1
  88. package/src/codex/account-priority.ts +3 -2
  89. package/src/codex/app-server-processes.ts +83 -7
  90. package/src/codex/auth-api.ts +50 -10
  91. package/src/codex/auth-context.ts +21 -18
  92. package/src/codex/autostart-health.ts +16 -0
  93. package/src/codex/catalog/aggregation.ts +18 -0
  94. package/src/codex/catalog/effort.ts +18 -3
  95. package/src/codex/catalog/metadata.ts +27 -1
  96. package/src/codex/catalog/native-models.ts +5 -2
  97. package/src/codex/catalog/parsing.ts +72 -27
  98. package/src/codex/catalog/provider-fetch.ts +187 -31
  99. package/src/codex/catalog/sync.ts +131 -5
  100. package/src/codex/catalog.ts +1 -1
  101. package/src/codex/codex-write-lock.ts +3 -1
  102. package/src/codex/convergence-types.ts +1 -1
  103. package/src/codex/convergence.ts +27 -2
  104. package/src/codex/desired-state.ts +2 -2
  105. package/src/codex/desktop-app-restart.ts +355 -0
  106. package/src/codex/history-job.ts +32 -3
  107. package/src/codex/history-manifest.ts +112 -0
  108. package/src/codex/history-migration-guardian.ts +5 -5
  109. package/src/codex/history-provider.ts +825 -247
  110. package/src/codex/history-worker.ts +8 -5
  111. package/src/codex/inject-coordination.ts +83 -0
  112. package/src/codex/inject.ts +63 -22
  113. package/src/codex/injected-marker.ts +1 -1
  114. package/src/codex/internal/history-writer.ts +4 -3
  115. package/src/codex/log-guard/inspect.ts +22 -4
  116. package/src/codex/model-entitlements.ts +9 -2
  117. package/src/codex/native-profile-startup.ts +157 -27
  118. package/src/codex/native-residue.ts +26 -33
  119. package/src/codex/prompt-layers.ts +371 -25
  120. package/src/codex/prompt-text-probe.ts +238 -0
  121. package/src/codex/quota.ts +91 -10
  122. package/src/codex/routing.ts +9 -0
  123. package/src/codex/shim.ts +56 -3
  124. package/src/codex/subagent-model-fallback.ts +104 -25
  125. package/src/codex/transition-state.ts +116 -8
  126. package/src/combos/failover.ts +27 -0
  127. package/src/combos/types.ts +10 -0
  128. package/src/compatibility/index.ts +26 -0
  129. package/src/compatibility/manifest.ts +253 -0
  130. package/src/compatibility/openai-responses.ts +121 -0
  131. package/src/config/atomic-write.ts +219 -0
  132. package/src/config/paths.ts +40 -0
  133. package/src/config/process-state.ts +309 -0
  134. package/src/config/provider-validation.ts +177 -0
  135. package/src/config/rebase-provenance.ts +68 -0
  136. package/src/config.ts +206 -824
  137. package/src/generated/compatibility-version.json +373 -213
  138. package/src/images/loop.ts +5 -4
  139. package/src/images/plan.ts +5 -4
  140. package/src/integrations/ownership-policy.ts +160 -0
  141. package/src/integrations/ownership.ts +46 -2
  142. package/src/integrations/registry.ts +7 -0
  143. package/src/integrations/state.ts +79 -7
  144. package/src/integrations/writer.ts +26 -2
  145. package/src/lab/conformance/fixtures/protocol-v1-cases.json +1 -1
  146. package/src/lab/fabric/producer-child.ts +1 -1
  147. package/src/lib/admin-secrets.ts +24 -0
  148. package/src/lib/bounded-body.ts +14 -2
  149. package/src/lib/config-ownership.ts +20 -0
  150. package/src/lib/errors.ts +36 -3
  151. package/src/lib/package-tree-integrity.ts +101 -0
  152. package/src/lib/process-control.ts +2 -1
  153. package/src/lib/service-secrets.ts +15 -0
  154. package/src/lib/tool-argument-integers.ts +56 -5
  155. package/src/oauth/callback-server.ts +22 -2
  156. package/src/oauth/cline.ts +203 -0
  157. package/src/oauth/generic-account-failover.ts +231 -0
  158. package/src/oauth/health.ts +1 -1
  159. package/src/oauth/index.ts +65 -6
  160. package/src/oauth/key-providers.ts +10 -1
  161. package/src/oauth/kimi.ts +9 -1
  162. package/src/oauth/kiro.ts +45 -0
  163. package/src/oauth/local-token-detect.ts +2 -0
  164. package/src/oauth/open-browser-choice.ts +26 -0
  165. package/src/oauth/store.ts +14 -5
  166. package/src/oauth/types.ts +15 -0
  167. package/src/providers/auto-compact-budget.ts +65 -0
  168. package/src/providers/command-code-efforts.ts +38 -6
  169. package/src/providers/context-cap.ts +4 -3
  170. package/src/providers/default-aliases.ts +65 -0
  171. package/src/providers/derive.ts +28 -0
  172. package/src/providers/fastwire.ts +7 -1
  173. package/src/providers/label.ts +34 -1
  174. package/src/providers/model-presets.ts +119 -0
  175. package/src/providers/new-model-policy.ts +146 -0
  176. package/src/providers/provider-id-rewrite.ts +2 -1
  177. package/src/providers/quota.ts +65 -7
  178. package/src/providers/registry.ts +184 -69
  179. package/src/providers/slug-codec.ts +52 -0
  180. package/src/providers/xai-transport.ts +21 -0
  181. package/src/reasoning-effort.ts +19 -2
  182. package/src/responses/apply-patch-envelope.ts +63 -0
  183. package/src/responses/code-mode-helper-compat.ts +50 -0
  184. package/src/responses/custom-tool-compat.ts +157 -39
  185. package/src/responses/namespace-tool-compat.ts +84 -4
  186. package/src/responses/parser.ts +7 -2
  187. package/src/responses/reasoning-replay-cache.ts +81 -3
  188. package/src/responses/schema.ts +5 -1
  189. package/src/responses/state.ts +94 -4
  190. package/src/responses/thought-signature-replay.ts +17 -0
  191. package/src/responses/turn-termination.ts +107 -0
  192. package/src/router.ts +40 -2
  193. package/src/routing/analytics.ts +1 -0
  194. package/src/routing/quota.ts +10 -0
  195. package/src/server/auth-cors.ts +27 -7
  196. package/src/server/chat-completions.ts +24 -16
  197. package/src/server/chat-native-sse.ts +3 -3
  198. package/src/server/chat-native.ts +30 -11
  199. package/src/server/claude-messages.ts +1 -1
  200. package/src/server/index.ts +179 -32
  201. package/src/server/lifecycle.ts +52 -1
  202. package/src/server/local-management-read-client.ts +1 -1
  203. package/src/server/local-provider-reload-client.ts +1 -1
  204. package/src/server/management/agent-settings-routes.ts +45 -23
  205. package/src/server/management/codex-prompt-routes.ts +570 -0
  206. package/src/server/management/combo-routes.ts +2 -1
  207. package/src/server/management/config-routes.ts +59 -12
  208. package/src/server/management/context.ts +10 -1
  209. package/src/server/management/logs-usage-routes.ts +30 -24
  210. package/src/server/management/model-routes.ts +274 -4
  211. package/src/server/management/model-rows.ts +4 -0
  212. package/src/server/management/native-integration-routes.ts +2 -1
  213. package/src/server/management/oauth-account-routes.ts +10 -4
  214. package/src/server/management/provider-capability-config.ts +1 -1
  215. package/src/server/management/provider-routes.ts +81 -11
  216. package/src/server/management/route-registry.ts +332 -0
  217. package/src/server/management/routing-profile-routes.ts +2 -2
  218. package/src/server/management/system-restart.ts +1 -1
  219. package/src/server/management-api.ts +18 -16
  220. package/src/server/port-reclaim.ts +1 -1
  221. package/src/server/proxy-liveness.ts +29 -5
  222. package/src/server/relay-eager.ts +147 -20
  223. package/src/server/relay.ts +251 -19
  224. package/src/server/request-log-conversation.ts +51 -0
  225. package/src/server/request-log.ts +70 -16
  226. package/src/server/responses/codex-auth-error.ts +55 -0
  227. package/src/server/responses/collaboration.ts +37 -3
  228. package/src/server/responses/combo-stream-preflight.ts +178 -0
  229. package/src/server/responses/compact.ts +6 -21
  230. package/src/server/responses/core.ts +810 -208
  231. package/src/server/responses/empty-completion-guard.ts +63 -6
  232. package/src/server/responses/fetch-helpers.ts +17 -102
  233. package/src/server/responses/input-admission.ts +3 -1
  234. package/src/server/responses/passthrough-error.ts +33 -9
  235. package/src/server/responses/responses-field-backfill.ts +105 -13
  236. package/src/server/responses/ws-upstream.ts +75 -2
  237. package/src/server/responses-custom-tool-repair.ts +87 -6
  238. package/src/server/responses-terminal-repair.ts +25 -4
  239. package/src/server/responses-undeclared-tool-guard.ts +241 -18
  240. package/src/server/sse-frame-buffer.ts +31 -4
  241. package/src/server/ws-bridge.ts +14 -2
  242. package/src/service.ts +42 -4
  243. package/src/storage/policy-job.ts +14 -4
  244. package/src/storage/policy.ts +88 -23
  245. package/src/types/config.ts +66 -0
  246. package/src/types/provider.ts +69 -0
  247. package/src/types/request.ts +17 -4
  248. package/src/types/tools.ts +116 -11
  249. package/src/types.ts +3 -1
  250. package/src/update/index.ts +5 -4
  251. package/src/update/job.ts +3 -1
  252. package/src/update/transactional-install.mjs +8 -1
  253. package/src/usage/expected-prices.ts +43 -24
  254. package/src/usage/log.ts +63 -12
  255. package/src/usage/summary.ts +211 -8
  256. package/src/vision/describe.ts +18 -13
  257. package/src/web-search/exa-executor.ts +40 -9
  258. package/src/web-search/executor.ts +10 -3
  259. package/src/web-search/loop.ts +5 -4
  260. package/gui/dist/assets/index-BF38heuV.js +0 -104
  261. package/gui/dist/assets/index-DMiI18Kv.css +0 -1
@@ -0,0 +1,101 @@
1
+ import { statSync } from "node:fs";
2
+
3
+ export interface PackageTreeObservation {
4
+ readonly device: bigint;
5
+ readonly inode: bigint;
6
+ readonly contentTimeNs: bigint;
7
+ readonly size: bigint;
8
+ }
9
+
10
+ export type PackageTreeIntegrityStatus =
11
+ | { readonly ok: true }
12
+ | { readonly ok: false; readonly reason: "package_tree_replaced" | "package_tree_unreadable" };
13
+
14
+ export interface PackageTreeIntegrityGuard {
15
+ status(): PackageTreeIntegrityStatus;
16
+ }
17
+
18
+ type ObservePackageTree = () => PackageTreeObservation | null;
19
+ type PackageTreeRuntimeInstall = "bun" | "npm" | "source";
20
+
21
+ const packageManifestUrl = new URL("../../package.json", import.meta.url);
22
+
23
+ function observePackageManifest(): PackageTreeObservation | null {
24
+ try {
25
+ const stat = statSync(packageManifestUrl, { bigint: true });
26
+ return {
27
+ device: stat.dev,
28
+ inode: stat.ino,
29
+ // mtimeNs, NOT ctimeNs. An inode-change time moves for METADATA writes that
30
+ // replace nothing: chmod, chown, touch, an editor normalizing permissions, a
31
+ // backup tool restoring modes. Each of those left device, inode and size
32
+ // identical, so the comparison below called the manifest "replaced" and every
33
+ // /v1/* request answered 503 until the process was restarted. Measured on
34
+ // macOS: chmod alone moved ctimeNs and left mtimeNs untouched.
35
+ //
36
+ // mtimeNs still catches every real replacement. An in-place rewrite of the
37
+ // same byte length moves mtimeNs while inode and size hold; an atomic
38
+ // install (write-then-rename, which is what a package manager does) changes
39
+ // the inode as well. Both were measured before this change was made.
40
+ contentTimeNs: stat.mtimeNs,
41
+ size: stat.size,
42
+ };
43
+ } catch {
44
+ return null;
45
+ }
46
+ }
47
+
48
+ function sameObservation(left: PackageTreeObservation, right: PackageTreeObservation): boolean {
49
+ return left.device === right.device
50
+ && left.inode === right.inode
51
+ && left.contentTimeNs === right.contentTimeNs
52
+ && left.size === right.size;
53
+ }
54
+
55
+ /**
56
+ * How long an `ok` observation is reused before the manifest is stat'd again.
57
+ *
58
+ * `status()` runs on `/healthz`, `/readyz` and every `/v1/*` request, so an unthrottled guard
59
+ * adds a filesystem syscall to the proxy's hot path to detect an event that happens at most
60
+ * once per install. A replaced tree is not time-critical either: the process is already
61
+ * serving broken imports, and one more second of that is not worse than a syscall per turn
62
+ * forever.
63
+ *
64
+ * A NEGATIVE result is never cached — once the tree looks wrong, every later request re-checks,
65
+ * so a repaired install recovers on its own instead of staying refused for a window.
66
+ */
67
+ const PACKAGE_TREE_RECHECK_MS = 1_000;
68
+
69
+ export function createPackageTreeIntegrityGuard(
70
+ observe: ObservePackageTree = observePackageManifest,
71
+ now: () => number = Date.now,
72
+ ): PackageTreeIntegrityGuard {
73
+ const boot = observe();
74
+ let lastOkAt: number | null = null;
75
+ return {
76
+ status(): PackageTreeIntegrityStatus {
77
+ const at = now();
78
+ if (lastOkAt !== null && at - lastOkAt < PACKAGE_TREE_RECHECK_MS) return { ok: true };
79
+ const current = observe();
80
+ if (boot === null || current === null) return { ok: false, reason: "package_tree_unreadable" };
81
+ if (!sameObservation(boot, current)) return { ok: false, reason: "package_tree_replaced" };
82
+ lastOkAt = at;
83
+ return { ok: true };
84
+ },
85
+ };
86
+ }
87
+
88
+ /**
89
+ * Installed packages fail closed when their manifest is replaced under a live process.
90
+ * A source checkout is different: editing package.json is ordinary development work, and
91
+ * Bun keeps serving the module snapshot already loaded by this process until the operator
92
+ * chooses to restart. Fencing every request there makes running dev directly impossible.
93
+ */
94
+ export function createRuntimePackageTreeIntegrityGuard(
95
+ installer: PackageTreeRuntimeInstall,
96
+ observe: ObservePackageTree = observePackageManifest,
97
+ now: () => number = Date.now,
98
+ ): PackageTreeIntegrityGuard {
99
+ if (installer === "source") return { status: () => ({ ok: true }) };
100
+ return createPackageTreeIntegrityGuard(observe, now);
101
+ }
@@ -1,5 +1,6 @@
1
1
  import { execFileSync } from "node:child_process";
2
- import { loadConfig, readRuntimePort } from "../config";
2
+ import { loadConfig } from "../config";
3
+ import { readRuntimePort } from "../config/process-state";
3
4
  import { configuredAdminToken } from "./admin-secrets";
4
5
 
5
6
  export function isProcessAlive(pid: number): boolean {
@@ -23,3 +23,18 @@ export function loadServiceTokenFromFile(env: Record<string, string | undefined>
23
23
  return null;
24
24
  }
25
25
  }
26
+
27
+ /**
28
+ * Contents of the installed service token file. The launch wrapper always re-exports
29
+ * this file as OPENCODEX_API_AUTH_TOKEN, so doctor and start must inspect it even
30
+ * when the calling shell has no data-plane env var.
31
+ * Returns the token or null — never throws, never logs the value.
32
+ */
33
+ export function readInstalledServiceToken(): string | null {
34
+ try {
35
+ const token = readFileSync(serviceApiTokenFilePath(), "utf8").trim();
36
+ return token || null;
37
+ } catch {
38
+ return null;
39
+ }
40
+ }
@@ -59,6 +59,35 @@ function declaresString(schema: SchemaNode): boolean {
59
59
  return Array.isArray(type) && type.includes("string");
60
60
  }
61
61
 
62
+ // Issue #2316: Codex advertises `multi_agent_v1__wait_agent`'s `timeout_ms` as a JSON
63
+ // Schema `number`, but its Rust runtime deserializes the field as `u64` and rejects
64
+ // `120000.0` with "invalid type: floating point `120000.0`, expected u64" before the
65
+ // tool ever runs. The schema lookup is not the problem — the error text comes from
66
+ // Codex's own deserializer, so the call reached it — the problem is that `number`
67
+ // alone never authorizes the integral-float repair below.
68
+ //
69
+ // This allowlist is deliberately one field wide. It names only what has a live
70
+ // reproduction against a real `u64`, because the repair is only unambiguous for a
71
+ // field that cannot legitimately hold a fraction. Generic names (`start`, `end`,
72
+ // `priority`, `port`) would silently rewrite a third-party tool's fractional value,
73
+ // so a new entry needs its own evidence, not a plausible-sounding name.
74
+ //
75
+ // Cursor's sibling `yield_time_ms` (src/adapters/cursor/tool-definitions.ts) is also
76
+ // declared `number`; it is NOT included here because no rejection has been captured
77
+ // against it. It gets its own change when it gets its own reproduction.
78
+ const U64_NUMBER_FIELDS = new Set(["timeout_ms"]);
79
+
80
+ // Issue #2443 / #2451: Codex Desktop's bare `wait` tool has the same schema/runtime
81
+ // split for `yield_time_ms` and `max_tokens`. Scope these names to that bare tool so a
82
+ // third-party or namespaced tool can still use fractional values legitimately.
83
+ // #2448 allowlisted the hyphenated `yield-time_ms`; live Grok 4.6 calls and the
84
+ // advertised wait schema both use the underscore form, so that name is the one
85
+ // with a captured u64 rejection. Cursor still uses the same underscore name on a
86
+ // namespaced tool; the wait-only map keeps that path byte-identical.
87
+ const U64_NUMBER_FIELDS_BY_TOOL = new Map<string, ReadonlySet<string>>([
88
+ ["wait", new Set(["yield_time_ms", "max_tokens"])],
89
+ ]);
90
+
62
91
  /** True when the node accepts a JSON number (`integer` or `number`), so a numeric
63
92
  * value is already schema-valid and must not be rewritten into a string. */
64
93
  function declaresNumeric(schema: SchemaNode): boolean {
@@ -118,7 +147,14 @@ interface CoerceResult {
118
147
  changed: boolean;
119
148
  }
120
149
 
121
- function coerceValue(value: unknown, schema: SchemaNode | undefined, root: SchemaNode, depth: number): CoerceResult {
150
+ function coerceValue(
151
+ value: unknown,
152
+ schema: SchemaNode | undefined,
153
+ root: SchemaNode,
154
+ depth: number,
155
+ toolName?: string,
156
+ propertyName?: string,
157
+ ): CoerceResult {
122
158
  // A hostile or deeply nested schema must not blow the stack.
123
159
  if (depth > 64) return { value, changed: false };
124
160
  const resolved = schema ? resolveRef(schema, root, new Set()) : undefined;
@@ -126,7 +162,19 @@ function coerceValue(value: unknown, schema: SchemaNode | undefined, root: Schem
126
162
  if (typeof value === "number") {
127
163
  if (!resolved) return { value, changed: false };
128
164
  const branches = compositionBranches(resolved);
129
- const integerDeclared = declaresInteger(resolved) || branches.some(declaresInteger);
165
+ // #2316: a known Codex-native u64 field counts as integer-declared even when the
166
+ // advertised schema says `number`, but only when the field really is numeric —
167
+ // an allowlisted name over a string-typed field is a disagreement, not a repair.
168
+ const nativeU64Field = propertyName !== undefined
169
+ && (
170
+ U64_NUMBER_FIELDS.has(propertyName)
171
+ || U64_NUMBER_FIELDS_BY_TOOL.get(toolName ?? "")?.has(propertyName) === true
172
+ );
173
+ const nativeU64Declared = nativeU64Field
174
+ && (declaresNumeric(resolved) || branches.some(declaresNumeric));
175
+ const integerDeclared = declaresInteger(resolved)
176
+ || branches.some(declaresInteger)
177
+ || nativeU64Declared;
130
178
  if (!integerDeclared && safelyIntegral(value)) {
131
179
  // Issue #1938: a bare integer in a string-only field has exactly one faithful
132
180
  // string reading. A field that also accepts a numeric type keeps the number.
@@ -148,7 +196,9 @@ function coerceValue(value: unknown, schema: SchemaNode | undefined, root: Schem
148
196
  const itemSchema = resolved ? asSchema(resolved.items) : undefined;
149
197
  let changed = false;
150
198
  const next = value.map(entry => {
151
- const result = coerceValue(entry, itemSchema, root, depth + 1);
199
+ // Array items have no property name of their own; passing the array's key would
200
+ // let `timeout_ms: [1.5]` inherit the allowlist. Items are judged by schema only.
201
+ const result = coerceValue(entry, itemSchema, root, depth + 1, toolName);
152
202
  if (result.changed) changed = true;
153
203
  return result.value;
154
204
  });
@@ -164,7 +214,7 @@ function coerceValue(value: unknown, schema: SchemaNode | undefined, root: Schem
164
214
  const next: Record<string, unknown> = {};
165
215
  for (const [key, entry] of Object.entries(object)) {
166
216
  const childSchema = asSchema(properties?.[key]) ?? additional;
167
- const result = coerceValue(entry, childSchema, root, depth + 1);
217
+ const result = coerceValue(entry, childSchema, root, depth + 1, toolName, key);
168
218
  if (result.changed) changed = true;
169
219
  next[key] = result.value;
170
220
  }
@@ -182,6 +232,7 @@ function coerceValue(value: unknown, schema: SchemaNode | undefined, root: Schem
182
232
  export function coerceIntegerToolArguments(
183
233
  args: string,
184
234
  parameters: Record<string, unknown> | undefined,
235
+ toolName?: string,
185
236
  ): string {
186
237
  if (!parameters || !args) return args;
187
238
  // Cheap reject: a payload with no digit cannot need either repair (integral-float
@@ -196,7 +247,7 @@ export function coerceIntegerToolArguments(
196
247
  return args;
197
248
  }
198
249
  const root = parameters as SchemaNode;
199
- const result = coerceValue(parsed, root, root, 0);
250
+ const result = coerceValue(parsed, root, root, 0, toolName);
200
251
  if (!result.changed) return args;
201
252
  return JSON.stringify(result.value);
202
253
  }
@@ -276,10 +276,30 @@ export function parseCallbackInput(input: string): { kind: "url" | "query" | "ra
276
276
 
277
277
  try {
278
278
  const url = new URL(value);
279
+ // Also read the fragment. No provider configured here returns one — every
280
+ // OAuthCallbackFlow asks for response_type=code without response_mode, so
281
+ // the parameters land in the query — but a full URL whose parameters sit in
282
+ // the hash parses as a valid URL with no code, and reading only the query
283
+ // rejects it as "no authorization code found in input". This is the one
284
+ // place a fragment-returning provider would land, and the raw branch below
285
+ // already understands `code#state`.
286
+ //
287
+ // The query wins as a WHOLE when it carries the response, and the two
288
+ // fields are never mixed across collections. Reading `code` and `state`
289
+ // independently would accept `?state=<expected>#code=<other>` — one
290
+ // response assembled from two sources — which is exactly the confusion a
291
+ // state check exists to prevent. An authorization response arrives in one
292
+ // place; treat it that way.
293
+ //
294
+ // Only `code` and `state` are read — never a token. This repo does not
295
+ // implement the implicit grant and a paste field must not become the place
296
+ // it appears.
297
+ const fragment = new URLSearchParams(url.hash.replace(/^#/, ""));
298
+ const source = url.searchParams.has("code") ? url.searchParams : fragment;
279
299
  return {
280
300
  kind: "url",
281
- code: url.searchParams.get("code") ?? undefined,
282
- state: url.searchParams.get("state") ?? undefined,
301
+ code: source.get("code") ?? undefined,
302
+ state: source.get("state") ?? undefined,
283
303
  };
284
304
  } catch {
285
305
  // Not a URL - check for query string format
@@ -0,0 +1,203 @@
1
+ /**
2
+ * Cline OAuth — automatic session import from local Cline CLI / VS Code extension
3
+ * and background WorkOS token refresh.
4
+ *
5
+ * Security: tokens are read securely from local config files and refreshed directly
6
+ * against WorkOS API (api.workos.com). Tokens and credentials are never logged.
7
+ */
8
+ import { existsSync, readFileSync } from "node:fs";
9
+ import { clineConfigPath } from "../clients/config-export";
10
+ import type { OAuthController, OAuthCredentials } from "./types";
11
+
12
+ export const WORKOS_CLIENT_ID = "client_01K3A541FN8TA3EPPHTD2325AR";
13
+ export const WORKOS_AUTH_URL = "https://api.workos.com/user_management/authenticate";
14
+ const WORKOS_TOKEN_PREFIX = "workos:";
15
+
16
+ export interface ClineLoginOptions {
17
+ importLocal?: "fallback" | "off";
18
+ forceLogin?: boolean;
19
+ }
20
+
21
+ interface ClineProvidersJson {
22
+ providers?: {
23
+ cline?: {
24
+ settings?: {
25
+ provider?: string;
26
+ auth?: {
27
+ accessToken?: unknown;
28
+ refreshToken?: unknown;
29
+ expiresAt?: unknown;
30
+ accountId?: unknown;
31
+ metadata?: {
32
+ userInfo?: {
33
+ email?: unknown;
34
+ name?: unknown;
35
+ subject?: unknown;
36
+ clineUserId?: unknown;
37
+ };
38
+ };
39
+ };
40
+ };
41
+ };
42
+ };
43
+ }
44
+
45
+ interface WorkOSAuthResponse {
46
+ access_token?: unknown;
47
+ refresh_token?: unknown;
48
+ user?: {
49
+ id?: unknown;
50
+ external_id?: unknown;
51
+ email?: unknown;
52
+ first_name?: unknown;
53
+ last_name?: unknown;
54
+ };
55
+ error?: unknown;
56
+ error_description?: unknown;
57
+ }
58
+
59
+ function decodeJwtExpiry(token: string): number {
60
+ try {
61
+ const rawJwt = token.startsWith(WORKOS_TOKEN_PREFIX) ? token.slice(WORKOS_TOKEN_PREFIX.length) : token;
62
+ const parts = rawJwt.split(".");
63
+ if (parts.length === 3 && parts[1]) {
64
+ const payload = JSON.parse(Buffer.from(parts[1], "base64url").toString("utf-8")) as { exp?: unknown };
65
+ if (typeof payload.exp === "number" && Number.isFinite(payload.exp)) {
66
+ return payload.exp * 1000;
67
+ }
68
+ }
69
+ } catch {
70
+ // fallback below
71
+ }
72
+ return Date.now() + 3600_000;
73
+ }
74
+
75
+ export function formatClineAccessToken(token: string): string {
76
+ return token.startsWith(WORKOS_TOKEN_PREFIX) ? token : `${WORKOS_TOKEN_PREFIX}${token}`;
77
+ }
78
+
79
+ export function detectLocalClineToken(): OAuthCredentials | null {
80
+ const configPath = clineConfigPath();
81
+ if (!existsSync(configPath)) return null;
82
+
83
+ try {
84
+ const raw = JSON.parse(readFileSync(configPath, "utf8")) as ClineProvidersJson;
85
+ const auth = raw.providers?.cline?.settings?.auth;
86
+ if (!auth || typeof auth.accessToken !== "string" || typeof auth.refreshToken !== "string") {
87
+ return null;
88
+ }
89
+
90
+ const accessToken = auth.accessToken.trim();
91
+ const refreshToken = auth.refreshToken.trim();
92
+ if (!accessToken || !refreshToken) return null;
93
+
94
+ const expiresAt = typeof auth.expiresAt === "number" && Number.isFinite(auth.expiresAt)
95
+ ? auth.expiresAt
96
+ : decodeJwtExpiry(accessToken);
97
+
98
+ const userInfo = auth.metadata?.userInfo;
99
+ const accountId = typeof auth.accountId === "string" && auth.accountId.length > 0
100
+ ? auth.accountId
101
+ : typeof userInfo?.clineUserId === "string" && userInfo.clineUserId.length > 0
102
+ ? userInfo.clineUserId
103
+ : typeof userInfo?.subject === "string" && userInfo.subject.length > 0
104
+ ? userInfo.subject
105
+ : undefined;
106
+
107
+ const email = typeof userInfo?.email === "string" && userInfo.email.length > 0
108
+ ? userInfo.email
109
+ : undefined;
110
+
111
+ return {
112
+ access: formatClineAccessToken(accessToken),
113
+ refresh: refreshToken,
114
+ expires: expiresAt,
115
+ ...(accountId ? { accountId } : {}),
116
+ ...(email ? { email } : {}),
117
+ source: "local-cli",
118
+ };
119
+ } catch {
120
+ return null;
121
+ }
122
+ }
123
+
124
+ export async function refreshClineToken(
125
+ refreshToken: string,
126
+ signal?: AbortSignal,
127
+ credential?: OAuthCredentials,
128
+ ): Promise<OAuthCredentials> {
129
+ if (signal?.aborted) {
130
+ throw signal.reason ?? new DOMException("Cline token refresh aborted", "AbortError");
131
+ }
132
+
133
+ const response = await fetch(WORKOS_AUTH_URL, {
134
+ method: "POST",
135
+ headers: {
136
+ "Content-Type": "application/json",
137
+ Accept: "application/json",
138
+ },
139
+ body: JSON.stringify({
140
+ client_id: WORKOS_CLIENT_ID,
141
+ grant_type: "refresh_token",
142
+ refresh_token: refreshToken,
143
+ }),
144
+ signal,
145
+ });
146
+
147
+ if (!response.ok) {
148
+ const body = await response.text().catch(() => "");
149
+ throw new Error(`Cline WorkOS token refresh failed (HTTP ${response.status}): ${body || response.statusText}`);
150
+ }
151
+
152
+ const data = (await response.json()) as WorkOSAuthResponse;
153
+ if (typeof data.access_token !== "string" || !data.access_token) {
154
+ throw new Error(`Cline token refresh returned no access_token: ${JSON.stringify(data)}`);
155
+ }
156
+
157
+ const nextRefreshToken = typeof data.refresh_token === "string" && data.refresh_token
158
+ ? data.refresh_token
159
+ : refreshToken;
160
+
161
+ const access = formatClineAccessToken(data.access_token);
162
+ const expires = decodeJwtExpiry(data.access_token);
163
+
164
+ const accountId = typeof data.user?.external_id === "string" && data.user.external_id
165
+ ? data.user.external_id
166
+ : typeof data.user?.id === "string" && data.user.id
167
+ ? data.user.id
168
+ : credential?.accountId;
169
+
170
+ const email = typeof data.user?.email === "string" && data.user.email
171
+ ? data.user.email
172
+ : credential?.email;
173
+
174
+ return {
175
+ access,
176
+ refresh: nextRefreshToken,
177
+ expires,
178
+ ...(accountId ? { accountId } : {}),
179
+ ...(email ? { email } : {}),
180
+ };
181
+ }
182
+
183
+ export async function loginCline(
184
+ ctrl?: OAuthController,
185
+ options: ClineLoginOptions = {},
186
+ ): Promise<OAuthCredentials> {
187
+ if (options.importLocal !== "off") {
188
+ ctrl?.onProgress?.("Checking local Cline CLI and VS Code installation for active session...");
189
+ const local = detectLocalClineToken();
190
+ if (local) {
191
+ if (local.expires <= Date.now() + 60_000) {
192
+ ctrl?.onProgress?.("Local Cline session token expired; refreshing via WorkOS...");
193
+ return await refreshClineToken(local.refresh, ctrl?.signal, local);
194
+ }
195
+ ctrl?.onProgress?.("Imported active Cline session successfully.");
196
+ return local;
197
+ }
198
+ }
199
+
200
+ throw new Error(
201
+ "No local Cline session found. Please log in to Cline via the VS Code extension or Cline CLI (`cline`), then re-run `ocx login cline` to import your account.",
202
+ );
203
+ }