@iislee/opencodex 2.11.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 (733) hide show
  1. package/AGENTS_INSTALL.md +109 -0
  2. package/README.md +117 -22
  3. package/bin/ocx.mjs +245 -88
  4. package/bin/package-main.mjs +1 -1
  5. package/gui/dist/assets/index-Be1sDpWw.css +1 -0
  6. package/gui/dist/assets/index-D4OKgUV4.js +114 -0
  7. package/gui/dist/index.html +2 -2
  8. package/gui/dist/provider-icons/alibaba-color.svg +1 -1
  9. package/gui/dist/provider-icons/antigravity-color.svg +1 -1
  10. package/gui/dist/provider-icons/claude-color.svg +1 -1
  11. package/gui/dist/provider-icons/cline-color.svg +16 -0
  12. package/gui/dist/provider-icons/cloudflare-ai-gateway-color.svg +1 -1
  13. package/gui/dist/provider-icons/commandcode-color.svg +1 -0
  14. package/gui/dist/provider-icons/copilot-color.svg +1 -1
  15. package/gui/dist/provider-icons/cursor-color.svg +1 -1
  16. package/gui/dist/provider-icons/deepseek-color.svg +1 -1
  17. package/gui/dist/provider-icons/firepass-color.svg +1 -1
  18. package/gui/dist/provider-icons/fireworks-color.svg +1 -1
  19. package/gui/dist/provider-icons/gemini-color.svg +1 -1
  20. package/gui/dist/provider-icons/github-copilot-color.svg +1 -1
  21. package/gui/dist/provider-icons/gitlab-duo-color.svg +1 -1
  22. package/gui/dist/provider-icons/grok.svg +1 -1
  23. package/gui/dist/provider-icons/groq-color.svg +1 -1
  24. package/gui/dist/provider-icons/huggingface-color.svg +1 -1
  25. package/gui/dist/provider-icons/kimi-color.svg +1 -1
  26. package/gui/dist/provider-icons/kiro-color.svg +2 -2
  27. package/gui/dist/provider-icons/lm-studio-color.svg +1 -1
  28. package/gui/dist/provider-icons/mistral-color.svg +1 -1
  29. package/gui/dist/provider-icons/moonshot-color.svg +1 -1
  30. package/gui/dist/provider-icons/nvidia-color.svg +1 -1
  31. package/gui/dist/provider-icons/ollama-color.svg +1 -1
  32. package/gui/dist/provider-icons/openai.svg +1 -1
  33. package/gui/dist/provider-icons/opencode.svg +2 -1
  34. package/gui/dist/provider-icons/openrouter-color.svg +1 -1
  35. package/gui/dist/provider-icons/pi.svg +2 -2
  36. package/gui/dist/provider-icons/qianfan-color.svg +1 -1
  37. package/gui/dist/provider-icons/qwen-portal-color.svg +1 -1
  38. package/gui/dist/provider-icons/vercel-ai-gateway-color.svg +1 -1
  39. package/gui/dist/provider-icons/vllm-color.svg +1 -1
  40. package/gui/dist/provider-icons/xiaomi-color.svg +1 -1
  41. package/package.json +21 -10
  42. package/src/adapters/anthropic-output-schema.ts +137 -0
  43. package/src/adapters/anthropic.ts +475 -62
  44. package/src/adapters/base.ts +82 -7
  45. package/src/adapters/client-fingerprint.ts +18 -12
  46. package/src/adapters/cline-pass-deepseek-v4-tool-replay.ts +69 -0
  47. package/src/adapters/command-code.ts +637 -0
  48. package/src/adapters/cursor/call-id.ts +44 -0
  49. package/src/adapters/cursor/catalog.ts +541 -0
  50. package/src/adapters/cursor/checkpoint-store.ts +308 -0
  51. package/src/adapters/cursor/cursor-errors.ts +144 -6
  52. package/src/adapters/cursor/discovery.ts +122 -14
  53. package/src/adapters/cursor/effort-map.ts +106 -4
  54. package/src/adapters/cursor/envelope-echo.ts +290 -0
  55. package/src/adapters/cursor/framing.ts +39 -0
  56. package/src/adapters/cursor/h2-pool.ts +123 -0
  57. package/src/adapters/cursor/http1-bidi.ts +361 -0
  58. package/src/adapters/cursor/images.ts +704 -0
  59. package/src/adapters/cursor/live-models.ts +180 -59
  60. package/src/adapters/cursor/live-transport.ts +623 -170
  61. package/src/adapters/cursor/message-mapper.ts +4 -1
  62. package/src/adapters/cursor/native-exec-common.ts +23 -2
  63. package/src/adapters/cursor/native-exec-desktop.ts +23 -0
  64. package/src/adapters/cursor/native-exec-fs.ts +10 -7
  65. package/src/adapters/cursor/native-exec-network.ts +1 -1
  66. package/src/adapters/cursor/native-exec-shell.ts +5 -3
  67. package/src/adapters/cursor/native-exec.ts +121 -14
  68. package/src/adapters/cursor/protobuf-events.ts +829 -11
  69. package/src/adapters/cursor/protobuf-request.ts +447 -75
  70. package/src/adapters/cursor/request-builder.ts +271 -35
  71. package/src/adapters/cursor/tool-definitions.ts +252 -12
  72. package/src/adapters/cursor/tool-result-normalize.ts +115 -0
  73. package/src/adapters/cursor/transport.ts +22 -0
  74. package/src/adapters/cursor/types.ts +48 -1
  75. package/src/adapters/cursor.ts +336 -39
  76. package/src/adapters/exec-tool-result-normalize.ts +99 -0
  77. package/src/adapters/google-antigravity-replay.ts +713 -41
  78. package/src/adapters/google-antigravity-wire.ts +48 -10
  79. package/src/adapters/google-errors.ts +44 -12
  80. package/src/adapters/google-http.ts +38 -10
  81. package/src/adapters/google-tool-schema.ts +80 -15
  82. package/src/adapters/google-truncation.ts +11 -0
  83. package/src/adapters/google.ts +742 -86
  84. package/src/adapters/identity.ts +39 -6
  85. package/src/adapters/image.ts +1 -1
  86. package/src/adapters/kiro-constants.ts +27 -0
  87. package/src/adapters/kiro-errors.ts +11 -0
  88. package/src/adapters/kiro-events.ts +19 -1
  89. package/src/adapters/kiro-thinking.ts +18 -2
  90. package/src/adapters/kiro-tools.ts +65 -17
  91. package/src/adapters/kiro.ts +270 -73
  92. package/src/adapters/mimo-free.ts +17 -0
  93. package/src/adapters/openai-chat-url.ts +11 -0
  94. package/src/adapters/openai-chat.ts +1377 -344
  95. package/src/adapters/openai-responses-url.ts +16 -0
  96. package/src/adapters/openai-responses.ts +1164 -64
  97. package/src/adapters/registry.ts +175 -0
  98. package/src/adapters/responses-tool-schema.ts +67 -0
  99. package/src/adapters/run-turn-queue.ts +36 -1
  100. package/src/adapters/tool-call-id.ts +119 -0
  101. package/src/adapters/tool-catalog-nudge.ts +105 -21
  102. package/src/adapters/xai-tool-schema.ts +436 -0
  103. package/src/adapters/xai-web-search.ts +186 -0
  104. package/src/bridge.ts +512 -92
  105. package/src/chat/inbound.ts +57 -20
  106. package/src/chat/outbound.ts +97 -33
  107. package/src/claude/agents-inject.ts +40 -10
  108. package/src/claude/context-windows.ts +37 -14
  109. package/src/claude/desktop-3p.ts +243 -9
  110. package/src/claude/gateway-cache.ts +41 -4
  111. package/src/claude/inbound.ts +72 -3
  112. package/src/claude/model-info.ts +38 -15
  113. package/src/claude/outbound.ts +88 -21
  114. package/src/cli/access.ts +46 -3
  115. package/src/cli/account-api.ts +150 -19
  116. package/src/cli/account-auth.ts +33 -6
  117. package/src/cli/account-catalog-refresh.ts +14 -0
  118. package/src/cli/account-extended.ts +648 -28
  119. package/src/cli/account-main.ts +317 -0
  120. package/src/cli/account.ts +97 -19
  121. package/src/cli/agent.ts +53 -2
  122. package/src/cli/alias.ts +66 -0
  123. package/src/cli/capabilities-command.ts +94 -0
  124. package/src/cli/capabilities.ts +496 -0
  125. package/src/cli/claude-agent-startup-sync.ts +73 -0
  126. package/src/cli/claude-desktop.ts +60 -15
  127. package/src/cli/claude.ts +129 -35
  128. package/src/cli/codex-log-guard-doctor.ts +103 -0
  129. package/src/cli/codex-shim-autorestore.ts +2 -0
  130. package/src/cli/codex-shim-readiness.ts +76 -0
  131. package/src/cli/combo.ts +8 -0
  132. package/src/cli/config-command.ts +74 -10
  133. package/src/cli/dispatch.ts +821 -0
  134. package/src/cli/doctor.ts +440 -56
  135. package/src/cli/ensure-desired-integrations.ts +152 -0
  136. package/src/cli/export-command.ts +46 -20
  137. package/src/cli/help.ts +34 -274
  138. package/src/cli/index.ts +417 -528
  139. package/src/cli/init.ts +5 -17
  140. package/src/cli/inspect.ts +230 -0
  141. package/src/cli/integrations.ts +120 -2
  142. package/src/cli/lab.ts +607 -0
  143. package/src/cli/launcher-context.ts +77 -0
  144. package/src/cli/minimax.ts +497 -0
  145. package/src/cli/models-runtime.ts +130 -2
  146. package/src/cli/models.ts +118 -16
  147. package/src/cli/observe.ts +123 -11
  148. package/src/cli/opencode.ts +4 -2
  149. package/src/cli/provider-runtime.ts +34 -3
  150. package/src/cli/provider.ts +25 -3
  151. package/src/cli/ready.ts +301 -0
  152. package/src/cli/registry.ts +476 -0
  153. package/src/cli/root.ts +86 -0
  154. package/src/cli/route-policy.ts +92 -0
  155. package/src/cli/runtime-api.ts +57 -10
  156. package/src/cli/star-prompt.ts +71 -15
  157. package/src/cli/status.ts +51 -4
  158. package/src/cli/storage.ts +234 -0
  159. package/src/cli/system-command.ts +16 -0
  160. package/src/cli/system-restart-client.ts +146 -0
  161. package/src/cli/tray-proxy.ts +153 -6
  162. package/src/cli/usage-report.ts +184 -0
  163. package/src/cli/v2.ts +137 -18
  164. package/src/cli/version-skew.ts +46 -0
  165. package/src/cli.ts +1 -1
  166. package/src/clients/config-export.ts +1442 -23
  167. package/src/clients/effective-status.ts +30 -2
  168. package/src/clients/probes/cline.ts +81 -0
  169. package/src/codex/account-label.ts +35 -1
  170. package/src/codex/account-lifecycle.ts +130 -13
  171. package/src/codex/account-namespaces.ts +49 -3
  172. package/src/codex/account-pause.ts +2 -1
  173. package/src/codex/account-priority.ts +84 -0
  174. package/src/codex/account-store.ts +29 -2
  175. package/src/codex/account-usability.ts +25 -2
  176. package/src/codex/admission.ts +256 -0
  177. package/src/codex/affinity-debug.ts +162 -0
  178. package/src/codex/app-server-processes.ts +570 -107
  179. package/src/codex/app-server-restart-service.ts +232 -0
  180. package/src/codex/auth-api.ts +893 -246
  181. package/src/codex/auth-collision.ts +5 -3
  182. package/src/codex/auth-context.ts +348 -32
  183. package/src/codex/autostart-health.ts +24 -1
  184. package/src/codex/catalog/account-models.ts +67 -0
  185. package/src/codex/catalog/aggregation.ts +86 -10
  186. package/src/codex/catalog/bundled.ts +331 -33
  187. package/src/codex/catalog/effort.ts +138 -32
  188. package/src/codex/catalog/filesystem-evidence.ts +302 -0
  189. package/src/codex/catalog/kinds.ts +2 -0
  190. package/src/codex/catalog/metadata.ts +555 -45
  191. package/src/codex/catalog/native-models.ts +75 -0
  192. package/src/codex/catalog/parsing.ts +269 -30
  193. package/src/codex/catalog/provider-fetch.ts +1626 -144
  194. package/src/codex/catalog/sync.ts +1576 -187
  195. package/src/codex/catalog-admission.ts +199 -0
  196. package/src/codex/catalog-refresh-status.ts +105 -0
  197. package/src/codex/catalog-write-serialization.ts +242 -0
  198. package/src/codex/catalog.ts +6 -3
  199. package/src/codex/codex-write-lock.ts +386 -0
  200. package/src/codex/convergence-types.ts +614 -0
  201. package/src/codex/convergence.ts +676 -0
  202. package/src/codex/coordinator-doctor.ts +332 -0
  203. package/src/codex/custom-model-catalog-migration.ts +176 -0
  204. package/src/codex/desired-state.ts +230 -0
  205. package/src/codex/desktop-app-restart.ts +355 -0
  206. package/src/codex/features.ts +636 -39
  207. package/src/codex/generation.ts +202 -0
  208. package/src/codex/history-job.ts +436 -0
  209. package/src/codex/history-lock.ts +242 -0
  210. package/src/codex/history-manifest.ts +112 -0
  211. package/src/codex/history-migration-guardian.ts +30 -24
  212. package/src/codex/history-provider.ts +1016 -235
  213. package/src/codex/history-transition.ts +105 -0
  214. package/src/codex/history-worker.ts +223 -0
  215. package/src/codex/inject-coordination.ts +373 -0
  216. package/src/codex/inject.ts +1114 -152
  217. package/src/codex/injected-marker.ts +37 -3
  218. package/src/codex/integration-record.ts +266 -0
  219. package/src/codex/internal/catalog-writer.ts +203 -0
  220. package/src/codex/internal/history-writer.ts +81 -0
  221. package/src/codex/journal.ts +66 -4
  222. package/src/codex/log-guard/inspect.ts +524 -0
  223. package/src/codex/log-guard/lock.ts +150 -0
  224. package/src/codex/log-guard/maintenance.ts +403 -0
  225. package/src/codex/log-guard/path-safety.ts +88 -0
  226. package/src/codex/log-guard/policy.ts +44 -0
  227. package/src/codex/log-guard/processes.ts +205 -0
  228. package/src/codex/log-guard/protection.ts +489 -0
  229. package/src/codex/log-guard/sqlite-errors.ts +9 -0
  230. package/src/codex/main-account-cache.ts +24 -0
  231. package/src/codex/main-account.ts +29 -1
  232. package/src/codex/management-convergence.ts +167 -0
  233. package/src/codex/model-cache.ts +56 -10
  234. package/src/codex/model-entitlements.ts +360 -0
  235. package/src/codex/native-main-admission.ts +47 -0
  236. package/src/codex/native-main-auth-temp.ts +187 -0
  237. package/src/codex/native-main-claim.ts +178 -0
  238. package/src/codex/native-main-lock-file.ts +162 -0
  239. package/src/codex/native-main-owner.ts +329 -0
  240. package/src/codex/native-profile-api.ts +247 -0
  241. package/src/codex/native-profile-manager.ts +1531 -0
  242. package/src/codex/native-profile-processes.ts +121 -0
  243. package/src/codex/native-profile-recovery.ts +99 -0
  244. package/src/codex/native-profile-stage-store.ts +387 -0
  245. package/src/codex/native-profile-startup.ts +622 -0
  246. package/src/codex/native-profile-store.ts +855 -0
  247. package/src/codex/native-profile-types.ts +120 -0
  248. package/src/codex/native-residue.ts +675 -0
  249. package/src/codex/paths.ts +80 -1
  250. package/src/codex/plan-from-token.ts +140 -0
  251. package/src/codex/plan.ts +40 -0
  252. package/src/codex/plugins-doctor.ts +1 -1
  253. package/src/codex/pool-rotation.ts +74 -4
  254. package/src/codex/project-config-warnings.ts +20 -6
  255. package/src/codex/prompt-journal.ts +352 -0
  256. package/src/codex/prompt-layers.ts +1313 -0
  257. package/src/codex/prompt-lock.ts +143 -0
  258. package/src/codex/prompt-text-probe.ts +238 -0
  259. package/src/codex/quota-rejection.ts +298 -0
  260. package/src/codex/quota.ts +264 -21
  261. package/src/codex/refresh.ts +11 -2
  262. package/src/codex/reset-credit-recovery.ts +1044 -0
  263. package/src/codex/routing.ts +514 -94
  264. package/src/codex/runtime.ts +159 -38
  265. package/src/codex/shim.ts +1065 -31
  266. package/src/codex/subagent-model-fallback.ts +437 -43
  267. package/src/codex/sync.ts +191 -2
  268. package/src/codex/transition-state.ts +720 -0
  269. package/src/codex/upstream-host-health.ts +368 -0
  270. package/src/codex/user-identity.ts +557 -0
  271. package/src/codex/warmup.ts +187 -81
  272. package/src/codex/write-coordination.ts +114 -0
  273. package/src/combos/failover.ts +47 -0
  274. package/src/combos/index.ts +4 -0
  275. package/src/combos/request.ts +32 -0
  276. package/src/combos/types.ts +91 -9
  277. package/src/compatibility/index.ts +26 -0
  278. package/src/compatibility/manifest.ts +253 -0
  279. package/src/compatibility/openai-responses.ts +121 -0
  280. package/src/config/atomic-write.ts +219 -0
  281. package/src/config/paths.ts +40 -0
  282. package/src/config/process-state.ts +309 -0
  283. package/src/config/provider-name.ts +24 -0
  284. package/src/config/provider-validation.ts +177 -0
  285. package/src/config/rebase-provenance.ts +68 -0
  286. package/src/config.ts +1713 -709
  287. package/src/generated/compatibility-version.json +3324 -0
  288. package/src/generated/{jawcode-model-metadata.ts → model-metadata.ts} +19 -17
  289. package/src/grok/inject.ts +16 -5
  290. package/src/grok/inspect.ts +45 -0
  291. package/src/grok/sync.ts +2 -2
  292. package/src/images/loop.ts +157 -33
  293. package/src/images/plan.ts +24 -13
  294. package/src/integrations/config-io.ts +269 -0
  295. package/src/integrations/journal.ts +315 -0
  296. package/src/integrations/merge.ts +135 -0
  297. package/src/integrations/mutation-flight.ts +71 -0
  298. package/src/integrations/native/ownership-preflight.ts +202 -0
  299. package/src/integrations/omp-yaml-source.ts +358 -0
  300. package/src/integrations/owned-refresh.ts +74 -0
  301. package/src/integrations/ownership-policy.ts +160 -0
  302. package/src/integrations/ownership.ts +155 -0
  303. package/src/integrations/registry.ts +166 -0
  304. package/src/integrations/serialize.ts +314 -0
  305. package/src/integrations/state.ts +433 -0
  306. package/src/integrations/store.ts +103 -0
  307. package/src/integrations/writer-lock.ts +98 -0
  308. package/src/integrations/writer.ts +715 -0
  309. package/src/lab/artifacts/sanitize.ts +586 -0
  310. package/src/lab/artifacts/secure-fs.ts +475 -0
  311. package/src/lab/artifacts/store.ts +310 -0
  312. package/src/lab/automation/budgets.ts +78 -0
  313. package/src/lab/automation/config-persistence.ts +256 -0
  314. package/src/lab/automation/constants.ts +39 -0
  315. package/src/lab/automation/cooldown.ts +103 -0
  316. package/src/lab/automation/dispatch.ts +211 -0
  317. package/src/lab/automation/index.ts +13 -0
  318. package/src/lab/automation/orchestrator.ts +499 -0
  319. package/src/lab/automation/persistence.ts +512 -0
  320. package/src/lab/automation/planner.ts +371 -0
  321. package/src/lab/automation/policy.ts +136 -0
  322. package/src/lab/automation/queue.ts +191 -0
  323. package/src/lab/automation/recovery.ts +24 -0
  324. package/src/lab/automation/route-context.ts +21 -0
  325. package/src/lab/automation/run-key.ts +44 -0
  326. package/src/lab/automation/runs-query.ts +34 -0
  327. package/src/lab/automation/types.ts +160 -0
  328. package/src/lab/conformance/assertion.ts +325 -0
  329. package/src/lab/conformance/digest.ts +22 -0
  330. package/src/lab/conformance/executor.ts +741 -0
  331. package/src/lab/conformance/fixture-provider.ts +27 -0
  332. package/src/lab/conformance/fixtures/live-v1-cases.json +175 -0
  333. package/src/lab/conformance/fixtures/protocol-v1-cases.json +461 -0
  334. package/src/lab/conformance/harness-budget.ts +47 -0
  335. package/src/lab/conformance/index.ts +5 -0
  336. package/src/lab/conformance/jcs.ts +64 -0
  337. package/src/lab/conformance/json-pointer.ts +39 -0
  338. package/src/lab/conformance/manifest.ts +180 -0
  339. package/src/lab/conformance/mcp-stub.ts +179 -0
  340. package/src/lab/conformance/negative-controls.ts +164 -0
  341. package/src/lab/conformance/observation.ts +355 -0
  342. package/src/lab/conformance/runner.ts +68 -0
  343. package/src/lab/conformance/sse-normalize.ts +59 -0
  344. package/src/lab/conformance/suite-manifest.ts +78 -0
  345. package/src/lab/conformance/types.ts +214 -0
  346. package/src/lab/constants.ts +126 -0
  347. package/src/lab/digest.ts +64 -0
  348. package/src/lab/events/errors.ts +9 -0
  349. package/src/lab/events/limits.ts +117 -0
  350. package/src/lab/events/types.ts +229 -0
  351. package/src/lab/events/validate.ts +781 -0
  352. package/src/lab/fabric/constants.ts +40 -0
  353. package/src/lab/fabric/executor.ts +492 -0
  354. package/src/lab/fabric/index.ts +80 -0
  355. package/src/lab/fabric/manifest.ts +222 -0
  356. package/src/lab/fabric/observe.ts +489 -0
  357. package/src/lab/fabric/patch.ts +79 -0
  358. package/src/lab/fabric/producer-child.ts +139 -0
  359. package/src/lab/fabric/producer-isolate.ts +276 -0
  360. package/src/lab/fabric/producer-protocol.ts +61 -0
  361. package/src/lab/fabric/scratch.ts +439 -0
  362. package/src/lab/fabric/subject.ts +106 -0
  363. package/src/lab/fabric/types.ts +134 -0
  364. package/src/lab/fabric/verifier.ts +98 -0
  365. package/src/lab/index.ts +54 -0
  366. package/src/lab/ledger/artifact-refs.ts +127 -0
  367. package/src/lab/ledger/invalidation.ts +136 -0
  368. package/src/lab/ledger/purge.ts +310 -0
  369. package/src/lab/ledger/store.ts +532 -0
  370. package/src/lab/live/credential-lease.ts +53 -0
  371. package/src/lab/live/destination.ts +155 -0
  372. package/src/lab/live/executor.ts +336 -0
  373. package/src/lab/live/inert-tools.ts +56 -0
  374. package/src/lab/live/manifest.ts +85 -0
  375. package/src/lab/live/mcp-loopback.ts +57 -0
  376. package/src/lab/live/runner.ts +19 -0
  377. package/src/lab/live/sandbox.ts +61 -0
  378. package/src/lab/live/suite-manifest.ts +41 -0
  379. package/src/lab/live/transport.ts +118 -0
  380. package/src/lab/live/types.ts +197 -0
  381. package/src/lab/observe/from-conformance.ts +301 -0
  382. package/src/lab/observe/from-live.ts +117 -0
  383. package/src/lab/paths.ts +153 -0
  384. package/src/lab/projection/rebuild.ts +495 -0
  385. package/src/lab/projection/schema.ts +135 -0
  386. package/src/lab/projection/verdicts.ts +474 -0
  387. package/src/lab/projection/verification.ts +412 -0
  388. package/src/lab/public/bundle.ts +217 -0
  389. package/src/lab/public/community-authority.ts +175 -0
  390. package/src/lab/public/community-files.ts +29 -0
  391. package/src/lab/public/community.ts +479 -0
  392. package/src/lab/public/file-safety.ts +155 -0
  393. package/src/lab/public/ids.ts +26 -0
  394. package/src/lab/public/index.ts +16 -0
  395. package/src/lab/public/mutation-lock.ts +424 -0
  396. package/src/lab/public/operator.ts +353 -0
  397. package/src/lab/public/origin-purge.ts +79 -0
  398. package/src/lab/public/origin.ts +203 -0
  399. package/src/lab/public/privacy.ts +143 -0
  400. package/src/lab/public/private-file.ts +261 -0
  401. package/src/lab/public/project.ts +124 -0
  402. package/src/lab/public/purge-test-fault.ts +21 -0
  403. package/src/lab/public/purge.ts +223 -0
  404. package/src/lab/public/registry.ts +44 -0
  405. package/src/lab/public/revocation.ts +252 -0
  406. package/src/lab/public/signature.ts +243 -0
  407. package/src/lab/public/storage.ts +105 -0
  408. package/src/lab/public/strict-json.ts +206 -0
  409. package/src/lab/public/time.ts +26 -0
  410. package/src/lab/public/types.ts +172 -0
  411. package/src/lab/public/validate.ts +391 -0
  412. package/src/lab/query/catalog.ts +101 -0
  413. package/src/lab/query/connection.ts +107 -0
  414. package/src/lab/query/constants.ts +4 -0
  415. package/src/lab/query/cursor.ts +132 -0
  416. package/src/lab/query/dto-map.ts +277 -0
  417. package/src/lab/query/errors.ts +22 -0
  418. package/src/lab/query/freshness.ts +53 -0
  419. package/src/lab/query/index.ts +45 -0
  420. package/src/lab/query/latest-observation.ts +59 -0
  421. package/src/lab/query/passive-production.ts +159 -0
  422. package/src/lab/query/queries.ts +444 -0
  423. package/src/lab/query/types.ts +266 -0
  424. package/src/lab/subject/behavior-fingerprint.ts +77 -0
  425. package/src/lab/subject/installation-salt.ts +112 -0
  426. package/src/lab/subject/protocol-subject.ts +80 -0
  427. package/src/lab/subject/route-subject.ts +74 -0
  428. package/src/lib/admin-secrets.ts +24 -0
  429. package/src/lib/app-owned-memory-stores.ts +22 -0
  430. package/src/lib/bounded-body.ts +167 -11
  431. package/src/lib/bun-runtime.ts +125 -12
  432. package/src/lib/bun-stream-caps.ts +13 -9
  433. package/src/lib/codex-restart-contract.ts +120 -0
  434. package/src/lib/config-ownership.ts +26 -2
  435. package/src/lib/destination-policy.ts +65 -1
  436. package/src/lib/errors.ts +80 -5
  437. package/src/lib/fabric-task-execution-authority.ts +7 -0
  438. package/src/lib/fabric-task-host.ts +29 -0
  439. package/src/lib/lab-activation.ts +223 -0
  440. package/src/lib/lab-live-execution-authority.ts +13 -0
  441. package/src/lib/lab-live-host.ts +30 -0
  442. package/src/lib/lab-live-pinned-sender.ts +56 -0
  443. package/src/lib/lab-live-route-production.ts +130 -0
  444. package/src/lib/lab-passive-linker-registration.ts +26 -0
  445. package/src/lib/local-management-attestation.ts +51 -0
  446. package/src/lib/local-management-capability.ts +100 -0
  447. package/src/lib/local-provider-reload-contract.ts +100 -0
  448. package/src/lib/optional-shutdown-hooks.ts +57 -0
  449. package/src/lib/package-tree-integrity.ts +101 -0
  450. package/src/lib/pinned-http.ts +145 -26
  451. package/src/lib/process-control.ts +6 -2
  452. package/src/lib/provider-outbound.ts +49 -9
  453. package/src/lib/redact.ts +419 -3
  454. package/src/lib/self-launch-argv.ts +15 -0
  455. package/src/lib/server-resource-ownership.ts +71 -0
  456. package/src/lib/service-secrets.ts +15 -0
  457. package/src/lib/shadow-call.ts +35 -4
  458. package/src/lib/sse-decoder.ts +41 -0
  459. package/src/lib/state-store-registrations.ts +10 -2
  460. package/src/lib/system-restart-contract.ts +73 -0
  461. package/src/lib/token-estimate.ts +19 -2
  462. package/src/lib/tool-argument-integers.ts +253 -0
  463. package/src/lib/translator-budget.ts +44 -0
  464. package/src/lib/upstream-http-version.ts +57 -0
  465. package/src/lib/upstream-reachability.ts +95 -0
  466. package/src/lib/upstream-retry.ts +156 -3
  467. package/src/lib/windows-atomic-replace.ts +155 -0
  468. package/src/lib/windows-elevation.ts +70 -2
  469. package/src/lib/windows-secret-acl.ts +409 -69
  470. package/src/lib/windows-service-wrappers.ts +72 -0
  471. package/src/lib/windows-text.ts +106 -0
  472. package/src/lib/windows-user-principal.ts +341 -0
  473. package/src/lib/winsw.ts +33 -5
  474. package/src/oauth/account-import/google-antigravity-adapter.ts +74 -0
  475. package/src/oauth/account-import/index.ts +15 -0
  476. package/src/oauth/account-import/parser.ts +83 -0
  477. package/src/oauth/account-import/registry.ts +18 -0
  478. package/src/oauth/account-import/service.ts +75 -0
  479. package/src/oauth/account-import/types.ts +91 -0
  480. package/src/oauth/anthropic.ts +12 -1
  481. package/src/oauth/callback-server.ts +30 -4
  482. package/src/oauth/chatgpt.ts +12 -1
  483. package/src/oauth/cline.ts +203 -0
  484. package/src/oauth/command-code.ts +239 -0
  485. package/src/oauth/cursor.ts +46 -5
  486. package/src/oauth/generic-account-failover.ts +231 -0
  487. package/src/oauth/google-antigravity.ts +35 -3
  488. package/src/oauth/health.ts +20 -12
  489. package/src/oauth/index.ts +462 -71
  490. package/src/oauth/key-providers.ts +25 -0
  491. package/src/oauth/kimi.ts +25 -3
  492. package/src/oauth/kiro.ts +95 -6
  493. package/src/oauth/local-token-detect.ts +13 -2
  494. package/src/oauth/log.ts +3 -1
  495. package/src/oauth/login-cli.ts +88 -28
  496. package/src/oauth/nous.ts +798 -0
  497. package/src/oauth/open-browser-choice.ts +26 -0
  498. package/src/oauth/store.ts +133 -26
  499. package/src/oauth/token-guardian.ts +9 -3
  500. package/src/oauth/types.ts +15 -0
  501. package/src/pi/models.ts +2 -2
  502. package/src/providers/alibaba-region-migration.ts +1 -1
  503. package/src/providers/antigravity-models.ts +521 -31
  504. package/src/providers/auto-compact-budget.ts +65 -0
  505. package/src/providers/base-url-choices.ts +10 -0
  506. package/src/providers/codex-capacity.ts +292 -0
  507. package/src/providers/command-code-efforts.ts +176 -0
  508. package/src/providers/context-cap.ts +26 -8
  509. package/src/providers/cursor-pool.ts +72 -0
  510. package/src/providers/default-aliases.ts +65 -0
  511. package/src/providers/derive.ts +281 -6
  512. package/src/providers/fastwire.ts +507 -0
  513. package/src/providers/free-directory.ts +10 -7
  514. package/src/providers/google-vertex-location.ts +14 -0
  515. package/src/providers/key-failover.ts +71 -3
  516. package/src/providers/label.ts +35 -2
  517. package/src/providers/model-discovery-limits.ts +16 -0
  518. package/src/providers/model-discovery.ts +115 -22
  519. package/src/providers/model-presets.ts +119 -0
  520. package/src/providers/model-rename-migration.ts +255 -0
  521. package/src/providers/model-rename-startup.ts +28 -0
  522. package/src/providers/new-model-policy.ts +146 -0
  523. package/src/providers/openai-sidecar.ts +72 -4
  524. package/src/providers/openai-tier-startup.ts +31 -2
  525. package/src/providers/openai-tiers.ts +119 -4
  526. package/src/providers/openai-virtual-models.ts +1 -0
  527. package/src/providers/opencode-zen-rate-limit.ts +102 -0
  528. package/src/providers/provider-id-rewrite.ts +30 -0
  529. package/src/providers/quota.ts +1379 -40
  530. package/src/providers/registry.ts +1545 -112
  531. package/src/providers/request-pacing.ts +310 -0
  532. package/src/providers/service-tier.ts +277 -0
  533. package/src/providers/slug-codec.ts +94 -6
  534. package/src/providers/static-model-discovery.ts +86 -0
  535. package/src/providers/xai-responses-opt-in.ts +15 -0
  536. package/src/providers/xai-transport.ts +32 -4
  537. package/src/reasoning-effort.ts +68 -3
  538. package/src/responses/apply-patch-envelope.ts +63 -0
  539. package/src/responses/code-mode-helper-compat.ts +50 -0
  540. package/src/responses/compaction.ts +26 -1
  541. package/src/responses/custom-tool-compat.ts +384 -0
  542. package/src/responses/hosted-tool-policy.ts +9 -0
  543. package/src/responses/namespace-tool-compat.ts +435 -0
  544. package/src/responses/parser.ts +225 -38
  545. package/src/responses/provider-continuation.ts +98 -0
  546. package/src/responses/provider-opaque-metadata.ts +73 -0
  547. package/src/responses/reasoning-envelope.ts +9 -1
  548. package/src/responses/reasoning-replay-cache.ts +504 -0
  549. package/src/responses/schema.ts +12 -2
  550. package/src/responses/spill-store.ts +75 -10
  551. package/src/responses/state.ts +659 -31
  552. package/src/responses/thought-signature-replay.ts +364 -0
  553. package/src/responses/tool-search-compat.ts +301 -0
  554. package/src/responses/truncated-stop-reason.ts +60 -0
  555. package/src/responses/turn-termination.ts +107 -0
  556. package/src/router.ts +405 -31
  557. package/src/routing/analytics.ts +379 -0
  558. package/src/routing/capability.ts +244 -0
  559. package/src/routing/compatibility/assemble.ts +73 -0
  560. package/src/routing/compatibility/behavior.ts +278 -0
  561. package/src/routing/compatibility/catalog.ts +99 -0
  562. package/src/routing/compatibility/endpoint.ts +52 -0
  563. package/src/routing/compatibility/lab-evidence-provider.ts +130 -0
  564. package/src/routing/compatibility/policy.ts +181 -0
  565. package/src/routing/compatibility/provider-slot.ts +56 -0
  566. package/src/routing/compatibility/reader.ts +110 -0
  567. package/src/routing/compatibility/subject.ts +191 -0
  568. package/src/routing/compatibility/types.ts +64 -0
  569. package/src/routing/compatibility/version.ts +104 -0
  570. package/src/routing/cost.ts +77 -0
  571. package/src/routing/evaluator.ts +495 -0
  572. package/src/routing/health.ts +412 -0
  573. package/src/routing/history/cursor.ts +43 -0
  574. package/src/routing/history/indexer.ts +605 -0
  575. package/src/routing/history/schema.ts +72 -0
  576. package/src/routing/profile-namespace.ts +15 -0
  577. package/src/routing/profile.ts +547 -0
  578. package/src/routing/quota.ts +155 -0
  579. package/src/routing/request-evidence.ts +45 -0
  580. package/src/routing/trace.ts +776 -0
  581. package/src/server/adapter-resolve.ts +2 -29
  582. package/src/server/auth-cors.ts +289 -48
  583. package/src/server/background-lifecycle.ts +182 -0
  584. package/src/server/chat-completions.ts +154 -72
  585. package/src/server/chat-native-sse.ts +331 -0
  586. package/src/server/chat-native.ts +445 -0
  587. package/src/server/claude-messages.ts +160 -44
  588. package/src/server/direct-local-http.ts +347 -0
  589. package/src/server/effort-policy.ts +18 -0
  590. package/src/server/github-copilot-responses-repair.ts +338 -0
  591. package/src/server/gui-static.ts +39 -10
  592. package/src/server/images.ts +94 -12
  593. package/src/server/index.ts +1021 -190
  594. package/src/server/lifecycle.ts +336 -14
  595. package/src/server/live.ts +136 -17
  596. package/src/server/local-management-read-client.ts +90 -0
  597. package/src/server/local-provider-reload-client.ts +137 -0
  598. package/src/server/management/agent-settings-routes.ts +433 -116
  599. package/src/server/management/api-key-usage.ts +31 -5
  600. package/src/server/management/body.ts +6 -0
  601. package/src/server/management/codex-prompt-routes.ts +570 -0
  602. package/src/server/management/combo-routes.ts +64 -25
  603. package/src/server/management/config-routes.ts +511 -51
  604. package/src/server/management/context.ts +89 -2
  605. package/src/server/management/integration-routes.ts +498 -0
  606. package/src/server/management/lab-automation-routes.ts +206 -0
  607. package/src/server/management/lab-routes.ts +563 -0
  608. package/src/server/management/logs-usage-routes.ts +127 -52
  609. package/src/server/management/model-routes.ts +461 -133
  610. package/src/server/management/model-rows.ts +167 -0
  611. package/src/server/management/native-integration-routes.ts +770 -0
  612. package/src/server/management/oauth-account-routes.ts +90 -8
  613. package/src/server/management/provider-capability-config.ts +48 -0
  614. package/src/server/management/provider-routes.ts +836 -159
  615. package/src/server/management/request-history-routes.ts +191 -0
  616. package/src/server/management/route-registry.ts +332 -0
  617. package/src/server/management/routing-analytics-routes.ts +74 -0
  618. package/src/server/management/routing-profile-routes.ts +380 -0
  619. package/src/server/management/shared.ts +27 -11
  620. package/src/server/management/sidebar-routes.ts +47 -31
  621. package/src/server/management/storage-log-guard-routes.ts +186 -0
  622. package/src/server/management/sync-response.ts +69 -0
  623. package/src/server/management/system-restart.ts +289 -33
  624. package/src/server/management/system-routes.ts +77 -0
  625. package/src/server/management/usage-summary-cache.ts +9 -1
  626. package/src/server/management/vision-sidecar-options.ts +167 -0
  627. package/src/server/management/web-search-sidecar-options.ts +120 -0
  628. package/src/server/management-api.ts +126 -23
  629. package/src/server/management-auth.ts +220 -5
  630. package/src/server/passive-route-linker.ts +66 -0
  631. package/src/server/port-reclaim.ts +1 -1
  632. package/src/server/ports.ts +41 -1
  633. package/src/server/proxy-liveness.ts +161 -10
  634. package/src/server/readiness.ts +99 -0
  635. package/src/server/relay-eager.ts +221 -54
  636. package/src/server/relay.ts +479 -87
  637. package/src/server/request-decompress.ts +113 -6
  638. package/src/server/request-log-conversation.ts +51 -0
  639. package/src/server/request-log.ts +298 -31
  640. package/src/server/responses/agent-task-recovery-cache.ts +143 -0
  641. package/src/server/responses/agent-task-recovery.ts +465 -0
  642. package/src/server/responses/codex-auth-error.ts +55 -0
  643. package/src/server/responses/collaboration.ts +239 -36
  644. package/src/server/responses/combo-stream-preflight.ts +178 -0
  645. package/src/server/responses/compact.ts +444 -72
  646. package/src/server/responses/core.ts +3579 -436
  647. package/src/server/responses/empty-completion-guard.ts +333 -0
  648. package/src/server/responses/encrypted-payload.ts +62 -39
  649. package/src/server/responses/fetch-helpers.ts +89 -99
  650. package/src/server/responses/input-admission.ts +187 -0
  651. package/src/server/responses/pacing-overload.ts +13 -0
  652. package/src/server/responses/passthrough-error.ts +33 -9
  653. package/src/server/responses/policy-fallback.ts +178 -0
  654. package/src/server/responses/responses-field-backfill.ts +343 -0
  655. package/src/server/responses/terminal-guard.ts +26 -5
  656. package/src/server/responses/upstream-error.ts +5 -0
  657. package/src/server/responses/ws-upstream.ts +381 -0
  658. package/src/server/responses-custom-tool-repair.ts +363 -0
  659. package/src/server/responses-item-id-repair.ts +54 -6
  660. package/src/server/responses-json-events.ts +90 -0
  661. package/src/server/responses-model-rewrite.ts +29 -0
  662. package/src/server/responses-reasoning-summary-rewrite.ts +178 -0
  663. package/src/server/responses-snapshot-repair.ts +621 -0
  664. package/src/server/responses-terminal-repair.ts +363 -0
  665. package/src/server/responses-tool-search-repair.ts +267 -0
  666. package/src/server/responses-undeclared-tool-guard.ts +376 -0
  667. package/src/server/responses.ts +18 -2
  668. package/src/server/search.ts +78 -13
  669. package/src/server/sse-frame-buffer.ts +319 -0
  670. package/src/server/sse-payload-rewrite.ts +110 -22
  671. package/src/server/startup-action-control.ts +8 -1
  672. package/src/server/startup-health-cache.ts +19 -1
  673. package/src/server/system-env.ts +80 -9
  674. package/src/server/ws-bridge.ts +53 -40
  675. package/src/service-manager-probe.ts +892 -0
  676. package/src/service.ts +1153 -94
  677. package/src/sidecar/auth.ts +92 -0
  678. package/src/sidecar/candidates.ts +83 -0
  679. package/src/storage/cleanup.ts +2 -2
  680. package/src/storage/policy-job.ts +14 -4
  681. package/src/storage/policy.ts +88 -23
  682. package/src/storage/scanner.ts +1 -1
  683. package/src/storage/worker-lifecycle.ts +14 -14
  684. package/src/tray/windows-tray.ps1 +83 -9
  685. package/src/tray/windows.ts +43 -16
  686. package/src/types/accounts.ts +37 -0
  687. package/src/types/config.ts +911 -0
  688. package/src/types/provider.ts +614 -0
  689. package/src/types/request.ts +397 -0
  690. package/src/types/tools.ts +236 -0
  691. package/src/types/wire.ts +80 -0
  692. package/src/types.ts +107 -1237
  693. package/src/update/index.ts +37 -23
  694. package/src/update/job.ts +445 -68
  695. package/src/update/notify.ts +12 -6
  696. package/src/update/npm-cache-preflight.d.mts +47 -0
  697. package/src/update/npm-cache-preflight.mjs +201 -0
  698. package/src/update/transactional-install.d.mts +22 -0
  699. package/src/update/transactional-install.mjs +266 -0
  700. package/src/usage/cost.ts +0 -0
  701. package/src/usage/expected-prices.ts +287 -16
  702. package/src/usage/log.ts +666 -50
  703. package/src/usage/summary.ts +388 -17
  704. package/src/usage/user-cost-overlay-reconciler.ts +313 -0
  705. package/src/usage/user-cost-overlays.ts +314 -0
  706. package/src/vision/anthropic-describe.ts +10 -6
  707. package/src/vision/backends.ts +97 -0
  708. package/src/vision/describe.ts +27 -16
  709. package/src/vision/eligibility.ts +250 -0
  710. package/src/vision/index.ts +238 -24
  711. package/src/vision/reasoning.ts +55 -0
  712. package/src/vision/routed-describe.ts +175 -0
  713. package/src/vision/timeout-bounds.ts +9 -0
  714. package/src/web-search/anthropic-executor.ts +13 -7
  715. package/src/web-search/backends.ts +108 -0
  716. package/src/web-search/exa-executor.ts +119 -0
  717. package/src/web-search/executor.ts +21 -6
  718. package/src/web-search/gemini-executor.ts +141 -0
  719. package/src/web-search/index.ts +150 -15
  720. package/src/web-search/loop.ts +284 -54
  721. package/src/web-search/parse.ts +125 -30
  722. package/src/web-search/sources.ts +60 -0
  723. package/src/web-search/xai-executor.ts +219 -0
  724. package/gui/dist/assets/index-DTpMHS4F.js +0 -67
  725. package/gui/dist/assets/index-ZNVDE3C7.css +0 -1
  726. package/gui/dist/provider-icons/antigravity.svg +0 -1
  727. package/gui/dist/provider-icons/claude.svg +0 -1
  728. package/gui/dist/provider-icons/copilot.svg +0 -1
  729. package/gui/dist/provider-icons/cursor.svg +0 -2
  730. package/gui/dist/provider-icons/gemini.svg +0 -1
  731. package/gui/dist/provider-icons/grok-color.svg +0 -1
  732. package/gui/dist/provider-icons/kiro.svg +0 -14
  733. package/src/cli/internal-dispatch.ts +0 -20
