@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.
- package/AGENTS_INSTALL.md +109 -0
- package/LICENSE +21 -0
- package/README.md +303 -0
- package/assets/architecture.png +0 -0
- package/assets/banner.png +0 -0
- package/assets/claude-code-models.gif +0 -0
- package/assets/codex-app-picker.png +0 -0
- package/bin/ocx.mjs +587 -0
- package/bin/package-main.mjs +9 -0
- package/gui/dist/assets/index-BNJ7r4Gd.js +102 -0
- package/gui/dist/assets/index-CGoDO3uO.css +1 -0
- package/gui/dist/favicon.png +0 -0
- package/gui/dist/icons.svg +24 -0
- package/gui/dist/index.html +25 -0
- package/gui/dist/logo.png +0 -0
- package/gui/dist/provider-icons/alibaba-color.svg +1 -0
- package/gui/dist/provider-icons/antigravity-color.svg +1 -0
- package/gui/dist/provider-icons/claude-color.svg +1 -0
- package/gui/dist/provider-icons/cline-color.svg +16 -0
- package/gui/dist/provider-icons/cloudflare-ai-gateway-color.svg +1 -0
- package/gui/dist/provider-icons/commandcode-color.svg +1 -0
- package/gui/dist/provider-icons/copilot-color.svg +1 -0
- package/gui/dist/provider-icons/cursor-color.svg +2 -0
- package/gui/dist/provider-icons/deepseek-color.svg +1 -0
- package/gui/dist/provider-icons/discord.svg +1 -0
- package/gui/dist/provider-icons/firepass-color.svg +1 -0
- package/gui/dist/provider-icons/fireworks-color.svg +1 -0
- package/gui/dist/provider-icons/gemini-color.svg +1 -0
- package/gui/dist/provider-icons/github-copilot-color.svg +1 -0
- package/gui/dist/provider-icons/gitlab-duo-color.svg +1 -0
- package/gui/dist/provider-icons/grok.svg +1 -0
- package/gui/dist/provider-icons/groq-color.svg +1 -0
- package/gui/dist/provider-icons/huggingface-color.svg +1 -0
- package/gui/dist/provider-icons/kimi-color.svg +1 -0
- package/gui/dist/provider-icons/kiro-color.svg +15 -0
- package/gui/dist/provider-icons/lm-studio-color.svg +1 -0
- package/gui/dist/provider-icons/mistral-color.svg +1 -0
- package/gui/dist/provider-icons/moonshot-color.svg +1 -0
- package/gui/dist/provider-icons/nvidia-color.svg +1 -0
- package/gui/dist/provider-icons/ollama-color.svg +1 -0
- package/gui/dist/provider-icons/openai.svg +1 -0
- package/gui/dist/provider-icons/opencode.svg +2 -0
- package/gui/dist/provider-icons/openrouter-color.svg +1 -0
- package/gui/dist/provider-icons/pi.svg +21 -0
- package/gui/dist/provider-icons/qianfan-color.svg +1 -0
- package/gui/dist/provider-icons/qwen-portal-color.svg +1 -0
- package/gui/dist/provider-icons/telegram.svg +1 -0
- package/gui/dist/provider-icons/vercel-ai-gateway-color.svg +1 -0
- package/gui/dist/provider-icons/vllm-color.svg +1 -0
- package/gui/dist/provider-icons/xiaomi-color.svg +1 -0
- package/package.json +108 -0
- package/src/AGENTS.md +28 -0
- package/src/adapters/anthropic-image-guard.ts +251 -0
- package/src/adapters/anthropic-image-normalize.ts +518 -0
- package/src/adapters/anthropic-output-schema.ts +137 -0
- package/src/adapters/anthropic.ts +1327 -0
- package/src/adapters/azure.ts +36 -0
- package/src/adapters/base.ts +121 -0
- package/src/adapters/client-fingerprint.ts +65 -0
- package/src/adapters/cline-pass-deepseek-v4-tool-replay.ts +69 -0
- package/src/adapters/command-code.ts +601 -0
- package/src/adapters/cursor/arg-codec.ts +38 -0
- package/src/adapters/cursor/arg-normalize.ts +104 -0
- package/src/adapters/cursor/checkpoint-store.ts +303 -0
- package/src/adapters/cursor/cursor-errors.ts +288 -0
- package/src/adapters/cursor/discovery.ts +333 -0
- package/src/adapters/cursor/effort-map.ts +151 -0
- package/src/adapters/cursor/exec-policy.ts +88 -0
- package/src/adapters/cursor/framing.ts +250 -0
- package/src/adapters/cursor/gen/agent_pb.ts +15274 -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/kv-store.ts +52 -0
- package/src/adapters/cursor/live-models.ts +269 -0
- package/src/adapters/cursor/live-smoke-gate.ts +41 -0
- package/src/adapters/cursor/live-transport.ts +1653 -0
- package/src/adapters/cursor/mcp-config.ts +42 -0
- package/src/adapters/cursor/mcp-manager.ts +333 -0
- package/src/adapters/cursor/message-mapper.ts +49 -0
- package/src/adapters/cursor/native-exec-common.ts +76 -0
- package/src/adapters/cursor/native-exec-desktop.ts +184 -0
- package/src/adapters/cursor/native-exec-fs.ts +332 -0
- package/src/adapters/cursor/native-exec-mcp.ts +153 -0
- package/src/adapters/cursor/native-exec-network.ts +43 -0
- package/src/adapters/cursor/native-exec-shell.ts +547 -0
- package/src/adapters/cursor/native-exec-tools.ts +118 -0
- package/src/adapters/cursor/native-exec.ts +663 -0
- package/src/adapters/cursor/protobuf-events.ts +1381 -0
- package/src/adapters/cursor/protobuf-request.ts +1032 -0
- package/src/adapters/cursor/request-builder.ts +461 -0
- package/src/adapters/cursor/thread-continuity.ts +67 -0
- package/src/adapters/cursor/tool-definitions.ts +735 -0
- package/src/adapters/cursor/tool-result-normalize.ts +92 -0
- package/src/adapters/cursor/transport-retry.ts +132 -0
- package/src/adapters/cursor/transport.ts +79 -0
- package/src/adapters/cursor/types.ts +79 -0
- package/src/adapters/cursor.ts +322 -0
- package/src/adapters/google-antigravity-hosts.ts +48 -0
- package/src/adapters/google-antigravity-replay.ts +827 -0
- package/src/adapters/google-antigravity-tools.ts +105 -0
- package/src/adapters/google-antigravity-wire.ts +141 -0
- package/src/adapters/google-errors.ts +92 -0
- package/src/adapters/google-http.ts +460 -0
- package/src/adapters/google-tool-schema.ts +238 -0
- package/src/adapters/google-truncation.ts +24 -0
- package/src/adapters/google-wire-compiler.ts +232 -0
- package/src/adapters/google.ts +1377 -0
- package/src/adapters/identity.ts +77 -0
- package/src/adapters/image.ts +23 -0
- package/src/adapters/kiro-constants.ts +16 -0
- package/src/adapters/kiro-errors.ts +208 -0
- package/src/adapters/kiro-events.ts +197 -0
- package/src/adapters/kiro-images.ts +129 -0
- package/src/adapters/kiro-retry.ts +312 -0
- package/src/adapters/kiro-thinking.ts +112 -0
- package/src/adapters/kiro-tool-fallback.ts +36 -0
- package/src/adapters/kiro-tools.ts +224 -0
- package/src/adapters/kiro-truncation.ts +33 -0
- package/src/adapters/kiro-wire.ts +129 -0
- package/src/adapters/kiro.ts +1936 -0
- package/src/adapters/mimo-free.ts +280 -0
- package/src/adapters/openai-chat-url.ts +11 -0
- package/src/adapters/openai-chat.ts +1980 -0
- package/src/adapters/openai-responses-url.ts +16 -0
- package/src/adapters/openai-responses.ts +1911 -0
- package/src/adapters/registry.ts +175 -0
- package/src/adapters/responses-tool-schema.ts +67 -0
- package/src/adapters/run-turn-queue.ts +114 -0
- package/src/adapters/tool-call-id.ts +119 -0
- package/src/adapters/tool-catalog-nudge.ts +154 -0
- package/src/adapters/upstream-http-error.ts +48 -0
- package/src/adapters/xai-web-search.ts +185 -0
- package/src/bridge.ts +1986 -0
- package/src/chat/inbound.ts +319 -0
- package/src/chat/outbound.ts +821 -0
- package/src/claude/agents-inject.ts +266 -0
- package/src/claude/alias.ts +149 -0
- package/src/claude/auth-detect.ts +229 -0
- package/src/claude/auth-mode-migration.ts +32 -0
- package/src/claude/auth-mode.ts +62 -0
- package/src/claude/context-windows.ts +205 -0
- package/src/claude/desktop-3p-guard.ts +35 -0
- package/src/claude/desktop-3p-paths.ts +84 -0
- package/src/claude/desktop-3p.ts +615 -0
- package/src/claude/desktop-health.ts +26 -0
- package/src/claude/desktop-profile.ts +263 -0
- package/src/claude/gateway-cache.ts +107 -0
- package/src/claude/inbound-debug.ts +163 -0
- package/src/claude/inbound.ts +578 -0
- package/src/claude/model-info.ts +174 -0
- package/src/claude/outbound.ts +926 -0
- package/src/cli/access.ts +108 -0
- package/src/cli/account-api.ts +302 -0
- package/src/cli/account-auth.ts +250 -0
- package/src/cli/account-catalog-refresh.ts +14 -0
- package/src/cli/account-extended.ts +737 -0
- package/src/cli/account-main.ts +317 -0
- package/src/cli/account.ts +299 -0
- package/src/cli/agent-driven.ts +70 -0
- package/src/cli/agent.ts +290 -0
- package/src/cli/catalog-prewarm.ts +27 -0
- package/src/cli/claude-agent-startup-sync.ts +73 -0
- package/src/cli/claude-desktop.ts +213 -0
- package/src/cli/claude.ts +355 -0
- package/src/cli/codex-log-guard-doctor.ts +103 -0
- package/src/cli/codex-shim-autorestore.ts +47 -0
- package/src/cli/codex-shim-readiness.ts +76 -0
- package/src/cli/combo.ts +127 -0
- package/src/cli/config-command.ts +209 -0
- package/src/cli/debug.ts +228 -0
- package/src/cli/dispatch.ts +585 -0
- package/src/cli/doctor.ts +1202 -0
- package/src/cli/ensure-desired-integrations.ts +152 -0
- package/src/cli/export-command.ts +213 -0
- package/src/cli/help.ts +101 -0
- package/src/cli/index.ts +973 -0
- package/src/cli/init.ts +211 -0
- package/src/cli/integrations.ts +260 -0
- package/src/cli/interactive-confirm.ts +133 -0
- 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 +245 -0
- package/src/cli/models.ts +422 -0
- package/src/cli/observe.ts +206 -0
- package/src/cli/opencode.ts +588 -0
- package/src/cli/provider-replit.ts +232 -0
- package/src/cli/provider-runtime.ts +179 -0
- package/src/cli/provider.ts +492 -0
- package/src/cli/ready.ts +301 -0
- package/src/cli/registry.ts +422 -0
- package/src/cli/replit-gateway-key-input.ts +138 -0
- package/src/cli/root.ts +86 -0
- package/src/cli/route-policy.ts +92 -0
- package/src/cli/runtime-api.ts +328 -0
- package/src/cli/star-prompt.ts +211 -0
- package/src/cli/status-oauth.ts +78 -0
- package/src/cli/status.ts +328 -0
- package/src/cli/system-command.ts +112 -0
- package/src/cli/system-restart-client.ts +146 -0
- package/src/cli/tray-proxy.ts +199 -0
- package/src/cli/v2.ts +268 -0
- package/src/cli.ts +10 -0
- package/src/clients/config-export.ts +1704 -0
- package/src/codex/account-id.ts +34 -0
- package/src/codex/account-label.ts +47 -0
- package/src/codex/account-lifecycle.ts +172 -0
- package/src/codex/account-namespace-match.ts +63 -0
- package/src/codex/account-namespaces.ts +195 -0
- package/src/codex/account-pause.ts +20 -0
- package/src/codex/account-priority.ts +83 -0
- package/src/codex/account-runtime-state.ts +31 -0
- package/src/codex/account-store.ts +544 -0
- package/src/codex/account-usability.ts +43 -0
- package/src/codex/admission.ts +256 -0
- package/src/codex/affinity-debug.ts +162 -0
- package/src/codex/agent-roles-sync.ts +225 -0
- package/src/codex/agent-roles.ts +238 -0
- package/src/codex/app-server-processes.ts +1143 -0
- package/src/codex/app-server-restart-service.ts +232 -0
- package/src/codex/auth-api.ts +2147 -0
- package/src/codex/auth-collision.ts +109 -0
- package/src/codex/auth-context.ts +665 -0
- package/src/codex/autostart-health.ts +156 -0
- package/src/codex/catalog/account-models.ts +67 -0
- package/src/codex/catalog/aggregation.ts +436 -0
- package/src/codex/catalog/bundled.ts +549 -0
- package/src/codex/catalog/effort.ts +446 -0
- package/src/codex/catalog/filesystem-evidence.ts +302 -0
- package/src/codex/catalog/kinds.ts +2 -0
- package/src/codex/catalog/metadata.ts +664 -0
- package/src/codex/catalog/native-models.ts +72 -0
- package/src/codex/catalog/parsing.ts +650 -0
- package/src/codex/catalog/provider-fetch.ts +2064 -0
- package/src/codex/catalog/sync.ts +1883 -0
- 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 +14 -0
- 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/data/upstream-models.json +830 -0
- package/src/codex/desired-state.ts +230 -0
- package/src/codex/exec-invocation.ts +22 -0
- package/src/codex/features.ts +1566 -0
- 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 +108 -0
- package/src/codex/history-provider.ts +979 -0
- package/src/codex/history-transition.ts +105 -0
- package/src/codex/history-worker.ts +220 -0
- package/src/codex/home.ts +206 -0
- package/src/codex/inject-coordination.ts +290 -0
- package/src/codex/inject.ts +1733 -0
- package/src/codex/injected-marker.ts +106 -0
- 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 +225 -0
- 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 +56 -0
- package/src/codex/main-account.ts +68 -0
- package/src/codex/management-convergence.ts +167 -0
- package/src/codex/model-cache.ts +273 -0
- 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 +144 -0
- package/src/codex/plan-from-token.ts +140 -0
- package/src/codex/plan.ts +40 -0
- package/src/codex/plugins-doctor.ts +242 -0
- package/src/codex/pool-rotation.ts +295 -0
- package/src/codex/project-config-warnings.ts +425 -0
- 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 +573 -0
- package/src/codex/refresh.ts +62 -0
- package/src/codex/reset-credit-recovery.ts +1044 -0
- package/src/codex/routing.ts +1888 -0
- package/src/codex/runtime.ts +659 -0
- package/src/codex/shim.ts +2170 -0
- package/src/codex/subagent-defaults.ts +550 -0
- package/src/codex/subagent-model-fallback.ts +784 -0
- package/src/codex/sync.ts +319 -0
- 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 +298 -0
- package/src/codex/websocket-registry.ts +100 -0
- package/src/codex/write-coordination.ts +114 -0
- package/src/combos/failover.ts +160 -0
- package/src/combos/index.ts +45 -0
- package/src/combos/request.ts +94 -0
- package/src/combos/resolve.ts +232 -0
- package/src/combos/types.ts +398 -0
- package/src/config/provider-name.ts +24 -0
- package/src/config.ts +4041 -0
- package/src/fork/register.ts +3 -0
- package/src/generated/compatibility-version.json +3116 -0
- package/src/generated/model-metadata.ts +106 -0
- package/src/github/star-state.ts +203 -0
- package/src/grok/inject.ts +530 -0
- package/src/grok/inspect.ts +45 -0
- package/src/grok/status.ts +121 -0
- package/src/grok/sync.ts +66 -0
- package/src/images/artifacts.ts +516 -0
- package/src/images/fulfill-video.ts +163 -0
- package/src/images/fulfill.ts +149 -0
- package/src/images/index.ts +4 -0
- package/src/images/loop.ts +955 -0
- package/src/images/plan.ts +143 -0
- package/src/images/synthetic-tool.ts +133 -0
- package/src/images/types.ts +41 -0
- package/src/images/xai-client.ts +141 -0
- package/src/images/xai-video-client.ts +163 -0
- package/src/index.ts +22 -0
- 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/abort.ts +146 -0
- package/src/lib/admin-secrets.ts +25 -0
- package/src/lib/admission.ts +83 -0
- package/src/lib/app-owned-memory-stores.ts +195 -0
- package/src/lib/app-owned-memory.ts +265 -0
- package/src/lib/bounded-body.ts +346 -0
- package/src/lib/bun-binary-validator.d.mts +3 -0
- package/src/lib/bun-binary-validator.mjs +18 -0
- package/src/lib/bun-runtime.ts +184 -0
- package/src/lib/bun-stream-caps.ts +130 -0
- package/src/lib/codex-restart-contract.ts +120 -0
- package/src/lib/config-ownership.ts +364 -0
- package/src/lib/crash-guard.ts +344 -0
- package/src/lib/debug-log-buffer.ts +83 -0
- package/src/lib/debug-settings.ts +108 -0
- package/src/lib/debug.ts +31 -0
- package/src/lib/destination-policy.ts +380 -0
- package/src/lib/errors.ts +406 -0
- package/src/lib/eventstream-decoder.ts +253 -0
- package/src/lib/fabric-task-execution-authority.ts +7 -0
- package/src/lib/fabric-task-host.ts +29 -0
- package/src/lib/gcp-adc.ts +341 -0
- package/src/lib/injection-debug-log.ts +58 -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/open-url.ts +25 -0
- package/src/lib/optional-shutdown-hooks.ts +57 -0
- package/src/lib/pinned-http.ts +270 -0
- package/src/lib/privacy.ts +20 -0
- package/src/lib/process-control.ts +168 -0
- package/src/lib/provider-outbound.ts +210 -0
- package/src/lib/provider-url.ts +14 -0
- package/src/lib/proxy-env.ts +18 -0
- package/src/lib/redact.ts +521 -0
- package/src/lib/retry-after.ts +55 -0
- package/src/lib/self-launch-argv.ts +15 -0
- package/src/lib/server-resource-ownership.ts +71 -0
- package/src/lib/service-secrets.ts +25 -0
- package/src/lib/shadow-call.ts +61 -0
- package/src/lib/sidecar-tracker.ts +52 -0
- package/src/lib/sse-decoder.ts +364 -0
- package/src/lib/state-store-registrations.ts +119 -0
- package/src/lib/state-store-sweeper.ts +184 -0
- package/src/lib/system-restart-contract.ts +73 -0
- package/src/lib/test-home-guard.ts +90 -0
- package/src/lib/token-estimate.ts +86 -0
- package/src/lib/tool-argument-integers.ts +202 -0
- package/src/lib/translator-budget.ts +400 -0
- package/src/lib/upstream-http-version.ts +57 -0
- package/src/lib/upstream-reachability.ts +95 -0
- package/src/lib/upstream-retry.ts +392 -0
- package/src/lib/win-exec.ts +115 -0
- package/src/lib/win-paths.ts +68 -0
- package/src/lib/windows-atomic-replace.ts +156 -0
- package/src/lib/windows-elevation.ts +773 -0
- package/src/lib/windows-secret-acl.ts +854 -0
- 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 +403 -0
- 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-routing.ts +594 -0
- package/src/oauth/anthropic.ts +188 -0
- package/src/oauth/antigravity-routing.ts +151 -0
- package/src/oauth/callback-server.ts +300 -0
- package/src/oauth/chatgpt.ts +161 -0
- package/src/oauth/command-code.ts +239 -0
- package/src/oauth/cursor.ts +252 -0
- package/src/oauth/github-copilot.ts +428 -0
- package/src/oauth/google-antigravity.ts +262 -0
- package/src/oauth/health.ts +407 -0
- package/src/oauth/index.ts +1504 -0
- package/src/oauth/key-providers.ts +124 -0
- package/src/oauth/kimi.ts +227 -0
- package/src/oauth/kiro-credentials.ts +726 -0
- package/src/oauth/kiro.ts +621 -0
- package/src/oauth/local-token-detect.ts +130 -0
- package/src/oauth/log.ts +50 -0
- package/src/oauth/login-cli.ts +223 -0
- package/src/oauth/nous.ts +798 -0
- package/src/oauth/pkce.ts +15 -0
- package/src/oauth/store.ts +728 -0
- package/src/oauth/token-guardian.ts +309 -0
- package/src/oauth/types.ts +62 -0
- package/src/oauth/xai.ts +241 -0
- package/src/providers/alibaba-region-backup.ts +75 -0
- package/src/providers/alibaba-region-migration.ts +156 -0
- package/src/providers/alibaba-region-startup.ts +36 -0
- package/src/providers/antigravity-models.ts +695 -0
- package/src/providers/antigravity-quota.ts +216 -0
- package/src/providers/api-keys.ts +140 -0
- package/src/providers/base-url-choices.ts +74 -0
- package/src/providers/codex-capacity.ts +292 -0
- package/src/providers/command-code-efforts.ts +144 -0
- package/src/providers/context-cap.ts +82 -0
- package/src/providers/cursor-pool.ts +72 -0
- package/src/providers/derive.ts +586 -0
- package/src/providers/fastwire.ts +501 -0
- package/src/providers/free-directory.ts +187 -0
- package/src/providers/github-copilot-transport.ts +56 -0
- package/src/providers/google-vertex-location.ts +14 -0
- package/src/providers/key-failover.ts +271 -0
- package/src/providers/kiro-models.ts +67 -0
- package/src/providers/label.ts +19 -0
- package/src/providers/model-discovery-limits.ts +16 -0
- package/src/providers/model-discovery.ts +449 -0
- package/src/providers/model-rename-migration.ts +255 -0
- package/src/providers/model-rename-startup.ts +28 -0
- package/src/providers/openai-sidecar.ts +243 -0
- package/src/providers/openai-tier-startup.ts +56 -0
- package/src/providers/openai-tiers.ts +423 -0
- package/src/providers/openai-virtual-models.ts +83 -0
- package/src/providers/opencode-zen-rate-limit.ts +102 -0
- package/src/providers/openrouter-routing.ts +102 -0
- package/src/providers/provider-id-rewrite.ts +185 -0
- package/src/providers/quota.ts +2345 -0
- package/src/providers/registry.ts +2918 -0
- package/src/providers/replit/constants.ts +27 -0
- package/src/providers/replit/derive.ts +85 -0
- package/src/providers/replit/headers.ts +28 -0
- package/src/providers/replit/origin.ts +55 -0
- package/src/providers/replit/pair-install-response.ts +72 -0
- package/src/providers/replit/probe.ts +199 -0
- package/src/providers/replit/setup.ts +350 -0
- package/src/providers/request-pacing.ts +310 -0
- package/src/providers/service-tier.ts +277 -0
- package/src/providers/slug-codec.ts +103 -0
- 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 +148 -0
- package/src/reasoning-effort.ts +183 -0
- package/src/responses/compaction.ts +142 -0
- 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 +838 -0
- package/src/responses/provider-continuation.ts +98 -0
- package/src/responses/provider-opaque-metadata.ts +73 -0
- package/src/responses/reasoning-envelope.ts +60 -0
- package/src/responses/reasoning-replay-cache.ts +426 -0
- package/src/responses/schema.ts +165 -0
- package/src/responses/spill-store.ts +459 -0
- package/src/responses/state.ts +1433 -0
- package/src/responses/thought-signature-replay.ts +347 -0
- package/src/responses/tool-groups.ts +19 -0
- package/src/responses/tool-search-compat.ts +301 -0
- package/src/responses/truncated-stop-reason.ts +60 -0
- package/src/router.ts +761 -0
- 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 +53 -0
- package/src/server/auth-cors.ts +751 -0
- package/src/server/background-lifecycle.ts +182 -0
- package/src/server/chat-completions.ts +442 -0
- package/src/server/chat-native-sse.ts +331 -0
- package/src/server/chat-native.ts +426 -0
- package/src/server/claude-messages.ts +1030 -0
- package/src/server/direct-local-http.ts +347 -0
- package/src/server/effort-policy.ts +190 -0
- package/src/server/github-copilot-responses-repair.ts +338 -0
- package/src/server/gui-static.ts +152 -0
- package/src/server/image-retry.ts +42 -0
- package/src/server/images.ts +568 -0
- package/src/server/index.ts +1813 -0
- package/src/server/lifecycle.ts +498 -0
- package/src/server/live.ts +717 -0
- 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 +1433 -0
- package/src/server/management/api-access.ts +141 -0
- package/src/server/management/api-key-usage.ts +193 -0
- package/src/server/management/body.ts +41 -0
- package/src/server/management/combo-routes.ts +263 -0
- package/src/server/management/config-routes.ts +835 -0
- package/src/server/management/context.ts +113 -0
- 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 +586 -0
- package/src/server/management/model-routes.ts +560 -0
- 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 +637 -0
- package/src/server/management/provider-capability-config.ts +48 -0
- package/src/server/management/provider-routes.ts +1033 -0
- package/src/server/management/replit-provider-routes.ts +86 -0
- 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 +385 -0
- package/src/server/management/shared.ts +286 -0
- package/src/server/management/sidebar-routes.ts +106 -0
- 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 +435 -0
- package/src/server/management/system-routes.ts +194 -0
- package/src/server/management/usage-summary-cache.ts +94 -0
- 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 +314 -0
- package/src/server/management-auth.ts +482 -0
- package/src/server/memory-watchdog.ts +156 -0
- package/src/server/passive-route-linker.ts +66 -0
- package/src/server/port-reclaim.ts +307 -0
- package/src/server/ports.ts +156 -0
- package/src/server/proxy-liveness.ts +328 -0
- package/src/server/readiness.ts +99 -0
- package/src/server/relay-eager.ts +353 -0
- package/src/server/relay.ts +1209 -0
- package/src/server/request-decompress.ts +239 -0
- package/src/server/request-log-conversation.ts +168 -0
- package/src/server/request-log.ts +1259 -0
- 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 +551 -0
- package/src/server/responses/compact.ts +771 -0
- package/src/server/responses/core.ts +5389 -0
- package/src/server/responses/empty-completion-guard.ts +276 -0
- package/src/server/responses/encrypted-payload.ts +331 -0
- package/src/server/responses/fetch-helpers.ts +232 -0
- package/src/server/responses/input-admission.ts +185 -0
- package/src/server/responses/pacing-overload.ts +13 -0
- package/src/server/responses/passthrough-error.ts +78 -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 +251 -0
- package/src/server/responses/upstream-error.ts +53 -0
- package/src/server/responses/ws-upstream.ts +308 -0
- package/src/server/responses-custom-tool-repair.ts +282 -0
- package/src/server/responses-image-gen-repair.ts +132 -0
- package/src/server/responses-item-id-repair.ts +272 -0
- 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 +25 -0
- package/src/server/search.ts +201 -0
- package/src/server/sse-frame-buffer.ts +292 -0
- package/src/server/sse-payload-rewrite.ts +263 -0
- package/src/server/startup-action-control.ts +315 -0
- package/src/server/startup-health-cache.ts +131 -0
- package/src/server/system-env.ts +484 -0
- package/src/server/windows-tcp-drop.ts +184 -0
- package/src/server/windows-tray-control.ts +41 -0
- package/src/server/ws-bridge.ts +472 -0
- package/src/service-manager-probe.ts +892 -0
- package/src/service.ts +3575 -0
- package/src/sidecar/auth.ts +92 -0
- package/src/sidecar/candidates.ts +83 -0
- package/src/stall-timeout.ts +20 -0
- package/src/storage/cleanup-job.ts +57 -0
- package/src/storage/cleanup.ts +3085 -0
- package/src/storage/policy-job.ts +457 -0
- package/src/storage/policy-scheduler.ts +40 -0
- package/src/storage/policy-worker.ts +59 -0
- package/src/storage/policy.ts +527 -0
- package/src/storage/restore-job.ts +299 -0
- package/src/storage/restore-worker.ts +58 -0
- package/src/storage/scanner.ts +238 -0
- package/src/storage/storage-mutation-coordinator.ts +139 -0
- package/src/storage/worker-lifecycle.ts +215 -0
- package/src/tray/assets/opencodex-tray-offline.ico +0 -0
- package/src/tray/assets/opencodex-tray-online.ico +0 -0
- package/src/tray/assets/opencodex-tray-warning.ico +0 -0
- package/src/tray/assets/opencodex-tray.png +0 -0
- package/src/tray/windows-tray.ps1 +364 -0
- package/src/tray/windows.ts +757 -0
- package/src/types/accounts.ts +37 -0
- package/src/types/config.ts +876 -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 +106 -0
- package/src/update/badge.ts +72 -0
- package/src/update/index.ts +415 -0
- package/src/update/job.ts +1887 -0
- package/src/update/notify.ts +263 -0
- package/src/update/npm-cache-preflight.d.mts +47 -0
- package/src/update/npm-cache-preflight.mjs +201 -0
- package/src/update/npm-invocation.d.mts +23 -0
- package/src/update/npm-invocation.mjs +94 -0
- package/src/update/transactional-install.d.mts +22 -0
- package/src/update/transactional-install.mjs +259 -0
- package/src/update/tray-update-plan.d.mts +18 -0
- package/src/update/tray-update-plan.mjs +38 -0
- package/src/usage/cost.ts +625 -0
- package/src/usage/debug.ts +97 -0
- package/src/usage/expected-prices.ts +416 -0
- package/src/usage/log.ts +1223 -0
- package/src/usage/summary.ts +753 -0
- package/src/usage/totals.ts +14 -0
- 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 +189 -0
- package/src/vision/backends.ts +97 -0
- package/src/vision/describe.ts +131 -0
- package/src/vision/eligibility.ts +250 -0
- package/src/vision/index.ts +681 -0
- 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 +195 -0
- package/src/web-search/backends.ts +108 -0
- package/src/web-search/exa-executor.ts +88 -0
- package/src/web-search/executor.ts +113 -0
- package/src/web-search/format-result.ts +89 -0
- package/src/web-search/gemini-executor.ts +141 -0
- package/src/web-search/index.ts +331 -0
- package/src/web-search/loop.ts +896 -0
- package/src/web-search/parse.ts +315 -0
- package/src/web-search/progress-stream.ts +342 -0
- package/src/web-search/sources.ts +60 -0
- package/src/web-search/synthetic-tool.ts +47 -0
- 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
|
+
}
|