@yansigit/opencodex 2.31.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 (826) hide show
  1. package/AGENTS_INSTALL.md +109 -0
  2. package/LICENSE +21 -0
  3. package/README.md +303 -0
  4. package/assets/architecture.png +0 -0
  5. package/assets/banner.png +0 -0
  6. package/assets/claude-code-models.gif +0 -0
  7. package/assets/codex-app-picker.png +0 -0
  8. package/bin/ocx.mjs +587 -0
  9. package/bin/package-main.mjs +9 -0
  10. package/gui/dist/assets/index-BNJ7r4Gd.js +102 -0
  11. package/gui/dist/assets/index-CGoDO3uO.css +1 -0
  12. package/gui/dist/favicon.png +0 -0
  13. package/gui/dist/icons.svg +24 -0
  14. package/gui/dist/index.html +25 -0
  15. package/gui/dist/logo.png +0 -0
  16. package/gui/dist/provider-icons/alibaba-color.svg +1 -0
  17. package/gui/dist/provider-icons/antigravity-color.svg +1 -0
  18. package/gui/dist/provider-icons/claude-color.svg +1 -0
  19. package/gui/dist/provider-icons/cline-color.svg +16 -0
  20. package/gui/dist/provider-icons/cloudflare-ai-gateway-color.svg +1 -0
  21. package/gui/dist/provider-icons/commandcode-color.svg +1 -0
  22. package/gui/dist/provider-icons/copilot-color.svg +1 -0
  23. package/gui/dist/provider-icons/cursor-color.svg +2 -0
  24. package/gui/dist/provider-icons/deepseek-color.svg +1 -0
  25. package/gui/dist/provider-icons/discord.svg +1 -0
  26. package/gui/dist/provider-icons/firepass-color.svg +1 -0
  27. package/gui/dist/provider-icons/fireworks-color.svg +1 -0
  28. package/gui/dist/provider-icons/gemini-color.svg +1 -0
  29. package/gui/dist/provider-icons/github-copilot-color.svg +1 -0
  30. package/gui/dist/provider-icons/gitlab-duo-color.svg +1 -0
  31. package/gui/dist/provider-icons/grok.svg +1 -0
  32. package/gui/dist/provider-icons/groq-color.svg +1 -0
  33. package/gui/dist/provider-icons/huggingface-color.svg +1 -0
  34. package/gui/dist/provider-icons/kimi-color.svg +1 -0
  35. package/gui/dist/provider-icons/kiro-color.svg +15 -0
  36. package/gui/dist/provider-icons/lm-studio-color.svg +1 -0
  37. package/gui/dist/provider-icons/mistral-color.svg +1 -0
  38. package/gui/dist/provider-icons/moonshot-color.svg +1 -0
  39. package/gui/dist/provider-icons/nvidia-color.svg +1 -0
  40. package/gui/dist/provider-icons/ollama-color.svg +1 -0
  41. package/gui/dist/provider-icons/openai.svg +1 -0
  42. package/gui/dist/provider-icons/opencode.svg +2 -0
  43. package/gui/dist/provider-icons/openrouter-color.svg +1 -0
  44. package/gui/dist/provider-icons/pi.svg +21 -0
  45. package/gui/dist/provider-icons/qianfan-color.svg +1 -0
  46. package/gui/dist/provider-icons/qwen-portal-color.svg +1 -0
  47. package/gui/dist/provider-icons/telegram.svg +1 -0
  48. package/gui/dist/provider-icons/vercel-ai-gateway-color.svg +1 -0
  49. package/gui/dist/provider-icons/vllm-color.svg +1 -0
  50. package/gui/dist/provider-icons/xiaomi-color.svg +1 -0
  51. package/package.json +108 -0
  52. package/src/AGENTS.md +28 -0
  53. package/src/adapters/anthropic-image-guard.ts +251 -0
  54. package/src/adapters/anthropic-image-normalize.ts +518 -0
  55. package/src/adapters/anthropic-output-schema.ts +137 -0
  56. package/src/adapters/anthropic.ts +1327 -0
  57. package/src/adapters/azure.ts +36 -0
  58. package/src/adapters/base.ts +121 -0
  59. package/src/adapters/client-fingerprint.ts +65 -0
  60. package/src/adapters/cline-pass-deepseek-v4-tool-replay.ts +69 -0
  61. package/src/adapters/command-code.ts +601 -0
  62. package/src/adapters/cursor/arg-codec.ts +38 -0
  63. package/src/adapters/cursor/arg-normalize.ts +104 -0
  64. package/src/adapters/cursor/checkpoint-store.ts +303 -0
  65. package/src/adapters/cursor/cursor-errors.ts +288 -0
  66. package/src/adapters/cursor/discovery.ts +333 -0
  67. package/src/adapters/cursor/effort-map.ts +151 -0
  68. package/src/adapters/cursor/exec-policy.ts +88 -0
  69. package/src/adapters/cursor/framing.ts +250 -0
  70. package/src/adapters/cursor/gen/agent_pb.ts +15274 -0
  71. package/src/adapters/cursor/h2-pool.ts +123 -0
  72. package/src/adapters/cursor/http1-bidi.ts +361 -0
  73. package/src/adapters/cursor/images.ts +704 -0
  74. package/src/adapters/cursor/kv-store.ts +52 -0
  75. package/src/adapters/cursor/live-models.ts +269 -0
  76. package/src/adapters/cursor/live-smoke-gate.ts +41 -0
  77. package/src/adapters/cursor/live-transport.ts +1653 -0
  78. package/src/adapters/cursor/mcp-config.ts +42 -0
  79. package/src/adapters/cursor/mcp-manager.ts +333 -0
  80. package/src/adapters/cursor/message-mapper.ts +49 -0
  81. package/src/adapters/cursor/native-exec-common.ts +76 -0
  82. package/src/adapters/cursor/native-exec-desktop.ts +184 -0
  83. package/src/adapters/cursor/native-exec-fs.ts +332 -0
  84. package/src/adapters/cursor/native-exec-mcp.ts +153 -0
  85. package/src/adapters/cursor/native-exec-network.ts +43 -0
  86. package/src/adapters/cursor/native-exec-shell.ts +547 -0
  87. package/src/adapters/cursor/native-exec-tools.ts +118 -0
  88. package/src/adapters/cursor/native-exec.ts +663 -0
  89. package/src/adapters/cursor/protobuf-events.ts +1381 -0
  90. package/src/adapters/cursor/protobuf-request.ts +1032 -0
  91. package/src/adapters/cursor/request-builder.ts +461 -0
  92. package/src/adapters/cursor/thread-continuity.ts +67 -0
  93. package/src/adapters/cursor/tool-definitions.ts +735 -0
  94. package/src/adapters/cursor/tool-result-normalize.ts +92 -0
  95. package/src/adapters/cursor/transport-retry.ts +132 -0
  96. package/src/adapters/cursor/transport.ts +79 -0
  97. package/src/adapters/cursor/types.ts +79 -0
  98. package/src/adapters/cursor.ts +322 -0
  99. package/src/adapters/google-antigravity-hosts.ts +48 -0
  100. package/src/adapters/google-antigravity-replay.ts +827 -0
  101. package/src/adapters/google-antigravity-tools.ts +105 -0
  102. package/src/adapters/google-antigravity-wire.ts +141 -0
  103. package/src/adapters/google-errors.ts +92 -0
  104. package/src/adapters/google-http.ts +460 -0
  105. package/src/adapters/google-tool-schema.ts +238 -0
  106. package/src/adapters/google-truncation.ts +24 -0
  107. package/src/adapters/google-wire-compiler.ts +232 -0
  108. package/src/adapters/google.ts +1377 -0
  109. package/src/adapters/identity.ts +77 -0
  110. package/src/adapters/image.ts +23 -0
  111. package/src/adapters/kiro-constants.ts +16 -0
  112. package/src/adapters/kiro-errors.ts +208 -0
  113. package/src/adapters/kiro-events.ts +197 -0
  114. package/src/adapters/kiro-images.ts +129 -0
  115. package/src/adapters/kiro-retry.ts +312 -0
  116. package/src/adapters/kiro-thinking.ts +112 -0
  117. package/src/adapters/kiro-tool-fallback.ts +36 -0
  118. package/src/adapters/kiro-tools.ts +224 -0
  119. package/src/adapters/kiro-truncation.ts +33 -0
  120. package/src/adapters/kiro-wire.ts +129 -0
  121. package/src/adapters/kiro.ts +1936 -0
  122. package/src/adapters/mimo-free.ts +280 -0
  123. package/src/adapters/openai-chat-url.ts +11 -0
  124. package/src/adapters/openai-chat.ts +1980 -0
  125. package/src/adapters/openai-responses-url.ts +16 -0
  126. package/src/adapters/openai-responses.ts +1911 -0
  127. package/src/adapters/registry.ts +175 -0
  128. package/src/adapters/responses-tool-schema.ts +67 -0
  129. package/src/adapters/run-turn-queue.ts +114 -0
  130. package/src/adapters/tool-call-id.ts +119 -0
  131. package/src/adapters/tool-catalog-nudge.ts +154 -0
  132. package/src/adapters/upstream-http-error.ts +48 -0
  133. package/src/adapters/xai-web-search.ts +185 -0
  134. package/src/bridge.ts +1986 -0
  135. package/src/chat/inbound.ts +319 -0
  136. package/src/chat/outbound.ts +821 -0
  137. package/src/claude/agents-inject.ts +266 -0
  138. package/src/claude/alias.ts +149 -0
  139. package/src/claude/auth-detect.ts +229 -0
  140. package/src/claude/auth-mode-migration.ts +32 -0
  141. package/src/claude/auth-mode.ts +62 -0
  142. package/src/claude/context-windows.ts +205 -0
  143. package/src/claude/desktop-3p-guard.ts +35 -0
  144. package/src/claude/desktop-3p-paths.ts +84 -0
  145. package/src/claude/desktop-3p.ts +615 -0
  146. package/src/claude/desktop-health.ts +26 -0
  147. package/src/claude/desktop-profile.ts +263 -0
  148. package/src/claude/gateway-cache.ts +107 -0
  149. package/src/claude/inbound-debug.ts +163 -0
  150. package/src/claude/inbound.ts +578 -0
  151. package/src/claude/model-info.ts +174 -0
  152. package/src/claude/outbound.ts +926 -0
  153. package/src/cli/access.ts +108 -0
  154. package/src/cli/account-api.ts +302 -0
  155. package/src/cli/account-auth.ts +250 -0
  156. package/src/cli/account-catalog-refresh.ts +14 -0
  157. package/src/cli/account-extended.ts +737 -0
  158. package/src/cli/account-main.ts +317 -0
  159. package/src/cli/account.ts +299 -0
  160. package/src/cli/agent-driven.ts +70 -0
  161. package/src/cli/agent.ts +290 -0
  162. package/src/cli/catalog-prewarm.ts +27 -0
  163. package/src/cli/claude-agent-startup-sync.ts +73 -0
  164. package/src/cli/claude-desktop.ts +213 -0
  165. package/src/cli/claude.ts +355 -0
  166. package/src/cli/codex-log-guard-doctor.ts +103 -0
  167. package/src/cli/codex-shim-autorestore.ts +47 -0
  168. package/src/cli/codex-shim-readiness.ts +76 -0
  169. package/src/cli/combo.ts +127 -0
  170. package/src/cli/config-command.ts +209 -0
  171. package/src/cli/debug.ts +228 -0
  172. package/src/cli/dispatch.ts +585 -0
  173. package/src/cli/doctor.ts +1202 -0
  174. package/src/cli/ensure-desired-integrations.ts +152 -0
  175. package/src/cli/export-command.ts +213 -0
  176. package/src/cli/help.ts +101 -0
  177. package/src/cli/index.ts +973 -0
  178. package/src/cli/init.ts +211 -0
  179. package/src/cli/integrations.ts +260 -0
  180. package/src/cli/interactive-confirm.ts +133 -0
  181. package/src/cli/lab.ts +607 -0
  182. package/src/cli/launcher-context.ts +77 -0
  183. package/src/cli/minimax.ts +497 -0
  184. package/src/cli/models-runtime.ts +245 -0
  185. package/src/cli/models.ts +422 -0
  186. package/src/cli/observe.ts +206 -0
  187. package/src/cli/opencode.ts +588 -0
  188. package/src/cli/provider-replit.ts +232 -0
  189. package/src/cli/provider-runtime.ts +179 -0
  190. package/src/cli/provider.ts +492 -0
  191. package/src/cli/ready.ts +301 -0
  192. package/src/cli/registry.ts +422 -0
  193. package/src/cli/replit-gateway-key-input.ts +138 -0
  194. package/src/cli/root.ts +86 -0
  195. package/src/cli/route-policy.ts +92 -0
  196. package/src/cli/runtime-api.ts +328 -0
  197. package/src/cli/star-prompt.ts +211 -0
  198. package/src/cli/status-oauth.ts +78 -0
  199. package/src/cli/status.ts +328 -0
  200. package/src/cli/system-command.ts +112 -0
  201. package/src/cli/system-restart-client.ts +146 -0
  202. package/src/cli/tray-proxy.ts +199 -0
  203. package/src/cli/v2.ts +268 -0
  204. package/src/cli.ts +10 -0
  205. package/src/clients/config-export.ts +1704 -0
  206. package/src/codex/account-id.ts +34 -0
  207. package/src/codex/account-label.ts +47 -0
  208. package/src/codex/account-lifecycle.ts +172 -0
  209. package/src/codex/account-namespace-match.ts +63 -0
  210. package/src/codex/account-namespaces.ts +195 -0
  211. package/src/codex/account-pause.ts +20 -0
  212. package/src/codex/account-priority.ts +83 -0
  213. package/src/codex/account-runtime-state.ts +31 -0
  214. package/src/codex/account-store.ts +544 -0
  215. package/src/codex/account-usability.ts +43 -0
  216. package/src/codex/admission.ts +256 -0
  217. package/src/codex/affinity-debug.ts +162 -0
  218. package/src/codex/agent-roles-sync.ts +225 -0
  219. package/src/codex/agent-roles.ts +238 -0
  220. package/src/codex/app-server-processes.ts +1143 -0
  221. package/src/codex/app-server-restart-service.ts +232 -0
  222. package/src/codex/auth-api.ts +2147 -0
  223. package/src/codex/auth-collision.ts +109 -0
  224. package/src/codex/auth-context.ts +665 -0
  225. package/src/codex/autostart-health.ts +156 -0
  226. package/src/codex/catalog/account-models.ts +67 -0
  227. package/src/codex/catalog/aggregation.ts +436 -0
  228. package/src/codex/catalog/bundled.ts +549 -0
  229. package/src/codex/catalog/effort.ts +446 -0
  230. package/src/codex/catalog/filesystem-evidence.ts +302 -0
  231. package/src/codex/catalog/kinds.ts +2 -0
  232. package/src/codex/catalog/metadata.ts +664 -0
  233. package/src/codex/catalog/native-models.ts +72 -0
  234. package/src/codex/catalog/parsing.ts +650 -0
  235. package/src/codex/catalog/provider-fetch.ts +2064 -0
  236. package/src/codex/catalog/sync.ts +1883 -0
  237. package/src/codex/catalog-admission.ts +199 -0
  238. package/src/codex/catalog-refresh-status.ts +105 -0
  239. package/src/codex/catalog-write-serialization.ts +242 -0
  240. package/src/codex/catalog.ts +14 -0
  241. package/src/codex/codex-write-lock.ts +384 -0
  242. package/src/codex/convergence-types.ts +614 -0
  243. package/src/codex/convergence.ts +651 -0
  244. package/src/codex/coordinator-doctor.ts +332 -0
  245. package/src/codex/custom-model-catalog-migration.ts +176 -0
  246. package/src/codex/data/upstream-models.json +830 -0
  247. package/src/codex/desired-state.ts +230 -0
  248. package/src/codex/exec-invocation.ts +22 -0
  249. package/src/codex/features.ts +1566 -0
  250. package/src/codex/generation.ts +202 -0
  251. package/src/codex/history-job.ts +407 -0
  252. package/src/codex/history-lock.ts +242 -0
  253. package/src/codex/history-migration-guardian.ts +108 -0
  254. package/src/codex/history-provider.ts +979 -0
  255. package/src/codex/history-transition.ts +105 -0
  256. package/src/codex/history-worker.ts +220 -0
  257. package/src/codex/home.ts +206 -0
  258. package/src/codex/inject-coordination.ts +290 -0
  259. package/src/codex/inject.ts +1733 -0
  260. package/src/codex/injected-marker.ts +106 -0
  261. package/src/codex/integration-record.ts +266 -0
  262. package/src/codex/internal/catalog-writer.ts +203 -0
  263. package/src/codex/internal/history-writer.ts +80 -0
  264. package/src/codex/journal.ts +225 -0
  265. package/src/codex/log-guard/inspect.ts +506 -0
  266. package/src/codex/log-guard/lock.ts +150 -0
  267. package/src/codex/log-guard/maintenance.ts +403 -0
  268. package/src/codex/log-guard/path-safety.ts +88 -0
  269. package/src/codex/log-guard/policy.ts +44 -0
  270. package/src/codex/log-guard/processes.ts +205 -0
  271. package/src/codex/log-guard/protection.ts +489 -0
  272. package/src/codex/log-guard/sqlite-errors.ts +9 -0
  273. package/src/codex/main-account-cache.ts +56 -0
  274. package/src/codex/main-account.ts +68 -0
  275. package/src/codex/management-convergence.ts +167 -0
  276. package/src/codex/model-cache.ts +273 -0
  277. package/src/codex/model-entitlements.ts +353 -0
  278. package/src/codex/native-main-admission.ts +47 -0
  279. package/src/codex/native-main-auth-temp.ts +187 -0
  280. package/src/codex/native-main-claim.ts +178 -0
  281. package/src/codex/native-main-lock-file.ts +162 -0
  282. package/src/codex/native-main-owner.ts +329 -0
  283. package/src/codex/native-profile-api.ts +247 -0
  284. package/src/codex/native-profile-manager.ts +1531 -0
  285. package/src/codex/native-profile-processes.ts +121 -0
  286. package/src/codex/native-profile-recovery.ts +99 -0
  287. package/src/codex/native-profile-stage-store.ts +387 -0
  288. package/src/codex/native-profile-startup.ts +492 -0
  289. package/src/codex/native-profile-store.ts +855 -0
  290. package/src/codex/native-profile-types.ts +120 -0
  291. package/src/codex/native-residue.ts +682 -0
  292. package/src/codex/paths.ts +144 -0
  293. package/src/codex/plan-from-token.ts +140 -0
  294. package/src/codex/plan.ts +40 -0
  295. package/src/codex/plugins-doctor.ts +242 -0
  296. package/src/codex/pool-rotation.ts +295 -0
  297. package/src/codex/project-config-warnings.ts +425 -0
  298. package/src/codex/prompt-journal.ts +352 -0
  299. package/src/codex/prompt-layers.ts +967 -0
  300. package/src/codex/prompt-lock.ts +143 -0
  301. package/src/codex/quota-rejection.ts +298 -0
  302. package/src/codex/quota.ts +573 -0
  303. package/src/codex/refresh.ts +62 -0
  304. package/src/codex/reset-credit-recovery.ts +1044 -0
  305. package/src/codex/routing.ts +1888 -0
  306. package/src/codex/runtime.ts +659 -0
  307. package/src/codex/shim.ts +2170 -0
  308. package/src/codex/subagent-defaults.ts +550 -0
  309. package/src/codex/subagent-model-fallback.ts +784 -0
  310. package/src/codex/sync.ts +319 -0
  311. package/src/codex/transition-state.ts +612 -0
  312. package/src/codex/upstream-host-health.ts +368 -0
  313. package/src/codex/user-identity.ts +557 -0
  314. package/src/codex/warmup.ts +298 -0
  315. package/src/codex/websocket-registry.ts +100 -0
  316. package/src/codex/write-coordination.ts +114 -0
  317. package/src/combos/failover.ts +160 -0
  318. package/src/combos/index.ts +45 -0
  319. package/src/combos/request.ts +94 -0
  320. package/src/combos/resolve.ts +232 -0
  321. package/src/combos/types.ts +398 -0
  322. package/src/config/provider-name.ts +24 -0
  323. package/src/config.ts +4041 -0
  324. package/src/fork/register.ts +3 -0
  325. package/src/generated/compatibility-version.json +3116 -0
  326. package/src/generated/model-metadata.ts +106 -0
  327. package/src/github/star-state.ts +203 -0
  328. package/src/grok/inject.ts +530 -0
  329. package/src/grok/inspect.ts +45 -0
  330. package/src/grok/status.ts +121 -0
  331. package/src/grok/sync.ts +66 -0
  332. package/src/images/artifacts.ts +516 -0
  333. package/src/images/fulfill-video.ts +163 -0
  334. package/src/images/fulfill.ts +149 -0
  335. package/src/images/index.ts +4 -0
  336. package/src/images/loop.ts +955 -0
  337. package/src/images/plan.ts +143 -0
  338. package/src/images/synthetic-tool.ts +133 -0
  339. package/src/images/types.ts +41 -0
  340. package/src/images/xai-client.ts +141 -0
  341. package/src/images/xai-video-client.ts +163 -0
  342. package/src/index.ts +22 -0
  343. package/src/integrations/config-io.ts +269 -0
  344. package/src/integrations/journal.ts +315 -0
  345. package/src/integrations/merge.ts +135 -0
  346. package/src/integrations/mutation-flight.ts +71 -0
  347. package/src/integrations/native/ownership-preflight.ts +202 -0
  348. package/src/integrations/omp-yaml-source.ts +358 -0
  349. package/src/integrations/owned-refresh.ts +74 -0
  350. package/src/integrations/ownership.ts +111 -0
  351. package/src/integrations/registry.ts +159 -0
  352. package/src/integrations/serialize.ts +314 -0
  353. package/src/integrations/state.ts +361 -0
  354. package/src/integrations/store.ts +103 -0
  355. package/src/integrations/writer-lock.ts +98 -0
  356. package/src/integrations/writer.ts +691 -0
  357. package/src/lab/artifacts/sanitize.ts +586 -0
  358. package/src/lab/artifacts/secure-fs.ts +475 -0
  359. package/src/lab/artifacts/store.ts +310 -0
  360. package/src/lab/automation/budgets.ts +78 -0
  361. package/src/lab/automation/config-persistence.ts +256 -0
  362. package/src/lab/automation/constants.ts +39 -0
  363. package/src/lab/automation/cooldown.ts +103 -0
  364. package/src/lab/automation/dispatch.ts +211 -0
  365. package/src/lab/automation/index.ts +13 -0
  366. package/src/lab/automation/orchestrator.ts +499 -0
  367. package/src/lab/automation/persistence.ts +512 -0
  368. package/src/lab/automation/planner.ts +371 -0
  369. package/src/lab/automation/policy.ts +136 -0
  370. package/src/lab/automation/queue.ts +191 -0
  371. package/src/lab/automation/recovery.ts +24 -0
  372. package/src/lab/automation/route-context.ts +21 -0
  373. package/src/lab/automation/run-key.ts +44 -0
  374. package/src/lab/automation/runs-query.ts +34 -0
  375. package/src/lab/automation/types.ts +160 -0
  376. package/src/lab/conformance/assertion.ts +325 -0
  377. package/src/lab/conformance/digest.ts +22 -0
  378. package/src/lab/conformance/executor.ts +741 -0
  379. package/src/lab/conformance/fixture-provider.ts +27 -0
  380. package/src/lab/conformance/fixtures/live-v1-cases.json +175 -0
  381. package/src/lab/conformance/fixtures/protocol-v1-cases.json +461 -0
  382. package/src/lab/conformance/harness-budget.ts +47 -0
  383. package/src/lab/conformance/index.ts +5 -0
  384. package/src/lab/conformance/jcs.ts +64 -0
  385. package/src/lab/conformance/json-pointer.ts +39 -0
  386. package/src/lab/conformance/manifest.ts +180 -0
  387. package/src/lab/conformance/mcp-stub.ts +179 -0
  388. package/src/lab/conformance/negative-controls.ts +164 -0
  389. package/src/lab/conformance/observation.ts +355 -0
  390. package/src/lab/conformance/runner.ts +68 -0
  391. package/src/lab/conformance/sse-normalize.ts +59 -0
  392. package/src/lab/conformance/suite-manifest.ts +78 -0
  393. package/src/lab/conformance/types.ts +214 -0
  394. package/src/lab/constants.ts +126 -0
  395. package/src/lab/digest.ts +64 -0
  396. package/src/lab/events/errors.ts +9 -0
  397. package/src/lab/events/limits.ts +117 -0
  398. package/src/lab/events/types.ts +229 -0
  399. package/src/lab/events/validate.ts +781 -0
  400. package/src/lab/fabric/constants.ts +40 -0
  401. package/src/lab/fabric/executor.ts +492 -0
  402. package/src/lab/fabric/index.ts +80 -0
  403. package/src/lab/fabric/manifest.ts +222 -0
  404. package/src/lab/fabric/observe.ts +489 -0
  405. package/src/lab/fabric/patch.ts +79 -0
  406. package/src/lab/fabric/producer-child.ts +139 -0
  407. package/src/lab/fabric/producer-isolate.ts +276 -0
  408. package/src/lab/fabric/producer-protocol.ts +61 -0
  409. package/src/lab/fabric/scratch.ts +439 -0
  410. package/src/lab/fabric/subject.ts +106 -0
  411. package/src/lab/fabric/types.ts +134 -0
  412. package/src/lab/fabric/verifier.ts +98 -0
  413. package/src/lab/index.ts +54 -0
  414. package/src/lab/ledger/artifact-refs.ts +127 -0
  415. package/src/lab/ledger/invalidation.ts +136 -0
  416. package/src/lab/ledger/purge.ts +310 -0
  417. package/src/lab/ledger/store.ts +532 -0
  418. package/src/lab/live/credential-lease.ts +53 -0
  419. package/src/lab/live/destination.ts +155 -0
  420. package/src/lab/live/executor.ts +336 -0
  421. package/src/lab/live/inert-tools.ts +56 -0
  422. package/src/lab/live/manifest.ts +85 -0
  423. package/src/lab/live/mcp-loopback.ts +57 -0
  424. package/src/lab/live/runner.ts +19 -0
  425. package/src/lab/live/sandbox.ts +61 -0
  426. package/src/lab/live/suite-manifest.ts +41 -0
  427. package/src/lab/live/transport.ts +118 -0
  428. package/src/lab/live/types.ts +197 -0
  429. package/src/lab/observe/from-conformance.ts +301 -0
  430. package/src/lab/observe/from-live.ts +117 -0
  431. package/src/lab/paths.ts +153 -0
  432. package/src/lab/projection/rebuild.ts +495 -0
  433. package/src/lab/projection/schema.ts +135 -0
  434. package/src/lab/projection/verdicts.ts +474 -0
  435. package/src/lab/projection/verification.ts +412 -0
  436. package/src/lab/public/bundle.ts +217 -0
  437. package/src/lab/public/community-authority.ts +175 -0
  438. package/src/lab/public/community-files.ts +29 -0
  439. package/src/lab/public/community.ts +479 -0
  440. package/src/lab/public/file-safety.ts +155 -0
  441. package/src/lab/public/ids.ts +26 -0
  442. package/src/lab/public/index.ts +16 -0
  443. package/src/lab/public/mutation-lock.ts +424 -0
  444. package/src/lab/public/operator.ts +353 -0
  445. package/src/lab/public/origin-purge.ts +79 -0
  446. package/src/lab/public/origin.ts +203 -0
  447. package/src/lab/public/privacy.ts +143 -0
  448. package/src/lab/public/private-file.ts +261 -0
  449. package/src/lab/public/project.ts +124 -0
  450. package/src/lab/public/purge-test-fault.ts +21 -0
  451. package/src/lab/public/purge.ts +223 -0
  452. package/src/lab/public/registry.ts +44 -0
  453. package/src/lab/public/revocation.ts +252 -0
  454. package/src/lab/public/signature.ts +243 -0
  455. package/src/lab/public/storage.ts +105 -0
  456. package/src/lab/public/strict-json.ts +206 -0
  457. package/src/lab/public/time.ts +26 -0
  458. package/src/lab/public/types.ts +172 -0
  459. package/src/lab/public/validate.ts +391 -0
  460. package/src/lab/query/catalog.ts +101 -0
  461. package/src/lab/query/connection.ts +107 -0
  462. package/src/lab/query/constants.ts +4 -0
  463. package/src/lab/query/cursor.ts +132 -0
  464. package/src/lab/query/dto-map.ts +277 -0
  465. package/src/lab/query/errors.ts +22 -0
  466. package/src/lab/query/freshness.ts +53 -0
  467. package/src/lab/query/index.ts +45 -0
  468. package/src/lab/query/latest-observation.ts +59 -0
  469. package/src/lab/query/passive-production.ts +159 -0
  470. package/src/lab/query/queries.ts +444 -0
  471. package/src/lab/query/types.ts +266 -0
  472. package/src/lab/subject/behavior-fingerprint.ts +77 -0
  473. package/src/lab/subject/installation-salt.ts +112 -0
  474. package/src/lab/subject/protocol-subject.ts +80 -0
  475. package/src/lab/subject/route-subject.ts +74 -0
  476. package/src/lib/abort.ts +146 -0
  477. package/src/lib/admin-secrets.ts +25 -0
  478. package/src/lib/admission.ts +83 -0
  479. package/src/lib/app-owned-memory-stores.ts +195 -0
  480. package/src/lib/app-owned-memory.ts +265 -0
  481. package/src/lib/bounded-body.ts +346 -0
  482. package/src/lib/bun-binary-validator.d.mts +3 -0
  483. package/src/lib/bun-binary-validator.mjs +18 -0
  484. package/src/lib/bun-runtime.ts +184 -0
  485. package/src/lib/bun-stream-caps.ts +130 -0
  486. package/src/lib/codex-restart-contract.ts +120 -0
  487. package/src/lib/config-ownership.ts +364 -0
  488. package/src/lib/crash-guard.ts +344 -0
  489. package/src/lib/debug-log-buffer.ts +83 -0
  490. package/src/lib/debug-settings.ts +108 -0
  491. package/src/lib/debug.ts +31 -0
  492. package/src/lib/destination-policy.ts +380 -0
  493. package/src/lib/errors.ts +406 -0
  494. package/src/lib/eventstream-decoder.ts +253 -0
  495. package/src/lib/fabric-task-execution-authority.ts +7 -0
  496. package/src/lib/fabric-task-host.ts +29 -0
  497. package/src/lib/gcp-adc.ts +341 -0
  498. package/src/lib/injection-debug-log.ts +58 -0
  499. package/src/lib/lab-activation.ts +223 -0
  500. package/src/lib/lab-live-execution-authority.ts +13 -0
  501. package/src/lib/lab-live-host.ts +30 -0
  502. package/src/lib/lab-live-pinned-sender.ts +56 -0
  503. package/src/lib/lab-live-route-production.ts +130 -0
  504. package/src/lib/lab-passive-linker-registration.ts +26 -0
  505. package/src/lib/local-management-attestation.ts +51 -0
  506. package/src/lib/local-management-capability.ts +100 -0
  507. package/src/lib/local-provider-reload-contract.ts +100 -0
  508. package/src/lib/open-url.ts +25 -0
  509. package/src/lib/optional-shutdown-hooks.ts +57 -0
  510. package/src/lib/pinned-http.ts +270 -0
  511. package/src/lib/privacy.ts +20 -0
  512. package/src/lib/process-control.ts +168 -0
  513. package/src/lib/provider-outbound.ts +210 -0
  514. package/src/lib/provider-url.ts +14 -0
  515. package/src/lib/proxy-env.ts +18 -0
  516. package/src/lib/redact.ts +521 -0
  517. package/src/lib/retry-after.ts +55 -0
  518. package/src/lib/self-launch-argv.ts +15 -0
  519. package/src/lib/server-resource-ownership.ts +71 -0
  520. package/src/lib/service-secrets.ts +25 -0
  521. package/src/lib/shadow-call.ts +61 -0
  522. package/src/lib/sidecar-tracker.ts +52 -0
  523. package/src/lib/sse-decoder.ts +364 -0
  524. package/src/lib/state-store-registrations.ts +119 -0
  525. package/src/lib/state-store-sweeper.ts +184 -0
  526. package/src/lib/system-restart-contract.ts +73 -0
  527. package/src/lib/test-home-guard.ts +90 -0
  528. package/src/lib/token-estimate.ts +86 -0
  529. package/src/lib/tool-argument-integers.ts +202 -0
  530. package/src/lib/translator-budget.ts +400 -0
  531. package/src/lib/upstream-http-version.ts +57 -0
  532. package/src/lib/upstream-reachability.ts +95 -0
  533. package/src/lib/upstream-retry.ts +392 -0
  534. package/src/lib/win-exec.ts +115 -0
  535. package/src/lib/win-paths.ts +68 -0
  536. package/src/lib/windows-atomic-replace.ts +156 -0
  537. package/src/lib/windows-elevation.ts +773 -0
  538. package/src/lib/windows-secret-acl.ts +854 -0
  539. package/src/lib/windows-service-wrappers.ts +72 -0
  540. package/src/lib/windows-text.ts +106 -0
  541. package/src/lib/windows-user-principal.ts +341 -0
  542. package/src/lib/winsw.ts +403 -0
  543. package/src/oauth/account-import/google-antigravity-adapter.ts +74 -0
  544. package/src/oauth/account-import/index.ts +15 -0
  545. package/src/oauth/account-import/parser.ts +83 -0
  546. package/src/oauth/account-import/registry.ts +18 -0
  547. package/src/oauth/account-import/service.ts +75 -0
  548. package/src/oauth/account-import/types.ts +91 -0
  549. package/src/oauth/anthropic-routing.ts +594 -0
  550. package/src/oauth/anthropic.ts +188 -0
  551. package/src/oauth/antigravity-routing.ts +151 -0
  552. package/src/oauth/callback-server.ts +300 -0
  553. package/src/oauth/chatgpt.ts +161 -0
  554. package/src/oauth/command-code.ts +239 -0
  555. package/src/oauth/cursor.ts +252 -0
  556. package/src/oauth/github-copilot.ts +428 -0
  557. package/src/oauth/google-antigravity.ts +262 -0
  558. package/src/oauth/health.ts +407 -0
  559. package/src/oauth/index.ts +1504 -0
  560. package/src/oauth/key-providers.ts +124 -0
  561. package/src/oauth/kimi.ts +227 -0
  562. package/src/oauth/kiro-credentials.ts +726 -0
  563. package/src/oauth/kiro.ts +621 -0
  564. package/src/oauth/local-token-detect.ts +130 -0
  565. package/src/oauth/log.ts +50 -0
  566. package/src/oauth/login-cli.ts +223 -0
  567. package/src/oauth/nous.ts +798 -0
  568. package/src/oauth/pkce.ts +15 -0
  569. package/src/oauth/store.ts +728 -0
  570. package/src/oauth/token-guardian.ts +309 -0
  571. package/src/oauth/types.ts +62 -0
  572. package/src/oauth/xai.ts +241 -0
  573. package/src/providers/alibaba-region-backup.ts +75 -0
  574. package/src/providers/alibaba-region-migration.ts +156 -0
  575. package/src/providers/alibaba-region-startup.ts +36 -0
  576. package/src/providers/antigravity-models.ts +695 -0
  577. package/src/providers/antigravity-quota.ts +216 -0
  578. package/src/providers/api-keys.ts +140 -0
  579. package/src/providers/base-url-choices.ts +74 -0
  580. package/src/providers/codex-capacity.ts +292 -0
  581. package/src/providers/command-code-efforts.ts +144 -0
  582. package/src/providers/context-cap.ts +82 -0
  583. package/src/providers/cursor-pool.ts +72 -0
  584. package/src/providers/derive.ts +586 -0
  585. package/src/providers/fastwire.ts +501 -0
  586. package/src/providers/free-directory.ts +187 -0
  587. package/src/providers/github-copilot-transport.ts +56 -0
  588. package/src/providers/google-vertex-location.ts +14 -0
  589. package/src/providers/key-failover.ts +271 -0
  590. package/src/providers/kiro-models.ts +67 -0
  591. package/src/providers/label.ts +19 -0
  592. package/src/providers/model-discovery-limits.ts +16 -0
  593. package/src/providers/model-discovery.ts +449 -0
  594. package/src/providers/model-rename-migration.ts +255 -0
  595. package/src/providers/model-rename-startup.ts +28 -0
  596. package/src/providers/openai-sidecar.ts +243 -0
  597. package/src/providers/openai-tier-startup.ts +56 -0
  598. package/src/providers/openai-tiers.ts +423 -0
  599. package/src/providers/openai-virtual-models.ts +83 -0
  600. package/src/providers/opencode-zen-rate-limit.ts +102 -0
  601. package/src/providers/openrouter-routing.ts +102 -0
  602. package/src/providers/provider-id-rewrite.ts +185 -0
  603. package/src/providers/quota.ts +2345 -0
  604. package/src/providers/registry.ts +2918 -0
  605. package/src/providers/replit/constants.ts +27 -0
  606. package/src/providers/replit/derive.ts +85 -0
  607. package/src/providers/replit/headers.ts +28 -0
  608. package/src/providers/replit/origin.ts +55 -0
  609. package/src/providers/replit/pair-install-response.ts +72 -0
  610. package/src/providers/replit/probe.ts +199 -0
  611. package/src/providers/replit/setup.ts +350 -0
  612. package/src/providers/request-pacing.ts +310 -0
  613. package/src/providers/service-tier.ts +277 -0
  614. package/src/providers/slug-codec.ts +103 -0
  615. package/src/providers/static-model-discovery.ts +86 -0
  616. package/src/providers/xai-responses-opt-in.ts +15 -0
  617. package/src/providers/xai-transport.ts +148 -0
  618. package/src/reasoning-effort.ts +183 -0
  619. package/src/responses/compaction.ts +142 -0
  620. package/src/responses/custom-tool-compat.ts +266 -0
  621. package/src/responses/hosted-tool-policy.ts +9 -0
  622. package/src/responses/namespace-tool-compat.ts +355 -0
  623. package/src/responses/parser.ts +838 -0
  624. package/src/responses/provider-continuation.ts +98 -0
  625. package/src/responses/provider-opaque-metadata.ts +73 -0
  626. package/src/responses/reasoning-envelope.ts +60 -0
  627. package/src/responses/reasoning-replay-cache.ts +426 -0
  628. package/src/responses/schema.ts +165 -0
  629. package/src/responses/spill-store.ts +459 -0
  630. package/src/responses/state.ts +1433 -0
  631. package/src/responses/thought-signature-replay.ts +347 -0
  632. package/src/responses/tool-groups.ts +19 -0
  633. package/src/responses/tool-search-compat.ts +301 -0
  634. package/src/responses/truncated-stop-reason.ts +60 -0
  635. package/src/router.ts +761 -0
  636. package/src/routing/analytics.ts +378 -0
  637. package/src/routing/capability.ts +244 -0
  638. package/src/routing/compatibility/assemble.ts +73 -0
  639. package/src/routing/compatibility/behavior.ts +278 -0
  640. package/src/routing/compatibility/catalog.ts +99 -0
  641. package/src/routing/compatibility/endpoint.ts +52 -0
  642. package/src/routing/compatibility/lab-evidence-provider.ts +130 -0
  643. package/src/routing/compatibility/policy.ts +181 -0
  644. package/src/routing/compatibility/provider-slot.ts +56 -0
  645. package/src/routing/compatibility/reader.ts +110 -0
  646. package/src/routing/compatibility/subject.ts +191 -0
  647. package/src/routing/compatibility/types.ts +64 -0
  648. package/src/routing/compatibility/version.ts +104 -0
  649. package/src/routing/cost.ts +77 -0
  650. package/src/routing/evaluator.ts +495 -0
  651. package/src/routing/health.ts +412 -0
  652. package/src/routing/history/cursor.ts +43 -0
  653. package/src/routing/history/indexer.ts +605 -0
  654. package/src/routing/history/schema.ts +72 -0
  655. package/src/routing/profile-namespace.ts +15 -0
  656. package/src/routing/profile.ts +547 -0
  657. package/src/routing/quota.ts +145 -0
  658. package/src/routing/request-evidence.ts +45 -0
  659. package/src/routing/trace.ts +776 -0
  660. package/src/server/adapter-resolve.ts +53 -0
  661. package/src/server/auth-cors.ts +751 -0
  662. package/src/server/background-lifecycle.ts +182 -0
  663. package/src/server/chat-completions.ts +442 -0
  664. package/src/server/chat-native-sse.ts +331 -0
  665. package/src/server/chat-native.ts +426 -0
  666. package/src/server/claude-messages.ts +1030 -0
  667. package/src/server/direct-local-http.ts +347 -0
  668. package/src/server/effort-policy.ts +190 -0
  669. package/src/server/github-copilot-responses-repair.ts +338 -0
  670. package/src/server/gui-static.ts +152 -0
  671. package/src/server/image-retry.ts +42 -0
  672. package/src/server/images.ts +568 -0
  673. package/src/server/index.ts +1813 -0
  674. package/src/server/lifecycle.ts +498 -0
  675. package/src/server/live.ts +717 -0
  676. package/src/server/local-management-read-client.ts +90 -0
  677. package/src/server/local-provider-reload-client.ts +137 -0
  678. package/src/server/management/agent-settings-routes.ts +1433 -0
  679. package/src/server/management/api-access.ts +141 -0
  680. package/src/server/management/api-key-usage.ts +193 -0
  681. package/src/server/management/body.ts +41 -0
  682. package/src/server/management/combo-routes.ts +263 -0
  683. package/src/server/management/config-routes.ts +835 -0
  684. package/src/server/management/context.ts +113 -0
  685. package/src/server/management/integration-routes.ts +498 -0
  686. package/src/server/management/lab-automation-routes.ts +206 -0
  687. package/src/server/management/lab-routes.ts +563 -0
  688. package/src/server/management/logs-usage-routes.ts +586 -0
  689. package/src/server/management/model-routes.ts +560 -0
  690. package/src/server/management/model-rows.ts +163 -0
  691. package/src/server/management/native-integration-routes.ts +769 -0
  692. package/src/server/management/oauth-account-routes.ts +637 -0
  693. package/src/server/management/provider-capability-config.ts +48 -0
  694. package/src/server/management/provider-routes.ts +1033 -0
  695. package/src/server/management/replit-provider-routes.ts +86 -0
  696. package/src/server/management/request-history-routes.ts +191 -0
  697. package/src/server/management/routing-analytics-routes.ts +74 -0
  698. package/src/server/management/routing-profile-routes.ts +385 -0
  699. package/src/server/management/shared.ts +286 -0
  700. package/src/server/management/sidebar-routes.ts +106 -0
  701. package/src/server/management/storage-log-guard-routes.ts +186 -0
  702. package/src/server/management/sync-response.ts +69 -0
  703. package/src/server/management/system-restart.ts +435 -0
  704. package/src/server/management/system-routes.ts +194 -0
  705. package/src/server/management/usage-summary-cache.ts +94 -0
  706. package/src/server/management/vision-sidecar-options.ts +167 -0
  707. package/src/server/management/web-search-sidecar-options.ts +120 -0
  708. package/src/server/management-api.ts +314 -0
  709. package/src/server/management-auth.ts +482 -0
  710. package/src/server/memory-watchdog.ts +156 -0
  711. package/src/server/passive-route-linker.ts +66 -0
  712. package/src/server/port-reclaim.ts +307 -0
  713. package/src/server/ports.ts +156 -0
  714. package/src/server/proxy-liveness.ts +328 -0
  715. package/src/server/readiness.ts +99 -0
  716. package/src/server/relay-eager.ts +353 -0
  717. package/src/server/relay.ts +1209 -0
  718. package/src/server/request-decompress.ts +239 -0
  719. package/src/server/request-log-conversation.ts +168 -0
  720. package/src/server/request-log.ts +1259 -0
  721. package/src/server/responses/agent-task-recovery-cache.ts +143 -0
  722. package/src/server/responses/agent-task-recovery.ts +465 -0
  723. package/src/server/responses/collaboration.ts +551 -0
  724. package/src/server/responses/compact.ts +771 -0
  725. package/src/server/responses/core.ts +5389 -0
  726. package/src/server/responses/empty-completion-guard.ts +276 -0
  727. package/src/server/responses/encrypted-payload.ts +331 -0
  728. package/src/server/responses/fetch-helpers.ts +232 -0
  729. package/src/server/responses/input-admission.ts +185 -0
  730. package/src/server/responses/pacing-overload.ts +13 -0
  731. package/src/server/responses/passthrough-error.ts +78 -0
  732. package/src/server/responses/policy-fallback.ts +178 -0
  733. package/src/server/responses/responses-field-backfill.ts +251 -0
  734. package/src/server/responses/terminal-guard.ts +251 -0
  735. package/src/server/responses/upstream-error.ts +53 -0
  736. package/src/server/responses/ws-upstream.ts +308 -0
  737. package/src/server/responses-custom-tool-repair.ts +282 -0
  738. package/src/server/responses-image-gen-repair.ts +132 -0
  739. package/src/server/responses-item-id-repair.ts +272 -0
  740. package/src/server/responses-json-events.ts +90 -0
  741. package/src/server/responses-model-rewrite.ts +29 -0
  742. package/src/server/responses-reasoning-summary-rewrite.ts +178 -0
  743. package/src/server/responses-snapshot-repair.ts +621 -0
  744. package/src/server/responses-terminal-repair.ts +342 -0
  745. package/src/server/responses-tool-search-repair.ts +267 -0
  746. package/src/server/responses-undeclared-tool-guard.ts +153 -0
  747. package/src/server/responses.ts +25 -0
  748. package/src/server/search.ts +201 -0
  749. package/src/server/sse-frame-buffer.ts +292 -0
  750. package/src/server/sse-payload-rewrite.ts +263 -0
  751. package/src/server/startup-action-control.ts +315 -0
  752. package/src/server/startup-health-cache.ts +131 -0
  753. package/src/server/system-env.ts +484 -0
  754. package/src/server/windows-tcp-drop.ts +184 -0
  755. package/src/server/windows-tray-control.ts +41 -0
  756. package/src/server/ws-bridge.ts +472 -0
  757. package/src/service-manager-probe.ts +892 -0
  758. package/src/service.ts +3575 -0
  759. package/src/sidecar/auth.ts +92 -0
  760. package/src/sidecar/candidates.ts +83 -0
  761. package/src/stall-timeout.ts +20 -0
  762. package/src/storage/cleanup-job.ts +57 -0
  763. package/src/storage/cleanup.ts +3085 -0
  764. package/src/storage/policy-job.ts +457 -0
  765. package/src/storage/policy-scheduler.ts +40 -0
  766. package/src/storage/policy-worker.ts +59 -0
  767. package/src/storage/policy.ts +527 -0
  768. package/src/storage/restore-job.ts +299 -0
  769. package/src/storage/restore-worker.ts +58 -0
  770. package/src/storage/scanner.ts +238 -0
  771. package/src/storage/storage-mutation-coordinator.ts +139 -0
  772. package/src/storage/worker-lifecycle.ts +215 -0
  773. package/src/tray/assets/opencodex-tray-offline.ico +0 -0
  774. package/src/tray/assets/opencodex-tray-online.ico +0 -0
  775. package/src/tray/assets/opencodex-tray-warning.ico +0 -0
  776. package/src/tray/assets/opencodex-tray.png +0 -0
  777. package/src/tray/windows-tray.ps1 +364 -0
  778. package/src/tray/windows.ts +757 -0
  779. package/src/types/accounts.ts +37 -0
  780. package/src/types/config.ts +876 -0
  781. package/src/types/provider.ts +545 -0
  782. package/src/types/request.ts +384 -0
  783. package/src/types/tools.ts +131 -0
  784. package/src/types/wire.ts +80 -0
  785. package/src/types.ts +106 -0
  786. package/src/update/badge.ts +72 -0
  787. package/src/update/index.ts +415 -0
  788. package/src/update/job.ts +1887 -0
  789. package/src/update/notify.ts +263 -0
  790. package/src/update/npm-cache-preflight.d.mts +47 -0
  791. package/src/update/npm-cache-preflight.mjs +201 -0
  792. package/src/update/npm-invocation.d.mts +23 -0
  793. package/src/update/npm-invocation.mjs +94 -0
  794. package/src/update/transactional-install.d.mts +22 -0
  795. package/src/update/transactional-install.mjs +259 -0
  796. package/src/update/tray-update-plan.d.mts +18 -0
  797. package/src/update/tray-update-plan.mjs +38 -0
  798. package/src/usage/cost.ts +625 -0
  799. package/src/usage/debug.ts +97 -0
  800. package/src/usage/expected-prices.ts +416 -0
  801. package/src/usage/log.ts +1223 -0
  802. package/src/usage/summary.ts +753 -0
  803. package/src/usage/totals.ts +14 -0
  804. package/src/usage/user-cost-overlay-reconciler.ts +313 -0
  805. package/src/usage/user-cost-overlays.ts +314 -0
  806. package/src/vision/anthropic-describe.ts +189 -0
  807. package/src/vision/backends.ts +97 -0
  808. package/src/vision/describe.ts +131 -0
  809. package/src/vision/eligibility.ts +250 -0
  810. package/src/vision/index.ts +681 -0
  811. package/src/vision/reasoning.ts +55 -0
  812. package/src/vision/routed-describe.ts +175 -0
  813. package/src/vision/timeout-bounds.ts +9 -0
  814. package/src/web-search/anthropic-executor.ts +195 -0
  815. package/src/web-search/backends.ts +108 -0
  816. package/src/web-search/exa-executor.ts +88 -0
  817. package/src/web-search/executor.ts +113 -0
  818. package/src/web-search/format-result.ts +89 -0
  819. package/src/web-search/gemini-executor.ts +141 -0
  820. package/src/web-search/index.ts +331 -0
  821. package/src/web-search/loop.ts +896 -0
  822. package/src/web-search/parse.ts +315 -0
  823. package/src/web-search/progress-stream.ts +342 -0
  824. package/src/web-search/sources.ts +60 -0
  825. package/src/web-search/synthetic-tool.ts +47 -0
  826. package/src/web-search/xai-executor.ts +219 -0