@@ -3,11 +3,15 @@ import type { AgentServerMessage, McpArgs, ToolCall } from "./gen/agent_pb";
3
3
  import { decodeCursorArgsMap } from "./arg-codec";
4
4
  import { normalizeArgKeys } from "./arg-normalize";
5
5
  import {
6
+ CODEX_APPLY_PATCH_TOOL,
7
+ CURSOR_MULTI_EDIT_TOOL,
6
8
  cursorShellBridgeArgsValid,
7
9
  cursorShellBridgeDropError,
8
10
  defaultShellBridgeArgNormalizeSchema,
9
11
  isCodexShellBridgeToolName,
12
+ isCursorStructuredEditToolName,
10
13
  normalizeCursorWireName,
14
+ normalizeCursorTextToolMarkers,
11
15
  OCX_RESPONSES_TOOL_PROVIDER,
12
16
  resolveShellBridgeAliasKey,
13
17
  responsesToolNameFromCursorWire,
@@ -17,6 +21,7 @@ import type { TranslatorBudget } from "../../lib/translator-budget";
17
21
 
18
22
  const DEFAULT_CONTEXT_USAGE_MAX_ENTRIES = 200;
19
23
  const DEFAULT_CONTEXT_USAGE_TTL_MS = 60 * 60 * 1_000;
24
+ const DEFAULT_MAX_CLIENT_TOOL_CALLS = 330;
20
25
 
21
26
  export interface CursorContextUsageControls {
22
27
  /**
@@ -150,25 +155,58 @@ export interface CursorProtobufEventState {
150
155
  */
151
156
  contextCarryForwardTokens?: number;
152
157
  recordContextTokens?: (tokens: number) => void;
153
- openToolCalls: Map<string, { name: string; args: string }>;
158
+ openToolCalls: Map<string, { name: string; args: string; awaitingNativeArgs?: boolean }>;
154
159
  completedToolCalls: Set<string>;
155
160
  /** Set once a terminal `done`/truncation has been emitted, so post-terminal frames stay inert. */
156
161
  terminated?: boolean;
157
162
  clientToolNames?: Set<string>;
163
+ /** Responses/Codex names of request-declared freeform tools advertised to Cursor. */
164
+ freeformToolNames?: ReadonlySet<string>;
158
165
  parallelToolCalls?: boolean;
159
166
  startedClientToolCalls: number;
167
+ /** Hard cap on client tool-call records retained during one upstream turn. */
168
+ maxClientToolCalls: number;
160
169
  /** Tool wire-name → original JSON Schema parameters object, for arg-key normalization. */
161
170
  toolSchemas?: Map<string, unknown>;
162
171
  /** Cursor wire-name → original Responses/Codex tool name for this request. */
163
172
  cursorToolNameMap?: Map<string, string>;
173
+ /**
174
+ * Bare names WE advertised as synthetic structured-edit tools on this request.
175
+ * See structuredEditCallIsOurs: conversion is gated on provenance, not on the name.
176
+ */
177
+ syntheticStructuredEditToolNames?: ReadonlySet<string>;
164
178
  translatorBudget?: TranslatorBudget;
165
179
  }
166
180
 
181
+
182
+ /**
183
+ * Did WE advertise this bare tool name as a synthetic structured-edit tool on this request?
184
+ *
185
+ * Provenance, not a name test. `edit_file` / `multi_edit` are ordinary names a client or MCP
186
+ * server may legitimately expose, and `cursorStructuredEditTools` already refuses to shadow one
187
+ * that exists. Converting on the name alone would undo that refusal at the other end of the
188
+ * request: the client's own call would be silently re-emitted as `apply_patch`, or dropped with
189
+ * an error naming a conversion the user never asked for.
190
+ *
191
+ * Absent set = we advertised nothing, so nothing converts. Fail-closed in the safe direction:
192
+ * an unconverted structured call is a visible, recoverable failure; a wrongly converted one
193
+ * edits a file.
194
+ */
195
+ function structuredEditCallIsOurs(
196
+ advertised: ReadonlySet<string> | undefined,
197
+ toolName: string,
198
+ ): boolean {
199
+ return advertised?.has(toolName) === true;
200
+ }
201
+
167
202
  export function createCursorProtobufEventState(options: {
168
203
  clientToolNames?: Iterable<string>;
204
+ freeformToolNames?: Iterable<string>;
169
205
  parallelToolCalls?: boolean;
206
+ maxClientToolCalls?: number;
170
207
  toolSchemas?: Map<string, unknown>;
171
208
  cursorToolNameMap?: Map<string, string>;
209
+ syntheticStructuredEditToolNames?: Iterable<string>;
172
210
  contextUsage?: CursorContextUsageControls;
173
211
  /**
174
212
  * Request-local input estimate derived from the payload actually sent. Used only
@@ -185,8 +223,17 @@ export function createCursorProtobufEventState(options: {
185
223
  openToolCalls: new Map(),
186
224
  completedToolCalls: new Set(),
187
225
  ...(options.clientToolNames ? { clientToolNames: new Set(options.clientToolNames) } : {}),
226
+ ...(options.freeformToolNames ? { freeformToolNames: new Set(options.freeformToolNames) } : {}),
227
+ ...(options.syntheticStructuredEditToolNames
228
+ ? { syntheticStructuredEditToolNames: new Set(options.syntheticStructuredEditToolNames) }
229
+ : {}),
188
230
  ...(options.parallelToolCalls !== undefined ? { parallelToolCalls: options.parallelToolCalls } : {}),
189
231
  startedClientToolCalls: 0,
232
+ maxClientToolCalls: typeof options.maxClientToolCalls === "number"
233
+ && Number.isFinite(options.maxClientToolCalls)
234
+ && options.maxClientToolCalls > 0
235
+ ? Math.floor(options.maxClientToolCalls)
236
+ : DEFAULT_MAX_CLIENT_TOOL_CALLS,
190
237
  ...(options.toolSchemas ? { toolSchemas: options.toolSchemas } : {}),
191
238
  ...(options.cursorToolNameMap ? { cursorToolNameMap: options.cursorToolNameMap } : {}),
192
239
  ...(options.translatorBudget ? { translatorBudget: options.translatorBudget } : {}),
@@ -302,13 +349,696 @@ function normalizeJsonText(text: string, toolName: string | undefined, state: Cu
302
349
  * streamed onward), and/or as a structured protobuf map on `toolCallCompleted`. We emit the args
303
350
  * exactly once, at completion, so they can always be schema-normalized regardless of which form
304
351
  * arrived. The completed map wins when present (canonical); otherwise the buffered streamed text is
305
- * used. Returns an empty string when there are no args (the bridge serializes that as `{}`).
352
+ * preserved verbatim so the bridge can reject malformed or truncated JSON instead of silently
353
+ * converting it to `{}`. A genuinely empty buffer remains the no-argument case.
306
354
  */
307
355
  function resolveCompletedArgs(buffered: string, args: McpArgs | undefined, state: CursorProtobufEventState): string {
308
356
  if (hasMcpArgBytes(args)) return decodeMcpArgsNormalized(args, state);
309
357
  const name = mcpWireNameFromArgs(args);
310
358
  if (isCompleteJson(buffered)) return normalizeJsonText(buffered, name, state);
311
- return "";
359
+ return buffered;
360
+ }
361
+
362
+ const PATCH_BEGIN = "*** Begin Patch";
363
+ const PATCH_END = "*** End Patch";
364
+ const GIT_HUNK_HEADER = /^@@ -\d+(?:,\d+)? \+\d+(?:,\d+)?(?: @@.*)?$/;
365
+ const MARKDOWN_FENCE = /^```[\w+-]*\s*$/;
366
+ const PATH_ARG_KEYS = ["file_path", "filePath", "path", "filepath", "filename", "file", "target_file", "targetFile", "target_path", "targetPath"] as const;
367
+ const OLD_STRING_KEYS = ["old_string", "oldString", "oldtext", "old_text", "old_content", "oldContent", "before", "search"] as const;
368
+ const NEW_STRING_KEYS = ["new_string", "newString", "newtext", "new_text", "contents", "content", "new_contents", "newContents", "after", "replace"] as const;
369
+
370
+ export type StructuredEditPair = { old_string: string; new_string: string };
371
+
372
+ /** First index where `needle` appears as consecutive whole lines in `haystack`, or -1. */
373
+ function lineBlockIndex(haystack: string, needle: string): number {
374
+ if (needle.length === 0) return -1;
375
+ const hay = patchLines(haystack);
376
+ const ned = patchLines(needle);
377
+ if (ned.length === 0 || ned.length > hay.length) return -1;
378
+ for (let i = 0; i <= hay.length - ned.length; i++) {
379
+ if (ned.every((line, j) => hay[i + j] === line)) return i;
380
+ }
381
+ return -1;
382
+ }
383
+
384
+ function replaceLineBlock(haystack: string, needle: string, replacement: string): string {
385
+ const at = lineBlockIndex(haystack, needle);
386
+ if (at < 0) return haystack;
387
+ const hay = patchLines(haystack);
388
+ const ned = patchLines(needle);
389
+ const next = [...hay.slice(0, at), ...patchLines(replacement), ...hay.slice(at + ned.length)];
390
+ return next.join("\n");
391
+ }
392
+
393
+ /**
394
+ * Codex apply_patch matches every hunk against the original file (atomic). Cursor models
395
+ * emit sequential multi_edit (later old_string is the text after an earlier replacement).
396
+ * Fold only when one side contains the other as whole lines — raw substring includes()
397
+ * merged independent edits (B8: `hello world` inside `x = hello world`).
398
+ */
399
+ export function foldSequentialStructuredEdits(edits: StructuredEditPair[]): StructuredEditPair[] {
400
+ const folded: StructuredEditPair[] = [];
401
+ for (const edit of edits) {
402
+ let absorbed = false;
403
+ for (let i = folded.length - 1; i >= 0; i--) {
404
+ const prior = folded[i];
405
+ if (lineBlockIndex(prior.new_string, edit.old_string) >= 0) {
406
+ folded[i] = {
407
+ old_string: prior.old_string,
408
+ new_string: replaceLineBlock(prior.new_string, edit.old_string, edit.new_string),
409
+ };
410
+ absorbed = true;
411
+ break;
412
+ }
413
+ if (lineBlockIndex(edit.old_string, prior.new_string) >= 0) {
414
+ folded[i] = {
415
+ old_string: replaceLineBlock(edit.old_string, prior.new_string, prior.old_string),
416
+ new_string: edit.new_string,
417
+ };
418
+ absorbed = true;
419
+ break;
420
+ }
421
+ }
422
+ if (!absorbed) folded.push({ old_string: edit.old_string, new_string: edit.new_string });
423
+ }
424
+ return folded;
425
+ }
426
+
427
+ const GIT_NO_NEWLINE = /^\\s*$/;
428
+ const GIT_META_PREFIX = /^(diff --git |index |new file mode |deleted file mode |old mode |new mode |similarity index |dissimilarity index |rename from |rename to |copy from |copy to )/;
429
+ const GIT_FILE_HEADER = /^(---|\+\+\+) (?:\/dev\/null|"[ab]\/|[ab]\/)/;
430
+
431
+ function isCodexFileOpLine(line: string): boolean {
432
+ return line.startsWith("*** Update File:")
433
+ || line.startsWith("*** Add File:")
434
+ || line.startsWith("*** Delete File:");
435
+ }
436
+
437
+ function canonicalizeCodexLine(line: string): string {
438
+ const trimmed = line.replace(/^\uFEFF/, "");
439
+ const lower = trimmed.toLowerCase();
440
+ if (lower === "*** begin patch" || lower.startsWith("*** begin patch ")) return PATCH_BEGIN;
441
+ if (lower === "*** end patch" || lower.startsWith("*** end patch ")) return PATCH_END;
442
+ const colonOps = [
443
+ ["*** update file:", "*** Update File:"],
444
+ ["*** add file:", "*** Add File:"],
445
+ ["*** delete file:", "*** Delete File:"],
446
+ ["*** move to:", "*** Move to:"],
447
+ ] as const;
448
+ for (const [needle, canon] of colonOps) {
449
+ if (lower.startsWith(needle)) return `${canon}${trimmed.slice(needle.length)}`;
450
+ }
451
+ const spaceOps = [
452
+ ["*** update file ", "*** Update File: "],
453
+ ["*** add file ", "*** Add File: "],
454
+ ["*** delete file ", "*** Delete File: "],
455
+ ] as const;
456
+ for (const [needle, canon] of spaceOps) {
457
+ if (lower.startsWith(needle)) return `${canon}${trimmed.slice(needle.length)}`;
458
+ }
459
+ return trimmed;
460
+ }
461
+
462
+ function isGitPreambleLine(line: string): boolean {
463
+ return line === "---"
464
+ || GIT_NO_NEWLINE.test(line)
465
+ || GIT_META_PREFIX.test(line)
466
+ || GIT_FILE_HEADER.test(line);
467
+ }
468
+
469
+ function unquoteGitPath(path: string): string {
470
+ const trimmed = path.trim();
471
+ if (
472
+ (trimmed.startsWith("\"") && trimmed.endsWith("\""))
473
+ || (trimmed.startsWith("'") && trimmed.endsWith("'"))
474
+ ) {
475
+ return normalizePatchPath(trimmed.slice(1, -1));
476
+ }
477
+ return normalizePatchPath(trimmed);
478
+ }
479
+
480
+ /** Grammar-only path cleanup: trim, POSIX slashes, drop a leading `./`. */
481
+ function normalizePatchPath(path: string): string {
482
+ let next = path.trim().replace(/\\/g, "/");
483
+ while (next.startsWith("./")) next = next.slice(2);
484
+ return next;
485
+ }
486
+
487
+ function parseDiffGitPaths(line: string): { a: string; b: string } | undefined {
488
+ const quoted = /^diff --git "a\/(.+)" "b\/(.+)"$/.exec(line);
489
+ if (quoted) return { a: unquoteGitPath(quoted[1]), b: unquoteGitPath(quoted[2]) };
490
+ const plain = /^diff --git a\/(.+) b\/(.+)$/.exec(line);
491
+ if (plain) return { a: unquoteGitPath(plain[1]), b: unquoteGitPath(plain[2]) };
492
+ return undefined;
493
+ }
494
+
495
+ function parseGitSidePath(line: string, side: "a" | "b"): string | undefined {
496
+ const quoted = new RegExp(`^(?:---|[+][+][+]) "${side}\\/(.+)"$`).exec(line);
497
+ if (quoted) return unquoteGitPath(quoted[1]);
498
+ const plain = new RegExp(`^(?:---|[+][+][+]) ${side}\\/(.+)$`).exec(line);
499
+ if (plain) return unquoteGitPath(plain[1]);
500
+ return undefined;
501
+ }
502
+
503
+ function isDevNull(line: string): boolean {
504
+ return /^(---|\+\+\+) \/dev\/null$/.test(line);
505
+ }
506
+
507
+ function rewriteHunkHeader(line: string): string {
508
+ return GIT_HUNK_HEADER.test(line) ? "@@" : line;
509
+ }
510
+
511
+ function isFenceLine(line: string): boolean {
512
+ return MARKDOWN_FENCE.test(line);
513
+ }
514
+
515
+ function isHunkBodyLine(line: string): boolean {
516
+ return line === "@@"
517
+ || line.startsWith("@@ ")
518
+ || GIT_HUNK_HEADER.test(line)
519
+ || line.startsWith("+")
520
+ || line.startsWith("-")
521
+ || line.startsWith(" ")
522
+ || line === "";
523
+ }
524
+
525
+ function rewriteCodexFileOpLine(line: string): string {
526
+ for (const prefix of ["*** Update File:", "*** Add File:", "*** Delete File:", "*** Move to:"] as const) {
527
+ if (!line.startsWith(prefix)) continue;
528
+ const path = normalizePatchPath(line.slice(prefix.length).replace(/^\s+/, ""));
529
+ return path ? `${prefix} ${path}` : line;
530
+ }
531
+ return line;
532
+ }
533
+
534
+ function normalizeAddFileBody(lines: readonly string[]): string[] {
535
+ const out: string[] = [];
536
+ let inAdd = false;
537
+ for (const line of lines) {
538
+ if (line.startsWith("*** Add File:")) {
539
+ inAdd = true;
540
+ out.push(line);
541
+ continue;
542
+ }
543
+ if (isCodexFileOpLine(line)) {
544
+ inAdd = false;
545
+ out.push(line);
546
+ continue;
547
+ }
548
+ if (inAdd && (line === "@@" || line.startsWith("@@ ") || GIT_HUNK_HEADER.test(line))) continue;
549
+ if (inAdd && line.length > 0 && !line.startsWith("+")) {
550
+ out.push(`+${line}`);
551
+ continue;
552
+ }
553
+ out.push(line);
554
+ }
555
+ return out;
556
+ }
557
+
558
+ function hasNonEmptyCodexOp(lines: readonly string[]): boolean {
559
+ let kind: "add" | "update" | "delete" | undefined;
560
+ let hunk = false;
561
+ let any = false;
562
+ const flush = () => {
563
+ if (kind === "delete" || ((kind === "add" || kind === "update") && hunk)) any = true;
564
+ };
565
+ for (const line of lines) {
566
+ if (line.startsWith("*** Update File:")) {
567
+ flush();
568
+ kind = "update";
569
+ hunk = false;
570
+ continue;
571
+ }
572
+ if (line.startsWith("*** Add File:")) {
573
+ flush();
574
+ kind = "add";
575
+ hunk = false;
576
+ continue;
577
+ }
578
+ if (line.startsWith("*** Delete File:")) {
579
+ flush();
580
+ kind = "delete";
581
+ hunk = false;
582
+ continue;
583
+ }
584
+ if (line.startsWith("+") || line.startsWith("-") || line === "@@" || line.startsWith("@@ ")) hunk = true;
585
+ }
586
+ flush();
587
+ return any;
588
+ }
589
+
590
+ function trimEmptyEdges(lines: readonly string[]): string[] {
591
+ let start = 0;
592
+ let end = lines.length;
593
+ while (start < end && lines[start] === "") start++;
594
+ while (end > start && lines[end - 1] === "") end--;
595
+ return lines.slice(start, end);
596
+ }
597
+
598
+ function cleanHunkLines(lines: readonly string[]): string[] {
599
+ return trimEmptyEdges(
600
+ lines
601
+ .filter(line =>
602
+ !isGitPreambleLine(line)
603
+ && !isFenceLine(line)
604
+ && line !== PATCH_BEGIN
605
+ && line !== PATCH_END
606
+ && !isCodexFileOpLine(line)
607
+ && !line.startsWith("*** Move to:")
608
+ )
609
+ .map(rewriteHunkHeader)
610
+ .filter(line => isHunkBodyLine(line)),
611
+ );
612
+ }
613
+
614
+ function isGitSectionStart(line: string, splitOnDiffGit: boolean): boolean {
615
+ if (splitOnDiffGit) return line.startsWith("diff --git ");
616
+ return /^(--- )(?:\/dev\/null|"a\/|a\/)/.test(line);
617
+ }
618
+
619
+ function splitGitSections(lines: readonly string[]): string[][] {
620
+ const splitOnDiffGit = lines.some(line => line.startsWith("diff --git "));
621
+ const sections: string[][] = [];
622
+ let current: string[] = [];
623
+ for (const line of lines) {
624
+ if (isGitSectionStart(line, splitOnDiffGit) && current.length > 0) {
625
+ sections.push(current);
626
+ current = [line];
627
+ continue;
628
+ }
629
+ current.push(line);
630
+ }
631
+ if (current.length > 0) sections.push(current);
632
+ return sections;
633
+ }
634
+
635
+ function isGitBinarySection(lines: readonly string[]): boolean {
636
+ return lines.some(line => /^Binary files /.test(line) || line.startsWith("GIT binary patch"));
637
+ }
638
+
639
+ function isGitCopySection(lines: readonly string[]): boolean {
640
+ return lines.some(line => line.startsWith("copy from ") || line.startsWith("copy to "));
641
+ }
642
+
643
+ function isGitEmptyRenameSection(lines: readonly string[]): boolean {
644
+ const hasRenameMeta = lines.some(line => line.startsWith("rename from ") || line.startsWith("rename to "));
645
+ const diff = lines.map(parseDiffGitPaths).find(path => path !== undefined);
646
+ if (!hasRenameMeta && !(diff && diff.a !== diff.b)) return false;
647
+ const body = cleanHunkLines(lines);
648
+ return !body.some(line => line === "@@" || line.startsWith("@@ ") || line.startsWith("+") || line.startsWith("-"));
649
+ }
650
+
651
+ function isGitUntranslatableSection(lines: readonly string[]): boolean {
652
+ return isGitBinarySection(lines) || isGitCopySection(lines) || isGitEmptyRenameSection(lines);
653
+ }
654
+
655
+ function convertGitSection(lines: readonly string[]): string[] | undefined {
656
+ if (isGitBinarySection(lines)) return undefined;
657
+ const existingOp = lines.find(isCodexFileOpLine);
658
+ if (existingOp) {
659
+ const move = lines.filter(line => line.startsWith("*** Move to:"));
660
+ return [existingOp, ...move, ...cleanHunkLines(lines)];
661
+ }
662
+ const diffPaths = lines.map(parseDiffGitPaths).find(path => path !== undefined);
663
+ const plusPath = lines.map(line => parseGitSidePath(line, "b")).find(path => path !== undefined);
664
+ const minusPath = lines.map(line => parseGitSidePath(line, "a")).find(path => path !== undefined);
665
+ const renameFrom = lines.find(line => line.startsWith("rename from "))?.slice("rename from ".length);
666
+ const renameTo = lines.find(line => line.startsWith("rename to "))?.slice("rename to ".length);
667
+ const plusIsNull = lines.some(line => line.startsWith("+++ ") && isDevNull(line));
668
+ const minusIsNull = lines.some(line => line.startsWith("--- ") && isDevNull(line));
669
+ const isNewFile = lines.some(line => line.startsWith("new file mode ")) || minusIsNull;
670
+ const isDeleted = lines.some(line => line.startsWith("deleted file mode ")) || plusIsNull;
671
+ const pathA = minusPath ?? (renameFrom ? unquoteGitPath(renameFrom) : undefined) ?? diffPaths?.a;
672
+ const pathB = plusPath ?? (renameTo ? unquoteGitPath(renameTo) : undefined) ?? diffPaths?.b;
673
+ const body = cleanHunkLines(lines);
674
+ if (isDeleted) {
675
+ const path = pathA ?? pathB;
676
+ return path ? [`*** Delete File: ${path}`] : undefined;
677
+ }
678
+ if (isNewFile) {
679
+ const path = pathB ?? pathA;
680
+ if (!path) return undefined;
681
+ return [`*** Add File: ${path}`, ...body.filter(line => line.startsWith("+"))];
682
+ }
683
+ const hasMinus = body.some(line => line.startsWith("-"));
684
+ if (plusPath && !minusPath && !diffPaths && !hasMinus) {
685
+ return [`*** Add File: ${plusPath}`, ...body.filter(line => line.startsWith("+"))];
686
+ }
687
+ const from = (renameFrom ? unquoteGitPath(renameFrom) : undefined) ?? pathA ?? pathB;
688
+ const to = (renameTo ? unquoteGitPath(renameTo) : undefined) ?? pathB;
689
+ if (!from) return undefined;
690
+ const hasHunk = body.some(line => line === "@@" || line.startsWith("@@ ") || line.startsWith("+") || line.startsWith("-"));
691
+ // Codex 0.147 rejects "Update file hunk ... is empty" (mode-only diffs, 100% renames).
692
+ if (!hasHunk) return undefined;
693
+ const header = [`*** Update File: ${from}`];
694
+ if (to && to !== from) header.push(`*** Move to: ${to}`);
695
+ return [...header, ...body];
696
+ }
697
+
698
+ function hasCodexFileOp(lines: readonly string[]): boolean {
699
+ return lines.some(isCodexFileOpLine);
700
+ }
701
+
702
+ /** Grammar-only repair for Cursor-emitted freeform apply_patch. Not a fuzzy filesystem apply. */
703
+ export function sanitizeCodexApplyPatch(patch: string): string {
704
+ const trimmed = patch.replace(/^\uFEFF/, "").replace(/\n+$/, "");
705
+ const rawLines = trimmed.split("\n").map(line => canonicalizeCodexLine(line.endsWith("\r") ? line.slice(0, -1) : line));
706
+ const hasCodex = rawLines.some(isCodexFileOpLine);
707
+ const hasGitHeaders = rawLines.some(line => line.startsWith("diff --git ") || GIT_FILE_HEADER.test(line));
708
+ if (hasGitHeaders && !hasCodex) {
709
+ const sections = splitGitSections(rawLines);
710
+ // A binary hunk cannot be expressed in Codex apply_patch. Leave the original
711
+ // text alone rather than wrapping the text files and dropping the binary one.
712
+ if (sections.some(isGitUntranslatableSection)) return patch.replace(/^\uFEFF/, "");
713
+ const ops = sections
714
+ .map(convertGitSection)
715
+ .filter((section): section is string[] => section !== undefined && hasCodexFileOp(section))
716
+ .map(normalizeAddFileBody);
717
+ if (ops.length > 0) return [PATCH_BEGIN, ...ops.flat(), PATCH_END].join("\n");
718
+ }
719
+ const selected: string[] = [];
720
+ let inAdd = false;
721
+ for (const raw of rawLines) {
722
+ if (isGitPreambleLine(raw) || isFenceLine(raw) || raw === PATCH_BEGIN || raw === PATCH_END) continue;
723
+ const line = rewriteCodexFileOpLine(rewriteHunkHeader(raw));
724
+ if (line.startsWith("*** Add File:")) {
725
+ inAdd = true;
726
+ selected.push(line);
727
+ continue;
728
+ }
729
+ if (isCodexFileOpLine(line)) {
730
+ inAdd = false;
731
+ selected.push(line);
732
+ continue;
733
+ }
734
+ if (line.startsWith("*** Move to:") || isHunkBodyLine(line) || (inAdd && line.length > 0)) {
735
+ selected.push(line);
736
+ }
737
+ }
738
+ const lines = normalizeAddFileBody(trimEmptyEdges(selected));
739
+ const looksLikePatch = lines.some(line =>
740
+ line === "@@"
741
+ || line.startsWith("@@ ")
742
+ || isCodexFileOpLine(line)
743
+ );
744
+ if (!looksLikePatch) return patch.replace(/^\uFEFF/, "");
745
+ // A hunk with no file op is not a valid Codex patch. Do not invent Begin/End around it.
746
+ if (!hasCodexFileOp(lines)) return lines.join("\n");
747
+ if (!hasNonEmptyCodexOp(lines)) return patch.replace(/^\uFEFF/, "");
748
+ return [PATCH_BEGIN, ...lines, PATCH_END].join("\n");
749
+ }
750
+
751
+ function coercePatchInput(value: unknown): string | undefined {
752
+ if (typeof value === "string") {
753
+ const trimmed = value.trim();
754
+ if (trimmed.startsWith("{")) {
755
+ try {
756
+ const inner: unknown = JSON.parse(trimmed);
757
+ if (inner && typeof inner === "object" && !Array.isArray(inner)) {
758
+ const record = inner as Record<string, unknown>;
759
+ if (typeof record.input === "string") return record.input;
760
+ if (Array.isArray(record.input) && record.input.every(item => typeof item === "string")) {
761
+ return record.input.join("\n");
762
+ }
763
+ }
764
+ } catch {
765
+ // The string is the patch, not nested JSON.
766
+ }
767
+ }
768
+ return value;
769
+ }
770
+ if (Array.isArray(value) && value.every(item => typeof item === "string")) return value.join("\n");
771
+ return undefined;
772
+ }
773
+
774
+ export function sanitizeEmittedApplyPatchArgs(argsText: string): string {
775
+ try {
776
+ const parsed: unknown = JSON.parse(argsText);
777
+ if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
778
+ const record = parsed as Record<string, unknown>;
779
+ const raw = coercePatchInput(record.input) ?? coercePatchInput(record.patch) ?? coercePatchInput(record.content);
780
+ if (raw !== undefined) {
781
+ const input = sanitizeCodexApplyPatch(raw);
782
+ if (input !== record.input) {
783
+ const next: Record<string, unknown> = { ...record, input };
784
+ delete next.patch;
785
+ delete next.content;
786
+ return JSON.stringify(next);
787
+ }
788
+ }
789
+ }
790
+ } catch {
791
+ if (
792
+ argsText.includes("@@")
793
+ || argsText.includes("***")
794
+ || argsText.includes("diff --git")
795
+ || argsText.includes("--- a/")
796
+ || argsText.includes("+++ b/")
797
+ || argsText.includes("--- /dev/null")
798
+ ) {
799
+ return JSON.stringify({ input: sanitizeCodexApplyPatch(argsText) });
800
+ }
801
+ }
802
+ return argsText;
803
+ }
804
+
805
+ function firstStringArg(args: Record<string, unknown>, keys: readonly string[]): string | undefined {
806
+ for (const key of keys) {
807
+ const value = args[key];
808
+ if (typeof value === "string") return value;
809
+ }
810
+ return undefined;
811
+ }
812
+
813
+ function firstStringOrLines(args: Record<string, unknown>, keys: readonly string[]): string | undefined {
814
+ for (const key of keys) {
815
+ const value = args[key];
816
+ if (typeof value === "string") return value;
817
+ if (Array.isArray(value) && value.length > 0 && value.every(item => typeof item === "string")) {
818
+ return value.join("\n");
819
+ }
820
+ }
821
+ return undefined;
822
+ }
823
+
824
+ /** Split a replacement into patch lines, ignoring one trailing newline (line-based patch semantics). */
825
+ function patchLines(text: string): string[] {
826
+ const lines = text.split("\n");
827
+ if (lines.length > 0 && lines[lines.length - 1] === "") lines.pop();
828
+ return lines;
829
+ }
830
+
831
+ /**
832
+ * Models often copy indent in old_string and omit it in new_string. Codex trim-matches the
833
+ * old line and writes new_string verbatim, which strips indent. Copy old leading whitespace
834
+ * onto a flush-left new line of the same line count. Do not change a new line that already
835
+ * has indent (intentional dedent stays possible).
836
+ */
837
+ function restoreFlushLeftIndent(oldString: string, newString: string): string {
838
+ const oldLines = patchLines(oldString);
839
+ const newLines = patchLines(newString);
840
+ if (oldLines.length !== newLines.length || oldLines.length === 0) return newString;
841
+ // If the only difference is leading whitespace, the edit IS a deliberate
842
+ // indent change — restoring old indent would erase the user's intent.
843
+ const contentSame = oldLines.every((ol, i) => ol.trimStart() === newLines[i].trimStart());
844
+ const whitespaceDiffers = oldLines.some((ol, i) => {
845
+ const oldLead = /^[ \t]*/.exec(ol)?.[0] ?? "";
846
+ const newLead = /^[ \t]*/.exec(newLines[i])?.[0] ?? "";
847
+ return oldLead !== newLead;
848
+ });
849
+ if (contentSame && whitespaceDiffers) return newString;
850
+ return newLines.map((line, i) => {
851
+ const oldLead = /^[ \t]*/.exec(oldLines[i])?.[0] ?? "";
852
+ const newLead = /^[ \t]*/.exec(line)?.[0] ?? "";
853
+ if (oldLead.length > 0 && newLead.length === 0 && line.length > 0) return oldLead + line;
854
+ return line;
855
+ }).join("\n");
856
+ }
857
+
858
+ function addFilePatch(path: string, newString: string): StructuredEditTranslation {
859
+ const newLines = patchLines(newString);
860
+ if (newLines.length === 0) {
861
+ return { error: "structured edit requires a non-empty old_string; an empty replacement is not a valid edit" };
862
+ }
863
+ return { patch: [PATCH_BEGIN, `*** Add File: ${path}`, ...newLines.map(line => `+${line}`), PATCH_END].join("\n") };
864
+ }
865
+
866
+ /** One `@@` hunk replacing `oldString` with `newString`. */
867
+ function replacementHunk(oldString: string, newString: string): { hunk: string } | { error: string } {
868
+ if (oldString.length === 0) {
869
+ return {
870
+ error:
871
+ "structured edit requires a non-empty old_string to locate the replacement; for new files or insertions without existing text, call apply_patch with an `*** Add File` / context hunk or use the shell bridge",
872
+ };
873
+ }
874
+ const oldLines = patchLines(oldString);
875
+ const rawNewLines = patchLines(newString);
876
+ // Line-based patch semantics cannot express an edit that only adds or removes the file's
877
+ // final newline, and an old/new pair that normalizes to the same lines is a silent no-op —
878
+ // reject it rather than emitting an empty hunk that apply_patch would drop.
879
+ if (oldLines.length === 0 && rawNewLines.length === 0) {
880
+ return { error: "structured edit requires a non-empty old_string; an empty replacement is not a valid edit" };
881
+ }
882
+ // Check for no-op against the RAW new_string (before indent restoration) so that
883
+ // intentional dedent edits are not falsely classified as identical.
884
+ if (oldLines.length === rawNewLines.length && oldLines.every((line, i) => line === rawNewLines[i])) {
885
+ return { error: "structured edit old_string and new_string are identical after line normalization; the replacement is a no-op and was dropped" };
886
+ }
887
+ const restoredNew = restoreFlushLeftIndent(oldString, newString);
888
+ const newLines = patchLines(restoredNew);
889
+ const removed = oldLines.map(line => `-${line}`);
890
+ const added = newLines.map(line => `+${line}`);
891
+ return { hunk: ["@@", ...removed, ...added].join("\n") };
892
+ }
893
+
894
+ /**
895
+ * Convert a completed Cursor structured edit call (`edit_file` / `multi_edit`) into a valid Codex
896
+ * apply_patch freeform payload (#1017). Cursor-trained models cannot emit Codex's freeform patch
897
+ * grammar, so the adapter advertises exact-match replacement tools and performs the grammar here.
898
+ * Returns `{ patch }` for a valid conversion, `{ error }` for a malformed call (which must never be
899
+ * relayed verbatim: Codex would reject it locally after the HTTP 200, the reported failure mode),
900
+ * and `undefined` for tools that are not structured edits.
901
+ */
902
+ export type StructuredEditTranslation =
903
+ | { patch: string; error?: undefined }
904
+ | { error: string; patch?: undefined };
905
+
906
+ export function translateStructuredEditCall(
907
+ toolName: string,
908
+ argsText: string,
909
+ ): StructuredEditTranslation | undefined {
910
+ if (!isCursorStructuredEditToolName(toolName)) return undefined;
911
+ let parsed: unknown;
912
+ try {
913
+ parsed = JSON.parse(argsText);
914
+ if (typeof parsed === "string") parsed = JSON.parse(parsed);
915
+ } catch {
916
+ return {
917
+ error: `${toolName} arguments were not valid JSON; the call was dropped. ${
918
+ toolName === CURSOR_MULTI_EDIT_TOOL
919
+ ? "Use file_path and edits[] (each edit with old_string and new_string)."
920
+ : "Use file_path, old_string and new_string."
921
+ }`,
922
+ };
923
+ }
924
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
925
+ return { error: `${toolName} arguments must be a JSON object; the call was dropped.` };
926
+ }
927
+ const args = parsed as Record<string, unknown>;
928
+ const rawPath = firstStringArg(args, PATH_ARG_KEYS);
929
+ const path = rawPath ? normalizePatchPath(rawPath) : undefined;
930
+ if (!path) {
931
+ return { error: `${toolName} is missing a non-empty file_path; the call was dropped.` };
932
+ }
933
+ if (/[\n\r\0]/.test(path)) {
934
+ return { error: `${toolName} file_path must not contain a newline, CR, or NUL; the call was dropped.` };
935
+ }
936
+ if (args.replace_all === true || args.replaceAll === true) {
937
+ return {
938
+ error:
939
+ `${toolName} replace_all is not supported; Codex apply_patch first-matches only. Split into unique old_string hunks or include more surrounding lines.`,
940
+ };
941
+ }
942
+ if (args.delete_file === true || args.deleteFile === true) {
943
+ return { patch: [PATCH_BEGIN, `*** Delete File: ${path}`, PATCH_END].join("\n") };
944
+ }
945
+ const hunks: string[] = [];
946
+ const addReplacement = (record: Record<string, unknown>): StructuredEditTranslation => {
947
+ const oldString = firstStringOrLines(record, OLD_STRING_KEYS);
948
+ const newString = firstStringOrLines(record, NEW_STRING_KEYS);
949
+ if (oldString === undefined || newString === undefined) {
950
+ return { error: `${toolName} requires old_string and new_string; the call was dropped.` };
951
+ }
952
+ const hunk = replacementHunk(oldString, newString);
953
+ if ("error" in hunk) return { error: hunk.error };
954
+ return { patch: hunk.hunk };
955
+ };
956
+ if (toolName === CURSOR_MULTI_EDIT_TOOL) {
957
+ let edits: unknown = args.edits;
958
+ if (typeof edits === "string") {
959
+ try {
960
+ edits = JSON.parse(edits);
961
+ } catch {
962
+ return { error: "multi_edit edits were not valid JSON; the call was dropped." };
963
+ }
964
+ }
965
+ if (!Array.isArray(edits) || edits.length === 0) {
966
+ return { error: "multi_edit requires a non-empty edits array; the call was dropped." };
967
+ }
968
+ // Cap edit count to prevent quadratic CPU exhaustion in the fold and overlap scans.
969
+ if (edits.length > 500) {
970
+ return { error: "multi_edit exceeds the 500-edit limit; split into smaller batches." };
971
+ }
972
+ const pairs: StructuredEditPair[] = [];
973
+ for (const edit of edits) {
974
+ if (!edit || typeof edit !== "object" || Array.isArray(edit)) {
975
+ return { error: "multi_edit edits entries must be objects with old_string and new_string; the call was dropped." };
976
+ }
977
+ const record = edit as Record<string, unknown>;
978
+ if (record.replace_all === true || record.replaceAll === true) {
979
+ return {
980
+ error:
981
+ "multi_edit replace_all is not supported; Codex apply_patch first-matches only. Split into unique old_string hunks or include more surrounding lines.",
982
+ };
983
+ }
984
+ const oldString = firstStringOrLines(record, OLD_STRING_KEYS);
985
+ const newString = firstStringOrLines(record, NEW_STRING_KEYS);
986
+ if (oldString === undefined || newString === undefined) {
987
+ return { error: `${toolName} requires old_string and new_string; the call was dropped.` };
988
+ }
989
+ pairs.push({ old_string: oldString, new_string: newString });
990
+ }
991
+ const folded = foldSequentialStructuredEdits(pairs);
992
+ const seenOld = new Set<string>();
993
+ for (const edit of folded) {
994
+ const oldKey = patchLines(edit.old_string).join("\n");
995
+ if (seenOld.has(oldKey)) {
996
+ return {
997
+ error:
998
+ "multi_edit has two edits with the same old_string after line normalization; Codex apply_patch first-matches, so both hunks would hit the same location. Include more surrounding lines to disambiguate.",
999
+ };
1000
+ }
1001
+ seenOld.add(oldKey);
1002
+ }
1003
+ for (let i = 0; i < folded.length; i++) {
1004
+ for (let j = 0; j < folded.length; j++) {
1005
+ if (i === j || folded[j].old_string.length === 0) continue;
1006
+ if (lineBlockIndex(folded[i].old_string, folded[j].old_string) >= 0) {
1007
+ return {
1008
+ error:
1009
+ "multi_edit hunks overlap: one old_string is a whole-line subset of another. Codex apply_patch matches every hunk against the original file, so both would first-match the same region. Include more surrounding lines to disambiguate.",
1010
+ };
1011
+ }
1012
+ }
1013
+ }
1014
+ if (folded.length === 1 && folded[0].old_string.length === 0) {
1015
+ return addFilePatch(path, folded[0].new_string);
1016
+ }
1017
+ if (folded.some(edit => edit.old_string.length === 0)) {
1018
+ return {
1019
+ error:
1020
+ "multi_edit cannot mix an Add File (empty old_string) with an independent Update hunk on the same path; put the full new-file contents in one empty-old_string edit, or create the file first.",
1021
+ };
1022
+ }
1023
+ for (const edit of folded) {
1024
+ const hunk = replacementHunk(edit.old_string, edit.new_string);
1025
+ if ("error" in hunk) return { error: hunk.error };
1026
+ hunks.push(hunk.hunk);
1027
+ }
1028
+ } else {
1029
+ const oldString = firstStringOrLines(args, OLD_STRING_KEYS);
1030
+ const newString = firstStringOrLines(args, NEW_STRING_KEYS);
1031
+ if (oldString === "") {
1032
+ if (newString === undefined) {
1033
+ return { error: `${toolName} requires old_string and new_string; the call was dropped.` };
1034
+ }
1035
+ return addFilePatch(path, newString);
1036
+ }
1037
+ const editResult = addReplacement(args);
1038
+ if (editResult.error !== undefined) return editResult;
1039
+ hunks.push(editResult.patch);
1040
+ }
1041
+ return { patch: [PATCH_BEGIN, `*** Update File: ${path}`, ...hunks, PATCH_END].join("\n") };
312
1042
  }
313
1043
 
314
1044
  export function mapSyntheticMcpExecToToolEvents(
@@ -317,6 +1047,7 @@ export function mapSyntheticMcpExecToToolEvents(
317
1047
  options: { allowEmptyArgs?: boolean; state?: CursorProtobufEventState } = {},
318
1048
  ): CursorServerMessage[] {
319
1049
  if (args.providerIdentifier !== OCX_RESPONSES_TOOL_PROVIDER) return [];
1050
+ if (options.state?.terminated) return [];
320
1051
  if (options.allowEmptyArgs !== true && !hasMcpArgBytes(args)) return [];
321
1052
  const cursorWireName = mcpWireNameFromArgs(args);
322
1053
  if (!cursorWireName) return [{ type: "error", message: "Cursor requested a Responses tool without a tool name" }];
@@ -341,9 +1072,20 @@ export function mapSyntheticMcpExecToToolEvents(
341
1072
  }
342
1073
  }
343
1074
  // Stateless fallback (no shared event state): emit a complete, self-contained tool call.
1075
+ //
1076
+ // No conversion happens here by design (#1036 review). Structured-edit translation is gated on
1077
+ // provenance — did WE advertise this bare name on THIS request — and that record lives on the
1078
+ // request state, which this branch does not have. Converting anyway would reinstate the exact
1079
+ // hazard the gate exists to close: a client or MCP tool legitimately named `edit_file` would be
1080
+ // rewritten into an apply_patch it never asked for. The live path always carries state
1081
+ // (live-transport seeds it), so this only affects direct/unit callers.
1082
+ const emittedName = responsesName;
1083
+ const emittedArgs = normalizedArgs;
344
1084
  return [
345
- { type: "tool_call_start", id: callId, name: responsesName },
346
- ...(normalizedArgs.length > 2 ? [{ type: "tool_call_delta" as const, arguments: normalizedArgs }] : []),
1085
+ { type: "tool_call_start", id: callId, name: emittedName },
1086
+ ...(emittedArgs.length > 2
1087
+ ? [{ type: "tool_call_delta" as const, arguments: emittedArgs }]
1088
+ : []),
347
1089
  { type: "tool_call_end", id: callId },
348
1090
  ];
349
1091
  }
@@ -363,6 +1105,9 @@ function recordToolCall(state: CursorProtobufEventState, callId: string, cursorW
363
1105
  if (state.clientToolNames && !advertisedName) {
364
1106
  return [{ type: "error", message: `Cursor requested unknown Responses tool: ${cursorWireName}` }];
365
1107
  }
1108
+ if (state.startedClientToolCalls >= state.maxClientToolCalls) {
1109
+ return [{ type: "error", message: `Cursor exceeded client tool-call limit (${state.maxClientToolCalls})` }];
1110
+ }
366
1111
  // Prefer the advertised catalog name for Responses mapping so shell_command/exec_command aliases
367
1112
  // land on the tool Codex actually exposed this turn (#399).
368
1113
  const mapKey = advertisedName ?? normalizeCursorWireName(cursorWireName);
@@ -378,6 +1123,25 @@ function recordToolCall(state: CursorProtobufEventState, callId: string, cursorW
378
1123
  * recorded in `openToolCalls`. Because each completion emits a whole non-interleaved unit, the bridge
379
1124
  * (which tracks a single current tool call) serializes parallel Cursor calls correctly.
380
1125
  */
1126
+ function cursorFreeformWrapperValid(args: string): boolean {
1127
+ try {
1128
+ const parsed = JSON.parse(args) as unknown;
1129
+ return !!parsed
1130
+ && typeof parsed === "object"
1131
+ && !Array.isArray(parsed)
1132
+ && typeof (parsed as Record<string, unknown>).input === "string";
1133
+ } catch {
1134
+ return false;
1135
+ }
1136
+ }
1137
+
1138
+ function dropInvalidFreeformCall(state: CursorProtobufEventState, callId: string, toolName: string): CursorServerMessage[] {
1139
+ state.openToolCalls.delete(callId);
1140
+ state.translatorBudget?.closeCall(callId);
1141
+ state.completedToolCalls.add(callId);
1142
+ return [{ type: "error", message: `${toolName} call had invalid freeform arguments; expected {input:string}` }];
1143
+ }
1144
+
381
1145
  function dropShellBridgeCall(state: CursorProtobufEventState, callId: string, toolName: string): CursorServerMessage[] {
382
1146
  state.openToolCalls.delete(callId);
383
1147
  state.translatorBudget?.closeCall(callId);
@@ -385,13 +1149,33 @@ function dropShellBridgeCall(state: CursorProtobufEventState, callId: string, to
385
1149
  return [{ type: "error", message: cursorShellBridgeDropError(toolName) }];
386
1150
  }
387
1151
 
1152
+ function dropStructuredEditCall(state: CursorProtobufEventState, callId: string, toolName: string, reason: string): CursorServerMessage[] {
1153
+ state.openToolCalls.delete(callId);
1154
+ state.translatorBudget?.closeCall(callId);
1155
+ state.completedToolCalls.add(callId);
1156
+ // Recoverable: a fatal adapter error becomes response.failed / upstream_server_error and
1157
+ // the model never sees the reason. Completing with text keeps the turn alive (#1388).
1158
+ return [{ type: "text", text: `\n${toolName} call was not converted to apply_patch: ${reason}` }];
1159
+ }
1160
+
388
1161
  function commitToolCall(state: CursorProtobufEventState, callId: string, finalArgs: string): CursorServerMessage[] {
389
1162
  const open = state.openToolCalls.get(callId);
390
1163
  if (!open) return [];
1164
+ if (state.freeformToolNames?.has(open.name) && !cursorFreeformWrapperValid(finalArgs)) {
1165
+ return dropInvalidFreeformCall(state, callId, open.name);
1166
+ }
391
1167
  const schema = toolSchemaForWireName(state, open.name);
392
1168
  if (!cursorShellBridgeArgsValid(finalArgs, open.name, schema)) {
393
1169
  if (isCodexShellBridgeToolName(open.name)) return dropShellBridgeCall(state, callId, open.name);
394
1170
  }
1171
+ // Structured edit calls are converted to apply_patch here so both the interactionUpdate and the
1172
+ // native-exec mcpArgs paths emit the same valid freeform payload (#1017).
1173
+ const translation = structuredEditCallIsOurs(state.syntheticStructuredEditToolNames, open.name)
1174
+ ? translateStructuredEditCall(open.name, finalArgs)
1175
+ : undefined;
1176
+ if (translation?.error !== undefined) {
1177
+ return dropStructuredEditCall(state, callId, open.name, translation.error);
1178
+ }
395
1179
  if (finalArgs !== open.args) {
396
1180
  const previousBytes = Buffer.byteLength(open.args);
397
1181
  const reservation = state.translatorBudget?.reserveTransient(
@@ -402,8 +1186,11 @@ function commitToolCall(state: CursorProtobufEventState, callId: string, finalAr
402
1186
  reservation?.commitRetained();
403
1187
  state.translatorBudget?.releaseRetained(previousBytes, { kind: "tool_args", callId });
404
1188
  }
405
- const out: CursorServerMessage[] = [{ type: "tool_call_start", id: callId, name: open.name }];
406
- if (finalArgs.length > 0) out.push({ type: "tool_call_delta", arguments: finalArgs });
1189
+ const emittedName = translation ? CODEX_APPLY_PATCH_TOOL : open.name;
1190
+ const rawArgs = translation ? JSON.stringify({ input: translation.patch }) : finalArgs;
1191
+ const emittedArgs = emittedName === CODEX_APPLY_PATCH_TOOL ? sanitizeEmittedApplyPatchArgs(rawArgs) : rawArgs;
1192
+ const out: CursorServerMessage[] = [{ type: "tool_call_start", id: callId, name: emittedName }];
1193
+ if (emittedArgs.length > 0) out.push({ type: "tool_call_delta", arguments: emittedArgs });
407
1194
  out.push(...endToolCall(state, callId));
408
1195
  return out;
409
1196
  }
@@ -457,7 +1244,10 @@ export function mapCursorProtobufServerMessage(
457
1244
  const update = serverMessage.message.value.message;
458
1245
  switch (update.case) {
459
1246
  case "textDelta":
460
- return update.value.text ? [{ type: "text", text: update.value.text }] : [];
1247
+ // #2305: fold Cursor display aliases inside textual pseudo tool-call markers back to
1248
+ // the advertised wire name before any client sees the text. Real frames are already
1249
+ // normalized structurally (mcpWireNameFromArgs above).
1250
+ return update.value.text ? [{ type: "text", text: normalizeCursorTextToolMarkers(update.value.text) }] : [];
461
1251
  case "thinkingDelta":
462
1252
  return update.value.text ? [{ type: "thinking", thinking: update.value.text }] : [];
463
1253
  case "toolCallStarted": {
@@ -487,12 +1277,27 @@ export function mapCursorProtobufServerMessage(
487
1277
  const args = mcpArgsFromToolCall(update.value.toolCall);
488
1278
  const openBeforeStart = state.openToolCalls.get(update.value.callId);
489
1279
  // Empty-arg completion handling:
490
- // - already open with empty args -> wait for the native-exec args path (do not commit yet).
1280
+ // - already-open named ordinary call with no buffered or structured args -> wait for native exec.
1281
+ // - request-declared freeform completion -> wait while its required input wrapper is absent or
1282
+ // incomplete, whether the completion repeats the name or is compact callId-only. A valid
1283
+ // buffered wrapper can commit immediately.
1284
+ // A compact ordinary no-arg call remains legitimate and commits below.
491
1285
  // - never started + not advertised -> Cursor prelude noise, drop it.
492
1286
  // - advertised client tool, not yet open -> a legitimate no-arg call: commit it (start+end)
493
1287
  // so it is not silently dropped; the bridge serializes empty args as "{}".
1288
+ if (
1289
+ openBeforeStart && !hasMcpArgBytes(args)
1290
+ && (
1291
+ (name !== undefined && openBeforeStart.args.length === 0)
1292
+ || (state.freeformToolNames?.has(openBeforeStart.name) === true
1293
+ && !cursorFreeformWrapperValid(openBeforeStart.args)
1294
+ )
1295
+ )
1296
+ ) {
1297
+ openBeforeStart.awaitingNativeArgs = true;
1298
+ return [];
1299
+ }
494
1300
  if (name && !hasMcpArgBytes(args)) {
495
- if (openBeforeStart && openBeforeStart.args.length === 0) return [];
496
1301
  // Only commit a no-arg call when the tool is *explicitly* advertised. Without an advertised
497
1302
  // tool list we cannot tell a real no-arg call from a Cursor prelude, so we keep dropping it.
498
1303
  const advertised = state.clientToolNames?.has(name) ?? false;
@@ -503,6 +1308,17 @@ export function mapCursorProtobufServerMessage(
503
1308
  if (name) out.push(...recordToolCall(state, update.value.callId, name));
504
1309
  if (out.some(event => event.type === "error")) return out;
505
1310
  const open = state.openToolCalls.get(update.value.callId);
1311
+ // A request-declared freeform call may first appear only in its completion frame. Record it so
1312
+ // later same-ID native mcpArgs can supply the authoritative wrapper, but do not broaden the
1313
+ // wait to ordinary advertised no-arg tools: those still commit immediately below.
1314
+ if (
1315
+ !openBeforeStart && open && !hasMcpArgBytes(args)
1316
+ && state.freeformToolNames?.has(open.name) === true
1317
+ && !cursorFreeformWrapperValid(open.args)
1318
+ ) {
1319
+ open.awaitingNativeArgs = true;
1320
+ return [];
1321
+ }
506
1322
  if (open) {
507
1323
  const finalArgs = resolveCompletedArgs(open.args, args, state);
508
1324
  out.push(...commitToolCall(state, update.value.callId, finalArgs));
@@ -549,8 +1365,10 @@ export function resolvedTurnUsage(state: CursorProtobufEventState): OcxUsage {
549
1365
  export function finalizeTurnEvents(state: CursorProtobufEventState): CursorServerMessage[] {
550
1366
  state.terminated = true;
551
1367
  if (state.openToolCalls.size > 0) {
552
- const openIds = [...state.openToolCalls.keys()].join(", ");
1368
+ const openCallIds = [...state.openToolCalls.keys()];
1369
+ const openIds = openCallIds.join(", ");
553
1370
  // Clear so a second turnEnded (should not happen, but defensive) doesn't re-emit.
1371
+ for (const callId of openCallIds) state.translatorBudget?.closeCall(callId);
554
1372
  state.openToolCalls.clear();
555
1373
  return [{ type: "error", message: `Cursor stream ended with incomplete tool call(s): ${openIds}. Arguments may be truncated; the call was not committed.` }];
556
1374
  }