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