@@ -0,0 +1,1381 @@
1
+ import type { OcxUsage } from "../../types";
2
+ import type { AgentServerMessage, McpArgs, ToolCall } from "./gen/agent_pb";
3
+ import { decodeCursorArgsMap } from "./arg-codec";
4
+ import { normalizeArgKeys } from "./arg-normalize";
5
+ import {
6
+ CODEX_APPLY_PATCH_TOOL,
7
+ CURSOR_MULTI_EDIT_TOOL,
8
+ cursorShellBridgeArgsValid,
9
+ cursorShellBridgeDropError,
10
+ defaultShellBridgeArgNormalizeSchema,
11
+ isCodexShellBridgeToolName,
12
+ isCursorStructuredEditToolName,
13
+ normalizeCursorWireName,
14
+ normalizeCursorTextToolMarkers,
15
+ OCX_RESPONSES_TOOL_PROVIDER,
16
+ resolveShellBridgeAliasKey,
17
+ responsesToolNameFromCursorWire,
18
+ } from "./tool-definitions";
19
+ import type { CursorServerMessage } from "./types";
20
+ import type { TranslatorBudget } from "../../lib/translator-budget";
21
+
22
+ const DEFAULT_CONTEXT_USAGE_MAX_ENTRIES = 200;
23
+ const DEFAULT_CONTEXT_USAGE_TTL_MS = 60 * 60 * 1_000;
24
+ const DEFAULT_MAX_CLIENT_TOOL_CALLS = 330;
25
+
26
+ export interface CursorContextUsageControls {
27
+ /**
28
+ * Last observed absolute context size for the same Cursor conversation and uncompacted context
29
+ * epoch. Used only when the current turn produces output but no checkpoint.
30
+ */
31
+ carryForwardTokens?: number;
32
+ /** Persist a fresh checkpoint for later turns in this conversation. */
33
+ recordContextTokens?: (tokens: number) => void;
34
+ }
35
+
36
+ export interface CursorContextUsageTracker {
37
+ controlsForConversation(conversationId: string, options?: { clearPrior?: boolean; storeCheckpoints?: boolean }): CursorContextUsageControls;
38
+ get(conversationId: string): number | undefined;
39
+ record(conversationId: string, tokens: number): void;
40
+ /** Copy numeric carry-forward totals when a conversation id is rotated for replay. */
41
+ rekey(fromConversationId: string, toConversationId: string): void;
42
+ clear(conversationId: string): void;
43
+ clearAll(): void;
44
+ }
45
+
46
+ interface CursorContextUsageEntry {
47
+ tokens: number;
48
+ updatedAt: number;
49
+ }
50
+
51
+ /**
52
+ * Bounded numeric-only carry-forward for Cursor context usage. Cursor checkpoints are absolute
53
+ * active-context sizes, but client-tool suspension can end a turn before a checkpoint arrives. Keep
54
+ * only the last known total per provider conversation so a no-checkpoint finalize does not overwrite
55
+ * a real active-context value with the current turn's tiny output delta. Context text, tool args,
56
+ * and model output are never stored here.
57
+ */
58
+ export function createCursorContextUsageTracker(options: { maxEntries?: number; ttlMs?: number; now?: () => number } = {}): CursorContextUsageTracker {
59
+ const maxEntries = options.maxEntries ?? DEFAULT_CONTEXT_USAGE_MAX_ENTRIES;
60
+ const ttlMs = options.ttlMs ?? DEFAULT_CONTEXT_USAGE_TTL_MS;
61
+ const now = options.now ?? (() => Date.now());
62
+ const entries = new Map<string, CursorContextUsageEntry>();
63
+
64
+ const prune = () => {
65
+ const at = now();
66
+ for (const [conversationId, entry] of entries) {
67
+ if (at - entry.updatedAt > ttlMs) entries.delete(conversationId);
68
+ }
69
+ while (entries.size > maxEntries) {
70
+ const oldest = entries.keys().next().value;
71
+ if (!oldest) break;
72
+ entries.delete(oldest);
73
+ }
74
+ };
75
+
76
+ const record = (conversationId: string, tokens: number) => {
77
+ if (!Number.isFinite(tokens) || tokens <= 0) return;
78
+ prune();
79
+ const existing = entries.get(conversationId);
80
+ if (existing && existing.tokens >= tokens) {
81
+ entries.delete(conversationId);
82
+ entries.set(conversationId, { tokens: existing.tokens, updatedAt: now() });
83
+ return;
84
+ }
85
+ entries.delete(conversationId);
86
+ entries.set(conversationId, { tokens, updatedAt: now() });
87
+ prune();
88
+ };
89
+
90
+ const get = (conversationId: string): number | undefined => {
91
+ prune();
92
+ const entry = entries.get(conversationId);
93
+ if (!entry) return undefined;
94
+ entries.delete(conversationId);
95
+ entries.set(conversationId, { tokens: entry.tokens, updatedAt: now() });
96
+ return entry.tokens;
97
+ };
98
+
99
+ return {
100
+ controlsForConversation(conversationId, requestOptions = {}) {
101
+ if (requestOptions.clearPrior === true) entries.delete(conversationId);
102
+ const storeCheckpoints = requestOptions.storeCheckpoints !== false;
103
+ const carryForwardTokens = storeCheckpoints ? get(conversationId) : undefined;
104
+ return {
105
+ ...(carryForwardTokens !== undefined ? { carryForwardTokens } : {}),
106
+ ...(storeCheckpoints ? { recordContextTokens: tokens => record(conversationId, tokens) } : {}),
107
+ };
108
+ },
109
+ get,
110
+ record,
111
+ rekey(fromConversationId, toConversationId) {
112
+ if (!fromConversationId || !toConversationId || fromConversationId === toConversationId) return;
113
+ prune();
114
+ const from = entries.get(fromConversationId);
115
+ if (!from) return;
116
+ const to = entries.get(toConversationId);
117
+ const tokens = Math.max(from.tokens, to?.tokens ?? 0);
118
+ entries.delete(fromConversationId);
119
+ entries.delete(toConversationId);
120
+ entries.set(toConversationId, { tokens, updatedAt: now() });
121
+ prune();
122
+ },
123
+ clear(conversationId) {
124
+ entries.delete(conversationId);
125
+ },
126
+ clearAll() {
127
+ entries.clear();
128
+ },
129
+ };
130
+ }
131
+
132
+ export interface CursorProtobufEventState {
133
+ usage: OcxUsage;
134
+ /**
135
+ * Absolute conversation context size from Cursor's `conversationCheckpointUpdate.usedTokens`
136
+ * (authoritative cumulative context, NOT a per-turn delta). Kept separate from `usage.outputTokens`
137
+ * so it is never folded into the additive per-turn output count. Surfaced as `done.usage.totalTokens`
138
+ * so Codex's `last_token_usage.total_tokens` reflects the real active context. Mirrors the Kiro
139
+ * contextUsagePercentage SOT fix (devlog 142.10): absolute context and additive output must not
140
+ * share one field, or Codex double-counts (e.g. 10000 then 10300 surfacing as 20300).
141
+ */
142
+ contextTokens?: number;
143
+ /**
144
+ * Request-local input estimate derived from the payload actually sent to Cursor.
145
+ * Used only when neither a checkpoint nor a carry-forward is available — a restart
146
+ * clears the tracker, and reporting inputTokens=0 makes Codex see an almost-empty
147
+ * context (#373). Never written back into the tracker: only real checkpoints are.
148
+ */
149
+ estimatedInputTokens?: number;
150
+ /**
151
+ * Session-level last-known absolute context size for this Cursor conversation. This is a fallback
152
+ * for no-checkpoint client-tool finalize turns only; any checkpoint observed during the current
153
+ * turn remains authoritative, with monotonic max semantics unless a compaction boundary reset the
154
+ * conversation cache before the turn.
155
+ */
156
+ contextCarryForwardTokens?: number;
157
+ recordContextTokens?: (tokens: number) => void;
158
+ openToolCalls: Map<string, { name: string; args: string; awaitingNativeArgs?: boolean }>;
159
+ completedToolCalls: Set<string>;
160
+ /** Set once a terminal `done`/truncation has been emitted, so post-terminal frames stay inert. */
161
+ terminated?: boolean;
162
+ clientToolNames?: Set<string>;
163
+ /** Responses/Codex names of request-declared freeform tools advertised to Cursor. */
164
+ freeformToolNames?: ReadonlySet<string>;
165
+ parallelToolCalls?: boolean;
166
+ startedClientToolCalls: number;
167
+ /** Hard cap on client tool-call records retained during one upstream turn. */
168
+ maxClientToolCalls: number;
169
+ /** Tool wire-name → original JSON Schema parameters object, for arg-key normalization. */
170
+ toolSchemas?: Map<string, unknown>;
171
+ /** Cursor wire-name → original Responses/Codex tool name for this request. */
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>;
178
+ translatorBudget?: TranslatorBudget;
179
+ }
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
+
202
+ export function createCursorProtobufEventState(options: {
203
+ clientToolNames?: Iterable<string>;
204
+ freeformToolNames?: Iterable<string>;
205
+ parallelToolCalls?: boolean;
206
+ maxClientToolCalls?: number;
207
+ toolSchemas?: Map<string, unknown>;
208
+ cursorToolNameMap?: Map<string, string>;
209
+ syntheticStructuredEditToolNames?: Iterable<string>;
210
+ contextUsage?: CursorContextUsageControls;
211
+ /**
212
+ * Request-local input estimate derived from the payload actually sent. Used only
213
+ * when neither a checkpoint nor a carry-forward is available; never recorded into
214
+ * the checkpoint tracker (#373).
215
+ */
216
+ estimatedInputTokens?: number;
217
+ translatorBudget?: TranslatorBudget;
218
+ } = {}): CursorProtobufEventState {
219
+ return {
220
+ // Cursor provides no authoritative usage frame; token counts are heuristic estimates from
221
+ // checkpoint/delta events, so mark estimated from the start.
222
+ usage: { inputTokens: 0, outputTokens: 0, estimated: true },
223
+ openToolCalls: new Map(),
224
+ completedToolCalls: new Set(),
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
+ : {}),
230
+ ...(options.parallelToolCalls !== undefined ? { parallelToolCalls: options.parallelToolCalls } : {}),
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,
237
+ ...(options.toolSchemas ? { toolSchemas: options.toolSchemas } : {}),
238
+ ...(options.cursorToolNameMap ? { cursorToolNameMap: options.cursorToolNameMap } : {}),
239
+ ...(options.translatorBudget ? { translatorBudget: options.translatorBudget } : {}),
240
+ ...(options.contextUsage?.carryForwardTokens !== undefined ? { contextCarryForwardTokens: options.contextUsage.carryForwardTokens } : {}),
241
+ ...(options.contextUsage?.recordContextTokens ? { recordContextTokens: options.contextUsage.recordContextTokens } : {}),
242
+ ...(typeof options.estimatedInputTokens === "number"
243
+ && Number.isFinite(options.estimatedInputTokens)
244
+ && options.estimatedInputTokens > 0
245
+ ? { estimatedInputTokens: options.estimatedInputTokens }
246
+ : {}),
247
+ };
248
+ }
249
+
250
+ function observeContextTokens(state: CursorProtobufEventState, usedTokens: number): void {
251
+ if (!Number.isFinite(usedTokens) || usedTokens <= 0) return;
252
+ if (usedTokens > (state.contextTokens ?? 0)) state.contextTokens = usedTokens;
253
+ state.recordContextTokens?.(usedTokens);
254
+ }
255
+
256
+ export function reportableContextTokens(state: CursorProtobufEventState): number | undefined {
257
+ const current = state.contextTokens;
258
+ const carry = state.contextCarryForwardTokens;
259
+ if (current === undefined) return carry;
260
+ if (carry === undefined) return current;
261
+ return Math.max(current, carry);
262
+ }
263
+
264
+ export function usageFromContextTokens(state: CursorProtobufEventState, contextTokens: number): OcxUsage {
265
+ return {
266
+ ...state.usage,
267
+ inputTokens: Math.max(0, contextTokens - state.usage.outputTokens),
268
+ totalTokens: contextTokens,
269
+ };
270
+ }
271
+
272
+ /** Exported for live-transport's client-tool frame classification (finalize revocation). */
273
+ export function mcpArgsFromToolCall(toolCall: ToolCall | undefined): McpArgs | undefined {
274
+ if (toolCall?.tool.case !== "mcpToolCall") return undefined;
275
+ const args = toolCall.tool.value.args;
276
+ return args?.providerIdentifier === OCX_RESPONSES_TOOL_PROVIDER ? args : undefined;
277
+ }
278
+
279
+ function mcpWireNameFromArgs(args: McpArgs | undefined): string | undefined {
280
+ const raw = args?.toolName || args?.name;
281
+ // Models may call the Cursor-displayed `mcp_<provider>_<tool>` name; fold it to the advertised name.
282
+ return raw && raw.length > 0 ? normalizeCursorWireName(raw) : undefined;
283
+ }
284
+
285
+ function mcpCursorWireName(toolCall: ToolCall | undefined): string | undefined {
286
+ return mcpWireNameFromArgs(mcpArgsFromToolCall(toolCall));
287
+ }
288
+
289
+ function decodeMcpArgs(args: McpArgs | undefined): string {
290
+ return JSON.stringify(decodeCursorArgsMap(args?.args));
291
+ }
292
+
293
+ /** Resolve an advertised client-tool wire name, including shell_command/exec_command aliases (#399). */
294
+ function resolveAdvertisedClientToolName(
295
+ state: CursorProtobufEventState,
296
+ cursorWireName: string,
297
+ ): string | undefined {
298
+ const normalized = normalizeCursorWireName(cursorWireName);
299
+ if (!state.clientToolNames) return normalized;
300
+ return resolveShellBridgeAliasKey(normalized, alias => (state.clientToolNames!.has(alias) ? alias : undefined));
301
+ }
302
+
303
+ function toolSchemaForWireName(state: CursorProtobufEventState, toolName: string | undefined): unknown | undefined {
304
+ if (!toolName || !state.toolSchemas) return undefined;
305
+ return resolveShellBridgeAliasKey(toolName, alias => state.toolSchemas!.get(alias));
306
+ }
307
+
308
+ function decodeMcpArgsNormalized(args: McpArgs | undefined, state: CursorProtobufEventState): string {
309
+ const decoded = decodeCursorArgsMap(args?.args);
310
+ const toolName = mcpWireNameFromArgs(args);
311
+ const schema = toolSchemaForWireName(state, toolName);
312
+ if (schema) return JSON.stringify(normalizeArgKeys(decoded, schema));
313
+ return JSON.stringify(decoded);
314
+ }
315
+
316
+ function hasMcpArgBytes(args: McpArgs | undefined): boolean {
317
+ return Object.keys(args?.args ?? {}).length > 0;
318
+ }
319
+
320
+ function isCompleteJson(text: string): boolean {
321
+ if (text.length === 0) return false;
322
+ try {
323
+ JSON.parse(text);
324
+ return true;
325
+ } catch {
326
+ return false;
327
+ }
328
+ }
329
+
330
+ /** Schema-normalize a JSON-text argument blob for a named tool, if a schema is known. */
331
+ function normalizeJsonText(text: string, toolName: string | undefined, state: CursorProtobufEventState): string {
332
+ const schema = toolSchemaForWireName(state, toolName);
333
+ if (!schema) return text;
334
+ try {
335
+ const parsed = JSON.parse(text);
336
+ if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
337
+ return JSON.stringify(normalizeArgKeys(parsed as Record<string, unknown>, schema));
338
+ }
339
+ } catch {
340
+ // Not parseable as an object: leave as-is.
341
+ }
342
+ return text;
343
+ }
344
+
345
+ /**
346
+ * Resolve the authoritative argument string for a completed client tool call.
347
+ *
348
+ * Cursor sends args two ways: incrementally as `argsTextDelta` (buffered into `open.args`, never
349
+ * streamed onward), and/or as a structured protobuf map on `toolCallCompleted`. We emit the args
350
+ * exactly once, at completion, so they can always be schema-normalized regardless of which form
351
+ * arrived. The completed map wins when present (canonical); otherwise the buffered streamed text is
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.
354
+ */
355
+ function resolveCompletedArgs(buffered: string, args: McpArgs | undefined, state: CursorProtobufEventState): string {
356
+ if (hasMcpArgBytes(args)) return decodeMcpArgsNormalized(args, state);
357
+ const name = mcpWireNameFromArgs(args);
358
+ if (isCompleteJson(buffered)) return normalizeJsonText(buffered, name, state);
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") };
1042
+ }
1043
+
1044
+ export function mapSyntheticMcpExecToToolEvents(
1045
+ args: McpArgs,
1046
+ fallbackCallId = "cursor_mcp_exec",
1047
+ options: { allowEmptyArgs?: boolean; state?: CursorProtobufEventState } = {},
1048
+ ): CursorServerMessage[] {
1049
+ if (args.providerIdentifier !== OCX_RESPONSES_TOOL_PROVIDER) return [];
1050
+ if (options.state?.terminated) return [];
1051
+ if (options.allowEmptyArgs !== true && !hasMcpArgBytes(args)) return [];
1052
+ const cursorWireName = mcpWireNameFromArgs(args);
1053
+ if (!cursorWireName) return [{ type: "error", message: "Cursor requested a Responses tool without a tool name" }];
1054
+ const callId = args.toolCallId || fallbackCallId;
1055
+ if (options.state?.completedToolCalls.has(callId)) return [];
1056
+ if (options.state) {
1057
+ // Native-exec delivers the whole client tool call at once. Record it (no-op if already opened by
1058
+ // an earlier started/partial event), then emit the atomic start -> delta -> end unit.
1059
+ const out: CursorServerMessage[] = [...recordToolCall(options.state, callId, cursorWireName)];
1060
+ if (out.some(event => event.type === "error")) return out;
1061
+ const open = options.state.openToolCalls.get(callId);
1062
+ const finalArgs = resolveCompletedArgs(open?.args ?? "", args, options.state);
1063
+ out.push(...commitToolCall(options.state, callId, finalArgs));
1064
+ return out;
1065
+ }
1066
+ const responsesName = responsesToolNameFromCursorWire(cursorWireName);
1067
+ const normSchema = defaultShellBridgeArgNormalizeSchema(responsesName);
1068
+ const normalizedArgs = JSON.stringify(normalizeArgKeys(decodeCursorArgsMap(args?.args), normSchema));
1069
+ if (!cursorShellBridgeArgsValid(normalizedArgs, responsesName, normSchema)) {
1070
+ if (isCodexShellBridgeToolName(responsesName)) {
1071
+ return [{ type: "error", message: cursorShellBridgeDropError(responsesName) }];
1072
+ }
1073
+ }
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;
1084
+ return [
1085
+ { type: "tool_call_start", id: callId, name: emittedName },
1086
+ ...(emittedArgs.length > 2
1087
+ ? [{ type: "tool_call_delta" as const, arguments: emittedArgs }]
1088
+ : []),
1089
+ { type: "tool_call_end", id: callId },
1090
+ ];
1091
+ }
1092
+
1093
+ /**
1094
+ * Record (open) a client tool call WITHOUT emitting `tool_call_start`. The outward start is deferred
1095
+ * to completion (see commitToolCall) so each Cursor tool call surfaces to the bridge as one atomic,
1096
+ * self-contained start -> delta -> end unit. This lets Cursor open several tool calls in parallel
1097
+ * (or interleave their partial-arg streams) without cross-wiring: nothing reaches the single-current-
1098
+ * call bridge until a call completes, and completed calls are emitted whole, one after another.
1099
+ * Returns an error only for a genuinely unknown (un-advertised) tool name.
1100
+ */
1101
+ function recordToolCall(state: CursorProtobufEventState, callId: string, cursorWireName: string): CursorServerMessage[] {
1102
+ if (state.completedToolCalls.has(callId)) return [];
1103
+ if (state.openToolCalls.has(callId)) return [];
1104
+ const advertisedName = resolveAdvertisedClientToolName(state, cursorWireName);
1105
+ if (state.clientToolNames && !advertisedName) {
1106
+ return [{ type: "error", message: `Cursor requested unknown Responses tool: ${cursorWireName}` }];
1107
+ }
1108
+ if (state.startedClientToolCalls >= state.maxClientToolCalls) {
1109
+ return [{ type: "error", message: `Cursor exceeded client tool-call limit (${state.maxClientToolCalls})` }];
1110
+ }
1111
+ // Prefer the advertised catalog name for Responses mapping so shell_command/exec_command aliases
1112
+ // land on the tool Codex actually exposed this turn (#399).
1113
+ const mapKey = advertisedName ?? normalizeCursorWireName(cursorWireName);
1114
+ state.openToolCalls.set(callId, { name: responsesToolNameFromCursorWire(mapKey, state.cursorToolNameMap), args: "" });
1115
+ state.translatorBudget?.openCall(callId);
1116
+ state.startedClientToolCalls++;
1117
+ return [];
1118
+ }
1119
+
1120
+ /**
1121
+ * Emit a completed client tool call as one atomic unit: `tool_call_start` (deferred from open time),
1122
+ * the full normalized arguments delta when present, then `tool_call_end`. The call must already be
1123
+ * recorded in `openToolCalls`. Because each completion emits a whole non-interleaved unit, the bridge
1124
+ * (which tracks a single current tool call) serializes parallel Cursor calls correctly.
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
+
1145
+ function dropShellBridgeCall(state: CursorProtobufEventState, callId: string, toolName: string): CursorServerMessage[] {
1146
+ state.openToolCalls.delete(callId);
1147
+ state.translatorBudget?.closeCall(callId);
1148
+ state.completedToolCalls.add(callId);
1149
+ return [{ type: "error", message: cursorShellBridgeDropError(toolName) }];
1150
+ }
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
+
1161
+ function commitToolCall(state: CursorProtobufEventState, callId: string, finalArgs: string): CursorServerMessage[] {
1162
+ const open = state.openToolCalls.get(callId);
1163
+ if (!open) return [];
1164
+ if (state.freeformToolNames?.has(open.name) && !cursorFreeformWrapperValid(finalArgs)) {
1165
+ return dropInvalidFreeformCall(state, callId, open.name);
1166
+ }
1167
+ const schema = toolSchemaForWireName(state, open.name);
1168
+ if (!cursorShellBridgeArgsValid(finalArgs, open.name, schema)) {
1169
+ if (isCodexShellBridgeToolName(open.name)) return dropShellBridgeCall(state, callId, open.name);
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
+ }
1179
+ if (finalArgs !== open.args) {
1180
+ const previousBytes = Buffer.byteLength(open.args);
1181
+ const reservation = state.translatorBudget?.reserveTransient(
1182
+ Buffer.byteLength(finalArgs),
1183
+ { kind: "tool_args", callId },
1184
+ );
1185
+ open.args = finalArgs;
1186
+ reservation?.commitRetained();
1187
+ state.translatorBudget?.releaseRetained(previousBytes, { kind: "tool_args", callId });
1188
+ }
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 });
1194
+ out.push(...endToolCall(state, callId));
1195
+ return out;
1196
+ }
1197
+
1198
+ /**
1199
+ * Buffer Cursor's cumulative `argsTextDelta` into the open call WITHOUT emitting a delta. Args are
1200
+ * emitted once, normalized, at completion (see resolveCompletedArgs), so a mis-keyed or
1201
+ * non-canonical streamed blob can still be repaired before Codex sees it. `argsTextDelta` is
1202
+ * cumulative; keep the longest value seen.
1203
+ */
1204
+ function bufferToolArgs(state: CursorProtobufEventState, callId: string, cumulative: string): void {
1205
+ const open = state.openToolCalls.get(callId);
1206
+ if (!open) return;
1207
+ if (cumulative.length >= open.args.length) {
1208
+ const encoder = new TextEncoder();
1209
+ const previousBytes = encoder.encode(open.args).byteLength;
1210
+ const reservation = state.translatorBudget?.reserveTransient(
1211
+ encoder.encode(cumulative).byteLength,
1212
+ { kind: "tool_args", callId },
1213
+ );
1214
+ open.args = cumulative;
1215
+ reservation?.commitRetained();
1216
+ state.translatorBudget?.releaseRetained(previousBytes, { kind: "tool_args", callId });
1217
+ }
1218
+ }
1219
+
1220
+ function endToolCall(state: CursorProtobufEventState, callId: string): CursorServerMessage[] {
1221
+ if (!state.openToolCalls.has(callId)) return [];
1222
+ state.openToolCalls.delete(callId);
1223
+ state.translatorBudget?.closeCall(callId);
1224
+ state.completedToolCalls.add(callId);
1225
+ return [{ type: "tool_call_end", id: callId }];
1226
+ }
1227
+
1228
+ export function mapCursorProtobufServerMessage(
1229
+ serverMessage: AgentServerMessage,
1230
+ state: CursorProtobufEventState,
1231
+ ): CursorServerMessage[] {
1232
+ if (state.terminated) return [];
1233
+
1234
+ if (serverMessage.message.case === "conversationCheckpointUpdate") {
1235
+ const usedTokens = serverMessage.message.value.tokenDetails?.usedTokens ?? 0;
1236
+ // `usedTokens` is the ABSOLUTE conversation context size, not a per-turn output delta. Track it
1237
+ // separately (monotonic max) and surface it as `done.usage.totalTokens`; folding it into
1238
+ // `outputTokens` (which also accumulates `tokenDelta`) double-counts in Codex. See contextTokens.
1239
+ observeContextTokens(state, usedTokens);
1240
+ return [];
1241
+ }
1242
+
1243
+ if (serverMessage.message.case !== "interactionUpdate") return [];
1244
+ const update = serverMessage.message.value.message;
1245
+ switch (update.case) {
1246
+ case "textDelta":
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) }] : [];
1251
+ case "thinkingDelta":
1252
+ return update.value.text ? [{ type: "thinking", thinking: update.value.text }] : [];
1253
+ case "toolCallStarted": {
1254
+ const name = mcpCursorWireName(update.value.toolCall);
1255
+ // Record the open call but defer the outward tool_call_start to completion (atomic emission).
1256
+ return name ? recordToolCall(state, update.value.callId, name) : [];
1257
+ }
1258
+ case "partialToolCall": {
1259
+ const out: CursorServerMessage[] = [];
1260
+ const name = mcpCursorWireName(update.value.toolCall);
1261
+ if (name) out.push(...recordToolCall(state, update.value.callId, name));
1262
+ if (out.some(event => event.type === "error")) return out;
1263
+ // Buffer cumulative args; do not emit a delta. Args are emitted once, normalized, at completion.
1264
+ if (state.openToolCalls.has(update.value.callId)) {
1265
+ bufferToolArgs(state, update.value.callId, update.value.argsTextDelta);
1266
+ }
1267
+ return out;
1268
+ }
1269
+ case "toolCallDelta":
1270
+ // Cursor's typed deltas currently cover native exec internals (shell/task/edit). Client
1271
+ // Responses tools return as McpToolCall plus partial args text, so native deltas stay internal.
1272
+ return [];
1273
+ case "toolCallCompleted": {
1274
+ const out: CursorServerMessage[] = [];
1275
+ if (state.completedToolCalls.has(update.value.callId)) return [];
1276
+ const name = mcpCursorWireName(update.value.toolCall);
1277
+ const args = mcpArgsFromToolCall(update.value.toolCall);
1278
+ const openBeforeStart = state.openToolCalls.get(update.value.callId);
1279
+ // Empty-arg completion handling:
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.
1285
+ // - never started + not advertised -> Cursor prelude noise, drop it.
1286
+ // - advertised client tool, not yet open -> a legitimate no-arg call: commit it (start+end)
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
+ }
1300
+ if (name && !hasMcpArgBytes(args)) {
1301
+ // Only commit a no-arg call when the tool is *explicitly* advertised. Without an advertised
1302
+ // tool list we cannot tell a real no-arg call from a Cursor prelude, so we keep dropping it.
1303
+ const advertised = state.clientToolNames?.has(name) ?? false;
1304
+ if (!openBeforeStart && !advertised) return [];
1305
+ }
1306
+ // Ensure the call is recorded (covers a completion with no prior started/partial event), then
1307
+ // emit it as one atomic start -> delta -> end unit so parallel Cursor calls serialize cleanly.
1308
+ if (name) out.push(...recordToolCall(state, update.value.callId, name));
1309
+ if (out.some(event => event.type === "error")) return out;
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
+ }
1322
+ if (open) {
1323
+ const finalArgs = resolveCompletedArgs(open.args, args, state);
1324
+ out.push(...commitToolCall(state, update.value.callId, finalArgs));
1325
+ }
1326
+ return out;
1327
+ }
1328
+ case "tokenDelta":
1329
+ state.usage.outputTokens += update.value.tokens;
1330
+ return [];
1331
+ case "turnEnded":
1332
+ return finalizeTurnEvents(state);
1333
+ default:
1334
+ return [];
1335
+ }
1336
+ }
1337
+
1338
+ /**
1339
+ * Resolve the usage to report for a turn, in order of trustworthiness: a checkpoint
1340
+ * observed this turn, then the session carry-forward, then the request-local estimate,
1341
+ * then the raw per-turn counters. Shared with the partial-usage path in live-transport
1342
+ * so a failed turn reports the same input side as a clean one (#373).
1343
+ */
1344
+ export function resolvedTurnUsage(state: CursorProtobufEventState): OcxUsage {
1345
+ const contextTokens = reportableContextTokens(state);
1346
+ if (contextTokens !== undefined) return usageFromContextTokens(state, contextTokens);
1347
+ const estimate = state.estimatedInputTokens;
1348
+ if (estimate !== undefined) {
1349
+ return {
1350
+ ...state.usage,
1351
+ inputTokens: estimate,
1352
+ totalTokens: estimate + state.usage.outputTokens,
1353
+ estimated: true,
1354
+ };
1355
+ }
1356
+ return { ...state.usage };
1357
+ }
1358
+
1359
+ /**
1360
+ * Finalize a Cursor turn. If any client tool call is still open (started but never completed),
1361
+ * the stream was truncated and the partial tool call must not reach Codex as a completed call
1362
+ * with corrupt/empty arguments. Emit an explicit error instead of done (fail-closed).
1363
+ * Mirrors kiro-truncation.ts behavior.
1364
+ */
1365
+ export function finalizeTurnEvents(state: CursorProtobufEventState): CursorServerMessage[] {
1366
+ state.terminated = true;
1367
+ if (state.openToolCalls.size > 0) {
1368
+ const openCallIds = [...state.openToolCalls.keys()];
1369
+ const openIds = openCallIds.join(", ");
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);
1372
+ state.openToolCalls.clear();
1373
+ return [{ type: "error", message: `Cursor stream ended with incomplete tool call(s): ${openIds}. Arguments may be truncated; the call was not committed.` }];
1374
+ }
1375
+ // Surface the absolute context size (when Cursor reported a checkpoint) as both totalTokens and
1376
+ // the estimated input side of Codex's visible `input + output` counter. Codex status lines can
1377
+ // render the additive pair instead of total_tokens, so leaving inputTokens at 0 makes a 16k-context
1378
+ // first turn display as "9 used". Keep outputTokens as the per-turn delta and clamp the inferred
1379
+ // input to 0 in case Cursor reports a checkpoint smaller than the streamed output delta.
1380
+ return [{ type: "done", usage: resolvedTurnUsage(state) }];
1381
+ }