mixdog 0.7.1
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/.claude-plugin/marketplace.json +31 -0
- package/.claude-plugin/plugin.json +20 -0
- package/.gitattributes +34 -0
- package/.mcp.json +14 -0
- package/ARCHITECTURE.md +77 -0
- package/CHANGELOG.md +7 -0
- package/CONTRIBUTING.md +45 -0
- package/DATA-FLOW.md +79 -0
- package/LICENSE +21 -0
- package/README.md +389 -0
- package/SECURITY.md +138 -0
- package/UNINSTALL.md +112 -0
- package/agents/maintenance.md +5 -0
- package/agents/memory-classification.md +30 -0
- package/agents/scheduler-task.md +18 -0
- package/agents/webhook-handler.md +27 -0
- package/agents/worker.md +24 -0
- package/bin/bridge +133 -0
- package/bin/statusline-launcher.mjs +78 -0
- package/bin/statusline-lib.mjs +550 -0
- package/bin/statusline.mjs +607 -0
- package/bun.lock +802 -0
- package/commands/config.md +16 -0
- package/commands/doctor.md +13 -0
- package/commands/setup.md +17 -0
- package/defaults/cycle3-review-prompt.md +90 -0
- package/defaults/hidden-roles.json +65 -0
- package/defaults/memory-chunk-prompt.md +63 -0
- package/defaults/memory-promote-prompt.md +135 -0
- package/defaults/mixdog-config.template.json +27 -0
- package/defaults/user-workflow.json +8 -0
- package/defaults/user-workflow.md +12 -0
- package/hooks/hooks.json +73 -0
- package/hooks/lib/active-instance.cjs +77 -0
- package/hooks/lib/permission-evaluator.cjs +411 -0
- package/hooks/lib/permission-route.cjs +63 -0
- package/hooks/lib/permission-rules.cjs +170 -0
- package/hooks/lib/settings-loader.cjs +116 -0
- package/hooks/post-tool-use.cjs +84 -0
- package/hooks/pre-mcp-sandbox.cjs +158 -0
- package/hooks/pre-tool-subagent.cjs +253 -0
- package/hooks/session-start.cjs +1372 -0
- package/hooks/turn-timer.cjs +82 -0
- package/lib/claude-md-writer.cjs +386 -0
- package/lib/config-cjs.cjs +61 -0
- package/lib/hook-pipe-path.cjs +10 -0
- package/lib/keychain-cjs.cjs +263 -0
- package/lib/plugin-paths.cjs +61 -0
- package/lib/rules-builder.cjs +241 -0
- package/lib/text-utils.cjs +61 -0
- package/native/README.md +117 -0
- package/native/prebuilt/linux-aarch64/mixdog-shim +0 -0
- package/native/prebuilt/linux-x86_64/mixdog-shim +0 -0
- package/native/prebuilt/macos-aarch64/mixdog-shim +0 -0
- package/native/prebuilt/macos-x86_64/mixdog-shim +0 -0
- package/native/prebuilt/windows-x86_64/mixdog-shim.exe +0 -0
- package/package.json +107 -0
- package/prompts/code-review.txt +16 -0
- package/prompts/security-audit.txt +17 -0
- package/rules/bridge/00-common.md +39 -0
- package/rules/bridge/20-skip-protocol.md +18 -0
- package/rules/bridge/30-explorer.md +33 -0
- package/rules/bridge/40-cycle1-agent.md +52 -0
- package/rules/bridge/41-cycle2-agent.md +62 -0
- package/rules/bridge/42-cycle3-agent.md +44 -0
- package/rules/lead/00-tool-lead.md +61 -0
- package/rules/lead/01-general.md +23 -0
- package/rules/lead/02-channels.md +49 -0
- package/rules/lead/03-team.md +27 -0
- package/rules/lead/04-workflow.md +20 -0
- package/rules/shared/00-language.md +14 -0
- package/rules/shared/01-tool.md +138 -0
- package/scripts/bootstrap.mjs +184 -0
- package/scripts/bridge-unify-smoke.mjs +308 -0
- package/scripts/build-runtime-linux.sh +348 -0
- package/scripts/build-runtime-macos.sh +217 -0
- package/scripts/build-runtime-windows.ps1 +242 -0
- package/scripts/builtin-utils-smoke.mjs +392 -0
- package/scripts/check-json.mjs +45 -0
- package/scripts/check-syntax-changed.mjs +102 -0
- package/scripts/check-syntax.mjs +58 -0
- package/scripts/code-graph-batch.test.mjs +33 -0
- package/scripts/config-preserve-smoke.mjs +180 -0
- package/scripts/doctor.mjs +484 -0
- package/scripts/edit-normalize-fuzz.mjs +130 -0
- package/scripts/edit-normalize-smoke.mjs +401 -0
- package/scripts/edit-operation-smoke.mjs +369 -0
- package/scripts/edit2-smoke.mjs +63 -0
- package/scripts/fuzzy-e2e.mjs +28 -0
- package/scripts/fuzzy-smoke.mjs +26 -0
- package/scripts/generate-runtime-manifest.mjs +166 -0
- package/scripts/guard-smoke.mjs +66 -0
- package/scripts/hidden-role-schema-smoke.mjs +162 -0
- package/scripts/hook-routing-smoke.mjs +29 -0
- package/scripts/inject-input.ps1 +204 -0
- package/scripts/io-complex-smoke.mjs +667 -0
- package/scripts/io-explore-bench.mjs +424 -0
- package/scripts/io-guardrails-smoke.mjs +205 -0
- package/scripts/io-mini-bench-baseline.json +11 -0
- package/scripts/io-mini-bench.mjs +216 -0
- package/scripts/io-route-harness.mjs +933 -0
- package/scripts/io-telemetry-report.mjs +691 -0
- package/scripts/mutation-bench.mjs +564 -0
- package/scripts/mutation-io-smoke.mjs +1081 -0
- package/scripts/native-patch-bridge-smoke.mjs +288 -0
- package/scripts/native-patch-smoke.mjs +304 -0
- package/scripts/patch-interior-context-smoke.mjs +49 -0
- package/scripts/patch-newline-utf8-smoke.mjs +157 -0
- package/scripts/perf-hook-smoke.mjs +71 -0
- package/scripts/permission-eval-smoke.mjs +426 -0
- package/scripts/prep-patch.mjs +53 -0
- package/scripts/prep-shim.mjs +96 -0
- package/scripts/provider-cache-smoke.mjs +687 -0
- package/scripts/report-runtime-health.mjs +132 -0
- package/scripts/run-mcp.mjs +1547 -0
- package/scripts/salvage-v4a-shatter.test.mjs +58 -0
- package/scripts/scoped-cache-io-smoke.mjs +103 -0
- package/scripts/shell-policy-round3-smoke.mjs +46 -0
- package/scripts/smoke-runtime-negative.ps1 +100 -0
- package/scripts/smoke-runtime-negative.sh +95 -0
- package/scripts/stall-policy-smoke.mjs +50 -0
- package/scripts/start-memory-worker.mjs +23 -0
- package/scripts/statusline-launcher-smoke.mjs +82 -0
- package/scripts/stress-atomic-write.mjs +1028 -0
- package/scripts/test-config-rmw-restore.mjs +122 -0
- package/scripts/test-fault-inject.mjs +164 -0
- package/scripts/test-large-file.mjs +174 -0
- package/scripts/tool-edge-smoke.mjs +209 -0
- package/scripts/uninstall.mjs +201 -0
- package/scripts/webhook-selfheal-smoke.mjs +29 -0
- package/scripts/write-overwrite-guard-smoke.mjs +56 -0
- package/server-main.mjs +3055 -0
- package/server.mjs +468 -0
- package/setup/config-merge.mjs +254 -0
- package/setup/install.mjs +120 -0
- package/setup/launch-core.mjs +507 -0
- package/setup/launch.mjs +101 -0
- package/setup/setup-server.mjs +3206 -0
- package/setup/setup.html +3693 -0
- package/skills/retro-skill-proposer/SKILL.md +92 -0
- package/skills/schedule-add/SKILL.md +77 -0
- package/skills/setup/SKILL.md +346 -0
- package/skills/webhook-add/SKILL.md +81 -0
- package/src/agent/bridge-stall-watchdog.mjs +337 -0
- package/src/agent/index.mjs +2138 -0
- package/src/agent/orchestrator/activity-bus.mjs +38 -0
- package/src/agent/orchestrator/ai-wrapped-dispatch.mjs +1010 -0
- package/src/agent/orchestrator/bridge-retry.mjs +220 -0
- package/src/agent/orchestrator/bridge-trace.mjs +583 -0
- package/src/agent/orchestrator/cache-mtime.mjs +58 -0
- package/src/agent/orchestrator/config.mjs +358 -0
- package/src/agent/orchestrator/context/collect.mjs +651 -0
- package/src/agent/orchestrator/dispatch-persist.mjs +549 -0
- package/src/agent/orchestrator/drain-registry.mjs +50 -0
- package/src/agent/orchestrator/explore-validator.mjs +8 -0
- package/src/agent/orchestrator/internal-roles.mjs +118 -0
- package/src/agent/orchestrator/internal-tools.mjs +88 -0
- package/src/agent/orchestrator/jobs.mjs +116 -0
- package/src/agent/orchestrator/mcp/client.mjs +364 -0
- package/src/agent/orchestrator/providers/anthropic-betas.mjs +21 -0
- package/src/agent/orchestrator/providers/anthropic-oauth.mjs +1745 -0
- package/src/agent/orchestrator/providers/anthropic.mjs +437 -0
- package/src/agent/orchestrator/providers/gemini.mjs +1175 -0
- package/src/agent/orchestrator/providers/grok-oauth.mjs +782 -0
- package/src/agent/orchestrator/providers/model-catalog.mjs +241 -0
- package/src/agent/orchestrator/providers/openai-compat.mjs +1467 -0
- package/src/agent/orchestrator/providers/openai-oauth-ws.mjs +1890 -0
- package/src/agent/orchestrator/providers/openai-oauth.mjs +1307 -0
- package/src/agent/orchestrator/providers/openai-ws.mjs +104 -0
- package/src/agent/orchestrator/providers/registry.mjs +192 -0
- package/src/agent/orchestrator/providers/retry-classifier.mjs +325 -0
- package/src/agent/orchestrator/session/abort-lookup.mjs +13 -0
- package/src/agent/orchestrator/session/cache/post-edit-marks.mjs +42 -0
- package/src/agent/orchestrator/session/cache/prefetch-cache.mjs +142 -0
- package/src/agent/orchestrator/session/cache/read-cache.mjs +319 -0
- package/src/agent/orchestrator/session/cache/scoped-cache-outcome.mjs +11 -0
- package/src/agent/orchestrator/session/cache/scoped-cache.mjs +361 -0
- package/src/agent/orchestrator/session/cache/util.mjs +49 -0
- package/src/agent/orchestrator/session/loop.mjs +1478 -0
- package/src/agent/orchestrator/session/manager.mjs +1975 -0
- package/src/agent/orchestrator/session/read-dedup.mjs +6 -0
- package/src/agent/orchestrator/session/result-classification.mjs +65 -0
- package/src/agent/orchestrator/session/save-session-worker.mjs +18 -0
- package/src/agent/orchestrator/session/store.mjs +624 -0
- package/src/agent/orchestrator/session/stream-watchdog.mjs +130 -0
- package/src/agent/orchestrator/session/tool-result-offload.mjs +166 -0
- package/src/agent/orchestrator/session/trim.mjs +491 -0
- package/src/agent/orchestrator/smart-bridge/CACHE-SHARD.md +115 -0
- package/src/agent/orchestrator/smart-bridge/bridge-llm.mjs +327 -0
- package/src/agent/orchestrator/smart-bridge/cache-obs.mjs +150 -0
- package/src/agent/orchestrator/smart-bridge/cache-strategy.mjs +228 -0
- package/src/agent/orchestrator/smart-bridge/index.mjs +215 -0
- package/src/agent/orchestrator/smart-bridge/profiles.mjs +37 -0
- package/src/agent/orchestrator/smart-bridge/registry.mjs +348 -0
- package/src/agent/orchestrator/smart-bridge/session-builder.mjs +116 -0
- package/src/agent/orchestrator/stall-policy.mjs +195 -0
- package/src/agent/orchestrator/tool-loop-guard.mjs +75 -0
- package/src/agent/orchestrator/tools/bash-policy-scan.mjs +77 -0
- package/src/agent/orchestrator/tools/bash-session.mjs +721 -0
- package/src/agent/orchestrator/tools/builtin/advisory-lock.mjs +171 -0
- package/src/agent/orchestrator/tools/builtin/arg-guard.mjs +455 -0
- package/src/agent/orchestrator/tools/builtin/atomic-write.mjs +236 -0
- package/src/agent/orchestrator/tools/builtin/bash-tool.mjs +480 -0
- package/src/agent/orchestrator/tools/builtin/binary-file.mjs +76 -0
- package/src/agent/orchestrator/tools/builtin/builtin-tools.mjs +256 -0
- package/src/agent/orchestrator/tools/builtin/cache-layers.mjs +386 -0
- package/src/agent/orchestrator/tools/builtin/cwd-utils.mjs +37 -0
- package/src/agent/orchestrator/tools/builtin/device-paths.mjs +154 -0
- package/src/agent/orchestrator/tools/builtin/diagnostics-tool.mjs +292 -0
- package/src/agent/orchestrator/tools/builtin/diff-utils.mjs +109 -0
- package/src/agent/orchestrator/tools/builtin/edit-base-guard.mjs +58 -0
- package/src/agent/orchestrator/tools/builtin/edit-byte-plan.mjs +240 -0
- package/src/agent/orchestrator/tools/builtin/edit-byte-utils.mjs +113 -0
- package/src/agent/orchestrator/tools/builtin/edit-commit.mjs +74 -0
- package/src/agent/orchestrator/tools/builtin/edit-context-utils.mjs +242 -0
- package/src/agent/orchestrator/tools/builtin/edit-diagnostics.mjs +211 -0
- package/src/agent/orchestrator/tools/builtin/edit-engine.mjs +1364 -0
- package/src/agent/orchestrator/tools/builtin/edit-failure-context.mjs +126 -0
- package/src/agent/orchestrator/tools/builtin/edit-hint.mjs +141 -0
- package/src/agent/orchestrator/tools/builtin/edit-match-utils.mjs +194 -0
- package/src/agent/orchestrator/tools/builtin/edit-partial-write.mjs +60 -0
- package/src/agent/orchestrator/tools/builtin/edit-stale-refresh.mjs +168 -0
- package/src/agent/orchestrator/tools/builtin/edit-tool.mjs +173 -0
- package/src/agent/orchestrator/tools/builtin/edit-utf8-guard.mjs +48 -0
- package/src/agent/orchestrator/tools/builtin/fs-reachability.mjs +48 -0
- package/src/agent/orchestrator/tools/builtin/fuzzy-match.mjs +99 -0
- package/src/agent/orchestrator/tools/builtin/glob-walk.mjs +170 -0
- package/src/agent/orchestrator/tools/builtin/grep-formatting.mjs +113 -0
- package/src/agent/orchestrator/tools/builtin/hash-utils.mjs +6 -0
- package/src/agent/orchestrator/tools/builtin/list-formatting.mjs +7 -0
- package/src/agent/orchestrator/tools/builtin/list-tool.mjs +593 -0
- package/src/agent/orchestrator/tools/builtin/native-edit-runner.mjs +89 -0
- package/src/agent/orchestrator/tools/builtin/notebook-edit-tool.mjs +300 -0
- package/src/agent/orchestrator/tools/builtin/open-config-tool.mjs +26 -0
- package/src/agent/orchestrator/tools/builtin/path-diagnostics.mjs +152 -0
- package/src/agent/orchestrator/tools/builtin/path-locks.mjs +35 -0
- package/src/agent/orchestrator/tools/builtin/path-utils.mjs +201 -0
- package/src/agent/orchestrator/tools/builtin/read-args.mjs +103 -0
- package/src/agent/orchestrator/tools/builtin/read-batch.mjs +172 -0
- package/src/agent/orchestrator/tools/builtin/read-constants.mjs +40 -0
- package/src/agent/orchestrator/tools/builtin/read-formatting.mjs +118 -0
- package/src/agent/orchestrator/tools/builtin/read-image-resize.mjs +189 -0
- package/src/agent/orchestrator/tools/builtin/read-image.mjs +88 -0
- package/src/agent/orchestrator/tools/builtin/read-lines.mjs +12 -0
- package/src/agent/orchestrator/tools/builtin/read-mode-tool.mjs +455 -0
- package/src/agent/orchestrator/tools/builtin/read-open.mjs +190 -0
- package/src/agent/orchestrator/tools/builtin/read-range-index.mjs +271 -0
- package/src/agent/orchestrator/tools/builtin/read-ranges.mjs +26 -0
- package/src/agent/orchestrator/tools/builtin/read-single-tool.mjs +728 -0
- package/src/agent/orchestrator/tools/builtin/read-snapshot-runtime.mjs +173 -0
- package/src/agent/orchestrator/tools/builtin/read-special-files.mjs +268 -0
- package/src/agent/orchestrator/tools/builtin/read-streaming.mjs +602 -0
- package/src/agent/orchestrator/tools/builtin/read-tool.mjs +530 -0
- package/src/agent/orchestrator/tools/builtin/read-windows.mjs +107 -0
- package/src/agent/orchestrator/tools/builtin/rename-tool.mjs +196 -0
- package/src/agent/orchestrator/tools/builtin/rg-runner.mjs +422 -0
- package/src/agent/orchestrator/tools/builtin/search-builders.mjs +158 -0
- package/src/agent/orchestrator/tools/builtin/search-tool.mjs +869 -0
- package/src/agent/orchestrator/tools/builtin/shell-analysis.mjs +653 -0
- package/src/agent/orchestrator/tools/builtin/shell-jobs.mjs +936 -0
- package/src/agent/orchestrator/tools/builtin/shell-output.mjs +36 -0
- package/src/agent/orchestrator/tools/builtin/shell-runtime.mjs +214 -0
- package/src/agent/orchestrator/tools/builtin/snapshot-helpers.mjs +143 -0
- package/src/agent/orchestrator/tools/builtin/snapshot-store.mjs +206 -0
- package/src/agent/orchestrator/tools/builtin/snapshot-validation.mjs +98 -0
- package/src/agent/orchestrator/tools/builtin/text-stats.mjs +69 -0
- package/src/agent/orchestrator/tools/builtin/windows-roots.mjs +23 -0
- package/src/agent/orchestrator/tools/builtin/write-tool.mjs +401 -0
- package/src/agent/orchestrator/tools/builtin.mjs +500 -0
- package/src/agent/orchestrator/tools/code-graph-prewarm-worker.mjs +39 -0
- package/src/agent/orchestrator/tools/code-graph-tool-defs.mjs +24 -0
- package/src/agent/orchestrator/tools/code-graph.mjs +4095 -0
- package/src/agent/orchestrator/tools/cwd-tool.mjs +298 -0
- package/src/agent/orchestrator/tools/destructive-warning.mjs +323 -0
- package/src/agent/orchestrator/tools/edit-normalize.mjs +603 -0
- package/src/agent/orchestrator/tools/env-scrub.mjs +100 -0
- package/src/agent/orchestrator/tools/graph-binary-fetcher.mjs +144 -0
- package/src/agent/orchestrator/tools/graph-manifest.json +26 -0
- package/src/agent/orchestrator/tools/host-input.mjs +204 -0
- package/src/agent/orchestrator/tools/mutation-content-cache.mjs +67 -0
- package/src/agent/orchestrator/tools/mutation-planner.mjs +75 -0
- package/src/agent/orchestrator/tools/next-call-utils.mjs +48 -0
- package/src/agent/orchestrator/tools/patch-binary-fetcher.mjs +133 -0
- package/src/agent/orchestrator/tools/patch-manifest.json +26 -0
- package/src/agent/orchestrator/tools/patch-tool-defs.mjs +20 -0
- package/src/agent/orchestrator/tools/patch.mjs +2754 -0
- package/src/agent/orchestrator/tools/progress-message.mjs +118 -0
- package/src/agent/orchestrator/tools/result-compression.mjs +279 -0
- package/src/agent/orchestrator/tools/shell-command.mjs +865 -0
- package/src/agent/orchestrator/tools/shell-exec-policy.mjs +89 -0
- package/src/agent/orchestrator/tools/shell-policy-danger-target.mjs +27 -0
- package/src/agent/orchestrator/tools/shell-policy-imports.mjs +7 -0
- package/src/agent/orchestrator/tools/shell-policy.mjs +345 -0
- package/src/agent/orchestrator/tools/shell-snapshot.mjs +313 -0
- package/src/agent/orchestrator/workflow-store.mjs +93 -0
- package/src/agent/tool-defs.mjs +103 -0
- package/src/channels/backends/discord.mjs +784 -0
- package/src/channels/data/voice-runtime-manifest.json +138 -0
- package/src/channels/index.mjs +3229 -0
- package/src/channels/lib/cli-worker-host.mjs +12 -0
- package/src/channels/lib/config-lock.mjs +13 -0
- package/src/channels/lib/config.mjs +292 -0
- package/src/channels/lib/drop-trace.mjs +71 -0
- package/src/channels/lib/event-pipeline.mjs +81 -0
- package/src/channels/lib/event-queue.mjs +345 -0
- package/src/channels/lib/executor.mjs +168 -0
- package/src/channels/lib/format.mjs +188 -0
- package/src/channels/lib/holidays.mjs +138 -0
- package/src/channels/lib/hook-pipe-server.mjs +802 -0
- package/src/channels/lib/interaction-workflows.mjs +184 -0
- package/src/channels/lib/memory-client.mjs +149 -0
- package/src/channels/lib/output-forwarder.mjs +765 -0
- package/src/channels/lib/runtime-paths.mjs +479 -0
- package/src/channels/lib/scheduler.mjs +723 -0
- package/src/channels/lib/session-control.mjs +36 -0
- package/src/channels/lib/session-discovery.mjs +103 -0
- package/src/channels/lib/settings.mjs +11 -0
- package/src/channels/lib/state-file.mjs +68 -0
- package/src/channels/lib/status-snapshot.mjs +219 -0
- package/src/channels/lib/tool-format.mjs +140 -0
- package/src/channels/lib/transcript-discovery.mjs +195 -0
- package/src/channels/lib/voice-runtime-fetcher.mjs +734 -0
- package/src/channels/lib/webhook.mjs +1179 -0
- package/src/channels/lib/whisper-server.mjs +477 -0
- package/src/channels/tool-defs.mjs +170 -0
- package/src/daemon/host.mjs +118 -0
- package/src/daemon/mcp-transport.mjs +47 -0
- package/src/daemon/session.mjs +100 -0
- package/src/daemon/thin-client.mjs +71 -0
- package/src/daemon/transport.mjs +163 -0
- package/src/memory/data/runtime-manifest.json +40 -0
- package/src/memory/index.mjs +3305 -0
- package/src/memory/lib/agent-ipc.mjs +93 -0
- package/src/memory/lib/bridge-trace-queries.mjs +120 -0
- package/src/memory/lib/core-memory-store.mjs +330 -0
- package/src/memory/lib/embedding-provider.mjs +269 -0
- package/src/memory/lib/embedding-worker.mjs +323 -0
- package/src/memory/lib/llm-worker-host.mjs +17 -0
- package/src/memory/lib/memory-cycle.mjs +11 -0
- package/src/memory/lib/memory-cycle1.mjs +641 -0
- package/src/memory/lib/memory-cycle2.mjs +1284 -0
- package/src/memory/lib/memory-cycle3.mjs +540 -0
- package/src/memory/lib/memory-embed.mjs +299 -0
- package/src/memory/lib/memory-extraction.mjs +5 -0
- package/src/memory/lib/memory-maintenance-store.mjs +32 -0
- package/src/memory/lib/memory-ops-policy.mjs +190 -0
- package/src/memory/lib/memory-recall-id-patch.mjs +15 -0
- package/src/memory/lib/memory-recall-read-query.mjs +7 -0
- package/src/memory/lib/memory-recall-scope-filter.mjs +63 -0
- package/src/memory/lib/memory-recall-store.mjs +621 -0
- package/src/memory/lib/memory-retrievers.mjs +112 -0
- package/src/memory/lib/memory-score.mjs +71 -0
- package/src/memory/lib/memory-text-utils.mjs +58 -0
- package/src/memory/lib/memory.mjs +412 -0
- package/src/memory/lib/model-profile.mjs +85 -0
- package/src/memory/lib/pg/adapter.mjs +308 -0
- package/src/memory/lib/pg/process.mjs +360 -0
- package/src/memory/lib/pg/supervisor.mjs +396 -0
- package/src/memory/lib/project-id-resolver.mjs +86 -0
- package/src/memory/lib/runtime-fetcher.mjs +442 -0
- package/src/memory/lib/trace-store.mjs +728 -0
- package/src/memory/tool-defs.mjs +79 -0
- package/src/search/index.mjs +1173 -0
- package/src/search/lib/backends/anthropic-oauth.mjs +98 -0
- package/src/search/lib/backends/exa.mjs +50 -0
- package/src/search/lib/backends/firecrawl.mjs +61 -0
- package/src/search/lib/backends/gemini-api.mjs +83 -0
- package/src/search/lib/backends/grok-oauth.mjs +86 -0
- package/src/search/lib/backends/index.mjs +150 -0
- package/src/search/lib/backends/openai-api.mjs +144 -0
- package/src/search/lib/backends/openai-oauth.mjs +98 -0
- package/src/search/lib/backends/openai-web-search.mjs +76 -0
- package/src/search/lib/backends/tavily.mjs +55 -0
- package/src/search/lib/backends/xai-api.mjs +113 -0
- package/src/search/lib/cache.mjs +131 -0
- package/src/search/lib/config.mjs +192 -0
- package/src/search/lib/formatter.mjs +115 -0
- package/src/search/lib/provider-usage.mjs +67 -0
- package/src/search/lib/providers.mjs +47 -0
- package/src/search/lib/search-intent.mjs +109 -0
- package/src/search/lib/setup-handler.mjs +261 -0
- package/src/search/lib/state.mjs +201 -0
- package/src/search/lib/web-tools.mjs +1207 -0
- package/src/search/tool-defs.mjs +83 -0
- package/src/setup/defender-exclusion.mjs +183 -0
- package/src/shared/abort-controller.mjs +15 -0
- package/src/shared/atomic-file.mjs +420 -0
- package/src/shared/config.mjs +350 -0
- package/src/shared/daemon-recycle.mjs +108 -0
- package/src/shared/disable-claude-builtins.mjs +88 -0
- package/src/shared/err-text.mjs +12 -0
- package/src/shared/llm/cost.mjs +66 -0
- package/src/shared/llm/http-agent.mjs +123 -0
- package/src/shared/llm/index.mjs +41 -0
- package/src/shared/llm/pid-cleanup.mjs +27 -0
- package/src/shared/llm/usage-log.mjs +47 -0
- package/src/shared/plugin-paths.mjs +58 -0
- package/src/shared/schedules-store.mjs +70 -0
- package/src/shared/seed.mjs +119 -0
- package/src/shared/user-cwd.mjs +213 -0
- package/src/shared/user-data-guard.mjs +238 -0
- package/src/status/aggregator.mjs +584 -0
- package/src/status/server.mjs +413 -0
- package/tools.json +1653 -0
|
@@ -0,0 +1,1364 @@
|
|
|
1
|
+
import { readFileSync, statSync } from 'fs';
|
|
2
|
+
import * as fsPromises from 'fs/promises';
|
|
3
|
+
import { performance } from 'perf_hooks';
|
|
4
|
+
import { markCodeGraphDirtyPaths } from '../code-graph.mjs';
|
|
5
|
+
import {
|
|
6
|
+
diagnoseFoldTierAmbiguity as _diagnoseFoldTierAmbiguity,
|
|
7
|
+
findActualString as _findActualString,
|
|
8
|
+
stripTrailingWhitespacePerLine as _stripTrailingWhitespacePerLine,
|
|
9
|
+
stripTrailingWhitespaceForEdit as _stripTrailingWhitespaceForEdit,
|
|
10
|
+
preserveQuoteTypography as _preserveQuoteTypography,
|
|
11
|
+
formatStageInline as _formatStageInline,
|
|
12
|
+
formatStageNote as _formatStageNote,
|
|
13
|
+
} from '../edit-normalize.mjs';
|
|
14
|
+
import { getAbortSignalForSession } from '../../session/abort-lookup.mjs';
|
|
15
|
+
import { createMutationContentCache, isValidUtf8Buffer as _isValidUtf8Buffer } from '../mutation-content-cache.mjs';
|
|
16
|
+
import {
|
|
17
|
+
normalizeOutputPath,
|
|
18
|
+
resolveAgainstCwd,
|
|
19
|
+
} from './path-utils.mjs';
|
|
20
|
+
import {
|
|
21
|
+
findSimilarFile,
|
|
22
|
+
normalizeErrorMessage,
|
|
23
|
+
} from './path-diagnostics.mjs';
|
|
24
|
+
import { normalizePathAndStripLineCoordinate } from './read-args.mjs';
|
|
25
|
+
import {
|
|
26
|
+
withBuiltinPathLocks,
|
|
27
|
+
withPathLock as _withPathLock,
|
|
28
|
+
} from './path-locks.mjs';
|
|
29
|
+
import { withAdvisoryLocks } from './advisory-lock.mjs';
|
|
30
|
+
import { hashText as _hashText } from './hash-utils.mjs';
|
|
31
|
+
import { statMatchesSnapshot as _statMatchesSnapshot } from './snapshot-helpers.mjs';
|
|
32
|
+
import {
|
|
33
|
+
invalidateBuiltinResultCache,
|
|
34
|
+
getPathMutationGeneration as _getPathMutationGeneration,
|
|
35
|
+
rawContentCacheGet as _rawContentCacheGet,
|
|
36
|
+
seedRawContentCacheAfterWrite as _seedRawContentCacheAfterWrite,
|
|
37
|
+
} from './cache-layers.mjs';
|
|
38
|
+
import {
|
|
39
|
+
getReadSnapshot as _getReadSnapshot,
|
|
40
|
+
isSnapshotStale as _isSnapshotStale,
|
|
41
|
+
readContentIfSnapshotHashMatches as _readContentIfSnapshotHashMatches,
|
|
42
|
+
recordReadSnapshot as _recordReadSnapshot,
|
|
43
|
+
} from './read-snapshot-runtime.mjs';
|
|
44
|
+
import {
|
|
45
|
+
captureStableBaseStatSnapshot as _captureStableBaseStatSnapshot,
|
|
46
|
+
captureExpectedTargetSnapshot as _captureExpectedTargetSnapshot,
|
|
47
|
+
materialiseByteReplacements as _materialiseByteReplacements,
|
|
48
|
+
} from './edit-byte-utils.mjs';
|
|
49
|
+
import {
|
|
50
|
+
nativeEditShouldAttempt as _nativeEditShouldAttempt,
|
|
51
|
+
runNativeExactEdit as _runNativeExactEdit,
|
|
52
|
+
} from './native-edit-runner.mjs';
|
|
53
|
+
import {
|
|
54
|
+
countLiteralOccurrences as _countLiteralOccurrences,
|
|
55
|
+
findCrlfNormalisedMatches as _findCrlfNormalisedMatches,
|
|
56
|
+
findLiteralOccurrenceState as _findLiteralOccurrenceState,
|
|
57
|
+
formatMatchLines as _formatMatchLines,
|
|
58
|
+
occurrenceLinesCrlf as _occurrenceLinesCrlf,
|
|
59
|
+
occurrenceLinesPlain as _occurrenceLinesPlain,
|
|
60
|
+
replacementForOriginalSlice as _replacementForOriginalSlice,
|
|
61
|
+
replaceRangesFromOriginal as _replaceRangesFromOriginal,
|
|
62
|
+
replaceSingleLiteralAt as _replaceSingleLiteralAt,
|
|
63
|
+
validateEditChunkSize as _validateEditChunkSize,
|
|
64
|
+
} from './edit-match-utils.mjs';
|
|
65
|
+
import {
|
|
66
|
+
diagnoseBatchPeers as _diagnoseBatchPeers,
|
|
67
|
+
editNeedleEncodingNote as _editNeedleEncodingNote,
|
|
68
|
+
} from './edit-diagnostics.mjs';
|
|
69
|
+
import {
|
|
70
|
+
countLfInString as _countLfInString,
|
|
71
|
+
maybeAutoStripLineNumberPrefixes as _maybeAutoStripLineNumberPrefixes,
|
|
72
|
+
postEditSnapshotMeta as _postEditSnapshotMeta,
|
|
73
|
+
shiftSnapshotRangesForEdit as _shiftSnapshotRangesForEdit,
|
|
74
|
+
lineRangeForSubstring as _lineRangeForSubstring,
|
|
75
|
+
} from './edit-context-utils.mjs';
|
|
76
|
+
import {
|
|
77
|
+
tryBuildExactEditBuffer as _tryBuildExactEditBufferImpl,
|
|
78
|
+
tryBuildMultiExactEditBuffer as _tryBuildMultiExactEditBufferImpl,
|
|
79
|
+
} from './edit-byte-plan.mjs';
|
|
80
|
+
import { tryWriteSameSizeByteReplacementsSync as _tryWriteSameSizeByteReplacementsSyncImpl } from './edit-partial-write.mjs';
|
|
81
|
+
import {
|
|
82
|
+
buildStaleEditRecovery as _buildStaleEditRecovery,
|
|
83
|
+
editFailureContextHint as _editFailureContextHint,
|
|
84
|
+
primeReadSnapshotForEdit as _primeReadSnapshotForEdit,
|
|
85
|
+
} from './edit-failure-context.mjs';
|
|
86
|
+
import { validatePreparedEditBase as _validatePreparedEditBase } from './edit-base-guard.mjs';
|
|
87
|
+
import {
|
|
88
|
+
commitPreparedEditCheckedUnlocked as _commitPreparedEditCheckedUnlockedImpl,
|
|
89
|
+
commitPreparedEditUnlocked as _commitPreparedEditUnlockedImpl,
|
|
90
|
+
} from './edit-commit.mjs';
|
|
91
|
+
import { atomicWrite } from './atomic-write.mjs';
|
|
92
|
+
import {
|
|
93
|
+
hasUnsafeWin32Component,
|
|
94
|
+
isWindowsDevicePath,
|
|
95
|
+
} from './device-paths.mjs';
|
|
96
|
+
import { assertEditTargetUtf8 as _assertEditTargetUtf8 } from './edit-utf8-guard.mjs';
|
|
97
|
+
import { attemptStaleEditAutoRefresh as _attemptStaleEditAutoRefresh } from './edit-stale-refresh.mjs';
|
|
98
|
+
|
|
99
|
+
function _optionalEditMissDetails(content, oldString) {
|
|
100
|
+
return _editNeedleEncodingNote(content, oldString);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function _foldTierAmbiguityError(content, oldString, filePath, editPrefix = '', peerArgs = null) {
|
|
104
|
+
const amb = _diagnoseFoldTierAmbiguity(content, oldString);
|
|
105
|
+
if (!amb || amb.count <= 1) return null;
|
|
106
|
+
const stageNote = _formatStageInline(amb.stage);
|
|
107
|
+
return `Error [code 9]: ${editPrefix}old_string found ${amb.count} times in ${filePath}${stageNote};${_formatMatchLines(amb.lines, amb.count)} set replace_all:true or provide more unique context${peerArgs ? _diagnoseBatchPeers(...peerArgs) : ''}`;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function _ioTraceEnabled() {
|
|
111
|
+
return /^(1|true|yes|on)$/i.test(String(process.env.MIXDOG_IO_TRACE || ''));
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function _ioTraceStart() {
|
|
115
|
+
return _ioTraceEnabled() ? performance.now() : 0;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function _ioTrace(event, fields = {}) {
|
|
119
|
+
if (!_ioTraceEnabled()) return;
|
|
120
|
+
try {
|
|
121
|
+
process.stderr.write(`[io-trace] ${JSON.stringify({
|
|
122
|
+
event,
|
|
123
|
+
ts: Date.now(),
|
|
124
|
+
...fields,
|
|
125
|
+
})}\n`);
|
|
126
|
+
} catch {}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function _ioTraceDone(event, started, fields = {}) {
|
|
130
|
+
if (!started || !_ioTraceEnabled()) return;
|
|
131
|
+
_ioTrace(event, {
|
|
132
|
+
...fields,
|
|
133
|
+
ms: Number((performance.now() - started).toFixed(3)),
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function _editTraceEnabled() {
|
|
138
|
+
return _ioTraceEnabled() || /^(1|true|yes|on)$/i.test(String(process.env.MIXDOG_EDIT_TRACE || ''));
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function _editTrace(event, fields = {}) {
|
|
142
|
+
if (!_editTraceEnabled()) return;
|
|
143
|
+
try {
|
|
144
|
+
process.stderr.write(`[edit-trace] ${JSON.stringify({
|
|
145
|
+
event,
|
|
146
|
+
ts: Date.now(),
|
|
147
|
+
...fields,
|
|
148
|
+
})}\n`);
|
|
149
|
+
} catch {}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function _editPathTrace(event, filePath, fields = {}) {
|
|
153
|
+
if (!_editTraceEnabled()) return;
|
|
154
|
+
_editTrace(event, {
|
|
155
|
+
path: normalizeOutputPath(filePath),
|
|
156
|
+
...fields,
|
|
157
|
+
});
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function _loadEditTargetBytes(fullPath) {
|
|
161
|
+
try {
|
|
162
|
+
const rawBuf = readFileSync(fullPath);
|
|
163
|
+
if (!Buffer.isBuffer(rawBuf)) return null;
|
|
164
|
+
return { rawBuf, content: rawBuf.toString('utf-8') };
|
|
165
|
+
} catch {
|
|
166
|
+
return null;
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/** Authoritative target bytes while the path lock is held (cold-path TOCTOU guard). */
|
|
171
|
+
function _readEditTargetBytesUnderLock(fullPath, filePath, traceReason = null, mode = 'single') {
|
|
172
|
+
const loaded = _loadEditTargetBytes(fullPath);
|
|
173
|
+
if (!loaded) return null;
|
|
174
|
+
if (traceReason) {
|
|
175
|
+
_editPathTrace('edit_lock_cold_reread', filePath, { mode, reason: traceReason });
|
|
176
|
+
}
|
|
177
|
+
return loaded;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
function _tryStaleSnapshotAutoRefresh({
|
|
181
|
+
fullPath,
|
|
182
|
+
filePath,
|
|
183
|
+
scope,
|
|
184
|
+
stat,
|
|
185
|
+
readRanges,
|
|
186
|
+
oldStrings,
|
|
187
|
+
readCache,
|
|
188
|
+
recordPreviewSnapshot = false,
|
|
189
|
+
}) {
|
|
190
|
+
const refreshed = _attemptStaleEditAutoRefresh({
|
|
191
|
+
fullPath,
|
|
192
|
+
filePath,
|
|
193
|
+
scope,
|
|
194
|
+
stat,
|
|
195
|
+
readRanges,
|
|
196
|
+
oldStrings,
|
|
197
|
+
readCache,
|
|
198
|
+
recordPreviewSnapshot,
|
|
199
|
+
});
|
|
200
|
+
if (!refreshed) return null;
|
|
201
|
+
if (refreshed.ok === false && typeof refreshed.error === 'string') {
|
|
202
|
+
return { error: refreshed.error };
|
|
203
|
+
}
|
|
204
|
+
if (refreshed.ok === true && typeof refreshed.content === 'string' && Buffer.isBuffer(refreshed.rawBuf)) {
|
|
205
|
+
return { content: refreshed.content, rawBuf: refreshed.rawBuf };
|
|
206
|
+
}
|
|
207
|
+
return null;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
function _tryBuildExactEditBuffer(rawBuf, oldStr, newStr, replaceAll, snapshot, filePath) {
|
|
211
|
+
return _tryBuildExactEditBufferImpl(rawBuf, oldStr, newStr, replaceAll, snapshot, filePath);
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
function _tryBuildMultiExactEditBuffer(rawBuf, edits, args, snapshot, filePath) {
|
|
215
|
+
return _tryBuildMultiExactEditBufferImpl(rawBuf, edits, args, snapshot, filePath);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// Edit input normalization helpers (_normalizeForMatch, _nfcFoldMatch,
|
|
219
|
+
// _findActualString, _stripTrailingWhitespacePerLine, _formatStageInline,
|
|
220
|
+
// _formatStageNote) extracted to ./edit-normalize.mjs — see import at top
|
|
221
|
+
// of file. Pipeline: byte-exact → exact → curly-quote fold → nfc-fold →
|
|
222
|
+
// rstrip-fold → indent-fold → eol-fold (edit-normalize; last-resort + code 9)
|
|
223
|
+
// → crlf-fold (engine slow-path).
|
|
224
|
+
|
|
225
|
+
const _partialWriteHooks = {
|
|
226
|
+
ioTraceStart: _ioTraceStart,
|
|
227
|
+
ioTraceDone: _ioTraceDone,
|
|
228
|
+
validatePreparedEditBase: (...args) => _validatePreparedEditBase(...args),
|
|
229
|
+
};
|
|
230
|
+
|
|
231
|
+
function _tryWriteSameSizeByteReplacementsSync(fullPath, replacements, options = {}) {
|
|
232
|
+
return _tryWriteSameSizeByteReplacementsSyncImpl(fullPath, replacements, options, _partialWriteHooks);
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
async function _prepareMultiEdit(args, workDir, readStateScope, _pathOpts, options = {}) {
|
|
236
|
+
args.path = normalizePathAndStripLineCoordinate(args.path, workDir);
|
|
237
|
+
const filePath = args.path;
|
|
238
|
+
const edits = Array.isArray(args.edits) ? args.edits : [];
|
|
239
|
+
if (!filePath) return { ok: false, error: 'Error: path is required' };
|
|
240
|
+
if (edits.length === 0) return { ok: false, error: 'Error: edits array is required' };
|
|
241
|
+
// R12: Win32 component guard — reject trailing dot/space or NTFS ADS
|
|
242
|
+
// suffix (foo.txt:ads) and reserved device names (NUL, CON, …) before
|
|
243
|
+
// resolve so a relative path can't be coerced into a device alias.
|
|
244
|
+
if (typeof isWindowsDevicePath === 'function' && isWindowsDevicePath(filePath)) {
|
|
245
|
+
return { ok: false, error: `Error: cannot edit Windows device path (reserved name or raw-device namespace): ${normalizeOutputPath(filePath)}` };
|
|
246
|
+
}
|
|
247
|
+
if (typeof hasUnsafeWin32Component === 'function' && hasUnsafeWin32Component(filePath)) {
|
|
248
|
+
return { ok: false, error: `Error: cannot edit Windows path with trailing dot/space or NTFS ADS suffix (bypasses device guard): ${normalizeOutputPath(filePath)}` };
|
|
249
|
+
}
|
|
250
|
+
const fullPath = resolveAgainstCwd(filePath, workDir);
|
|
251
|
+
// R1: short-circuit UNC/SMB paths before ANY stat/read on the edit
|
|
252
|
+
// target to prevent NTLM credential leakage via implicit network
|
|
253
|
+
// auth. Mirrors CC FileEditTool.ts:176.
|
|
254
|
+
if (fullPath.startsWith('\\\\') || fullPath.startsWith('//')) {
|
|
255
|
+
return { ok: false, error: `Error: UNC/SMB paths are not supported (R1: NTLM-leak prevention): ${filePath}` };
|
|
256
|
+
}
|
|
257
|
+
if (typeof isWindowsDevicePath === 'function' && isWindowsDevicePath(fullPath)) {
|
|
258
|
+
return { ok: false, error: `Error: cannot edit Windows device path (reserved name or raw-device namespace): ${normalizeOutputPath(filePath)}` };
|
|
259
|
+
}
|
|
260
|
+
if (typeof hasUnsafeWin32Component === 'function' && hasUnsafeWin32Component(fullPath)) {
|
|
261
|
+
return { ok: false, error: `Error: cannot edit Windows path with trailing dot/space or NTFS ADS suffix (bypasses device guard): ${normalizeOutputPath(filePath)}` };
|
|
262
|
+
}
|
|
263
|
+
let mEditStat;
|
|
264
|
+
try { mEditStat = statSync(fullPath); }
|
|
265
|
+
catch (err) {
|
|
266
|
+
if (err && err.code === 'ENOENT') {
|
|
267
|
+
const similar = findSimilarFile(fullPath);
|
|
268
|
+
const hint = similar ? ` Did you mean "${normalizeOutputPath(similar)}"?` : '';
|
|
269
|
+
return { ok: false, error: `Error [code 4]: file not found: ${filePath}${hint}` };
|
|
270
|
+
}
|
|
271
|
+
return { ok: false, error: `Error: ${normalizeErrorMessage(err instanceof Error ? err.message : String(err))}` };
|
|
272
|
+
}
|
|
273
|
+
if (mEditStat.size > 1073741824) {
|
|
274
|
+
return { ok: false, error: `Error: edit refused: file too large (size: ${mEditStat.size}B, cap: 1GiB)` };
|
|
275
|
+
}
|
|
276
|
+
let mEditPreloadedContent = null;
|
|
277
|
+
let mEditPreloadedRawBuf = null;
|
|
278
|
+
let mEditSnapshot = _getReadSnapshot(fullPath, readStateScope);
|
|
279
|
+
if (!mEditSnapshot) {
|
|
280
|
+
const _mPrimed = _primeReadSnapshotForEdit({
|
|
281
|
+
fullPath,
|
|
282
|
+
filePath,
|
|
283
|
+
st: mEditStat,
|
|
284
|
+
scope: readStateScope,
|
|
285
|
+
oldStrings: [],
|
|
286
|
+
});
|
|
287
|
+
mEditSnapshot = _getReadSnapshot(fullPath, readStateScope);
|
|
288
|
+
if (_mPrimed) {
|
|
289
|
+
_editPathTrace('edit_auto_snapshot', filePath, { mode: 'multi' });
|
|
290
|
+
if (typeof _mPrimed.content === 'string' && Buffer.isBuffer(_mPrimed.rawBuf)) {
|
|
291
|
+
mEditPreloadedContent = _mPrimed.content;
|
|
292
|
+
mEditPreloadedRawBuf = _mPrimed.rawBuf;
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
const mEditSnapshotReadCache = createMutationContentCache();
|
|
297
|
+
if (mEditSnapshot && _isSnapshotStale(mEditStat, mEditSnapshot, fullPath, mEditSnapshotReadCache)) {
|
|
298
|
+
mEditPreloadedContent = _readContentIfSnapshotHashMatches(fullPath, mEditSnapshot, mEditSnapshotReadCache, mEditStat);
|
|
299
|
+
if (mEditPreloadedContent !== null) {
|
|
300
|
+
const cached = mEditSnapshotReadCache.getEntry(fullPath);
|
|
301
|
+
if (Buffer.isBuffer(cached?.rawBuf)) mEditPreloadedRawBuf = cached.rawBuf;
|
|
302
|
+
}
|
|
303
|
+
if (mEditPreloadedContent === null) {
|
|
304
|
+
const _staleRefresh = _tryStaleSnapshotAutoRefresh({
|
|
305
|
+
fullPath,
|
|
306
|
+
filePath,
|
|
307
|
+
scope: readStateScope,
|
|
308
|
+
stat: mEditStat,
|
|
309
|
+
readRanges: mEditSnapshot?.ranges,
|
|
310
|
+
oldStrings: edits,
|
|
311
|
+
readCache: mEditSnapshotReadCache,
|
|
312
|
+
recordPreviewSnapshot: false,
|
|
313
|
+
});
|
|
314
|
+
if (_staleRefresh?.error) return { ok: false, error: _staleRefresh.error };
|
|
315
|
+
if (_staleRefresh?.content) {
|
|
316
|
+
mEditPreloadedContent = _staleRefresh.content;
|
|
317
|
+
mEditPreloadedRawBuf = _staleRefresh.rawBuf;
|
|
318
|
+
mEditSnapshot = _getReadSnapshot(fullPath, readStateScope);
|
|
319
|
+
} else {
|
|
320
|
+
const recovery = _buildStaleEditRecovery({
|
|
321
|
+
fullPath,
|
|
322
|
+
scope: readStateScope,
|
|
323
|
+
oldStrings: edits,
|
|
324
|
+
recordPreviewSnapshot: false,
|
|
325
|
+
});
|
|
326
|
+
return { ok: false, error: `Error [code 7]: file modified since read (lint / formatter / external write) — read it again before editing: ${filePath}${recovery}` };
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
} else {
|
|
330
|
+
const cached = mEditSnapshotReadCache.getEntry(fullPath);
|
|
331
|
+
if (typeof cached?.content === 'string') {
|
|
332
|
+
mEditPreloadedContent = cached.content;
|
|
333
|
+
if (Buffer.isBuffer(cached.rawBuf)) mEditPreloadedRawBuf = cached.rawBuf;
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
try {
|
|
337
|
+
try {
|
|
338
|
+
mEditStat = statSync(fullPath);
|
|
339
|
+
} catch (err) {
|
|
340
|
+
if (err && err.code === 'ENOENT') {
|
|
341
|
+
const similar = findSimilarFile(fullPath);
|
|
342
|
+
const hint = similar ? ` Did you mean "${normalizeOutputPath(similar)}"?` : '';
|
|
343
|
+
return { ok: false, error: `Error [code 4]: file not found: ${filePath}${hint}` };
|
|
344
|
+
}
|
|
345
|
+
return { ok: false, error: `Error: ${normalizeErrorMessage(err instanceof Error ? err.message : String(err))}` };
|
|
346
|
+
}
|
|
347
|
+
if (!mEditSnapshot) {
|
|
348
|
+
const _cold = _readEditTargetBytesUnderLock(fullPath, filePath, 'no_snapshot', 'multi');
|
|
349
|
+
if (!_cold) {
|
|
350
|
+
return { ok: false, error: `Error: failed to read edit target: ${filePath}` };
|
|
351
|
+
}
|
|
352
|
+
mEditPreloadedContent = _cold.content;
|
|
353
|
+
mEditPreloadedRawBuf = _cold.rawBuf;
|
|
354
|
+
} else if (mEditPreloadedRawBuf !== null && mEditSnapshot
|
|
355
|
+
&& typeof mEditSnapshot.contentHash === 'string') {
|
|
356
|
+
const _primed = _loadEditTargetBytes(fullPath);
|
|
357
|
+
if (_primed) {
|
|
358
|
+
if (_hashText(_primed.rawBuf) !== _hashText(mEditPreloadedRawBuf)) {
|
|
359
|
+
_editPathTrace('edit_lock_cold_reread', filePath, {
|
|
360
|
+
mode: 'multi',
|
|
361
|
+
reason: 'auto_snapshot_content_drift',
|
|
362
|
+
});
|
|
363
|
+
}
|
|
364
|
+
if (_hashText(_primed.rawBuf) === mEditSnapshot.contentHash) {
|
|
365
|
+
mEditPreloadedContent = _primed.content;
|
|
366
|
+
mEditPreloadedRawBuf = _primed.rawBuf;
|
|
367
|
+
} else {
|
|
368
|
+
mEditPreloadedContent = null;
|
|
369
|
+
mEditPreloadedRawBuf = null;
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
let rawContent = mEditPreloadedRawBuf;
|
|
374
|
+
try {
|
|
375
|
+
if (rawContent === null) {
|
|
376
|
+
const cachedRaw = _rawContentCacheGet(fullPath, mEditStat);
|
|
377
|
+
rawContent = cachedRaw
|
|
378
|
+
|| (mEditPreloadedContent === null
|
|
379
|
+
? readFileSync(fullPath)
|
|
380
|
+
: Buffer.from(mEditPreloadedContent, 'utf-8'));
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
catch (err) { return { ok: false, error: `Error: ${normalizeErrorMessage(err instanceof Error ? err.message : String(err))}` }; }
|
|
384
|
+
let baseStatSnapshot = null;
|
|
385
|
+
try {
|
|
386
|
+
const postReadStat = statSync(fullPath);
|
|
387
|
+
if (_statMatchesSnapshot(postReadStat, mEditStat) && postReadStat.size === rawContent.length) {
|
|
388
|
+
baseStatSnapshot = {
|
|
389
|
+
mtimeMs: postReadStat.mtimeMs,
|
|
390
|
+
ctimeMs: postReadStat.ctimeMs,
|
|
391
|
+
size: postReadStat.size,
|
|
392
|
+
ino: Number(postReadStat.ino),
|
|
393
|
+
};
|
|
394
|
+
}
|
|
395
|
+
} catch { /* fall back to hash compare in _validatePreparedEditBase */ }
|
|
396
|
+
const baseContentHash = _hashText(rawContent);
|
|
397
|
+
const expectedTargetSnapshot = _captureExpectedTargetSnapshot(fullPath, mEditStat);
|
|
398
|
+
// Reviewer issue #3: validate encoding before the multi-edit
|
|
399
|
+
// byte-exact buffer build too. Previously this branch returned a
|
|
400
|
+
// prepared edit (and reached commit) before reaching the
|
|
401
|
+
// legacy `_isValidUtf8Buffer` check further below, so a Shift-JIS
|
|
402
|
+
// / Latin-1 / binary file could be mutated on the multi byte-exact
|
|
403
|
+
// path.
|
|
404
|
+
{
|
|
405
|
+
const _utf8Err = _assertEditTargetUtf8(rawContent, filePath);
|
|
406
|
+
if (_utf8Err) return { ok: false, error: _utf8Err };
|
|
407
|
+
}
|
|
408
|
+
const exactBufferEdit = _tryBuildMultiExactEditBuffer(rawContent, edits, args, mEditSnapshot, filePath);
|
|
409
|
+
if (exactBufferEdit?.error) return { ok: false, error: exactBufferEdit.error };
|
|
410
|
+
if (Buffer.isBuffer(exactBufferEdit?.updated) || (exactBufferEdit?.sameSize && Array.isArray(exactBufferEdit.replacements))) {
|
|
411
|
+
return {
|
|
412
|
+
ok: true,
|
|
413
|
+
filePath,
|
|
414
|
+
fullPath,
|
|
415
|
+
edits,
|
|
416
|
+
snapshot: mEditSnapshot,
|
|
417
|
+
content: exactBufferEdit.updated || null,
|
|
418
|
+
baseRawContent: rawContent,
|
|
419
|
+
sameSizeByteReplacements: exactBufferEdit.sameSize ? exactBufferEdit.replacements : null,
|
|
420
|
+
contentHash: exactBufferEdit.contentHash || (Buffer.isBuffer(exactBufferEdit.updated) ? _hashText(exactBufferEdit.updated) : null),
|
|
421
|
+
baseContentHash,
|
|
422
|
+
baseMutationGeneration: _getPathMutationGeneration(fullPath),
|
|
423
|
+
baseStatSnapshot,
|
|
424
|
+
expectedTargetSnapshot,
|
|
425
|
+
baseMode: mEditStat.mode & 0o777,
|
|
426
|
+
stageCounts: {},
|
|
427
|
+
};
|
|
428
|
+
}
|
|
429
|
+
if (!_isValidUtf8Buffer(rawContent)) {
|
|
430
|
+
const _bomNote = (rawContent.length >= 2 && rawContent[0] === 0xFF && rawContent[1] === 0xFE)
|
|
431
|
+
? 'file is UTF-16LE (BOM FF FE) — edit only supports UTF-8; use write (preserves UTF-16) or convert the file'
|
|
432
|
+
: (rawContent.length >= 2 && rawContent[0] === 0xFE && rawContent[1] === 0xFF)
|
|
433
|
+
? 'file is UTF-16BE (BOM FE FF) — edit only supports UTF-8; convert the file first'
|
|
434
|
+
: 'file appears to be non-UTF-8 (Shift-JIS/Latin-1/binary mix)';
|
|
435
|
+
return { ok: false, error: `Error: ${_bomNote}. Edit aborted to prevent silent corruption. Path: ${filePath}` };
|
|
436
|
+
}
|
|
437
|
+
let content = mEditPreloadedContent;
|
|
438
|
+
if (content === null) content = rawContent.toString('utf-8');
|
|
439
|
+
// CC parity: when new_string is empty (pure deletion) and old_string
|
|
440
|
+
// is followed by a newline in the file, swallow that newline as part
|
|
441
|
+
// of the match so the deletion doesn't leave an empty line behind.
|
|
442
|
+
// Mirrors FileEditTool/utils.ts applyEditToFile's stripTrailingNewline
|
|
443
|
+
// branch. CRLF fallback path keeps current behaviour to avoid range
|
|
444
|
+
// arithmetic complications.
|
|
445
|
+
const _absorbTrailingNewline = (cur, oldStr, newStr, replaceAll) => {
|
|
446
|
+
if (newStr !== '' || oldStr.endsWith('\n')) return oldStr;
|
|
447
|
+
// For replace_all pure deletion, do NOT globally rewrite oldStr —
|
|
448
|
+
// mixed-suffix occurrences (some followed by \n, some bare / at
|
|
449
|
+
// EOF) need per-occurrence absorption handled by the replace_all
|
|
450
|
+
// branch below; a single global eOldStr would skip bare sites.
|
|
451
|
+
if (replaceAll) return oldStr;
|
|
452
|
+
if (cur.includes(oldStr + '\r\n')) return oldStr + '\r\n';
|
|
453
|
+
return cur.includes(oldStr + '\n') ? oldStr + '\n' : oldStr;
|
|
454
|
+
};
|
|
455
|
+
// Sequential apply tracks an accumulated line delta so partial-
|
|
456
|
+
// coverage windows shift with the bytes. Each edit's delta is
|
|
457
|
+
// (newline count in new_string − newline count in old_string),
|
|
458
|
+
// multiplied by the number of occurrences actually replaced.
|
|
459
|
+
let _rollingSnapshot = mEditSnapshot;
|
|
460
|
+
const _bumpRollingSnapshot = (beforeContent, needle, lineDelta, replaceAll) => {
|
|
461
|
+
if (!Number.isFinite(lineDelta) || lineDelta === 0 || typeof needle !== 'string') return;
|
|
462
|
+
const span = _lineRangeForSubstring(beforeContent, needle, { replaceAll: replaceAll === true });
|
|
463
|
+
if (!span) return;
|
|
464
|
+
_rollingSnapshot = _shiftSnapshotRangesForEdit(_rollingSnapshot, {
|
|
465
|
+
editStartLine: span.startLine,
|
|
466
|
+
editEndLine: span.endLine,
|
|
467
|
+
lineDelta,
|
|
468
|
+
});
|
|
469
|
+
};
|
|
470
|
+
// Stage stats surface fold / nfc-fold / crlf-fold counts up to
|
|
471
|
+
// the caller so the response can flag non-exact matches without
|
|
472
|
+
// touching the per-edit return shape.
|
|
473
|
+
const _stageCounts = {};
|
|
474
|
+
// Hoisted markdown predicate — same trailing-whitespace policy as
|
|
475
|
+
// the single-edit case, computed once per call.
|
|
476
|
+
const _IS_MD_PATH = /\.(?:md|mdx)$/i.test(filePath);
|
|
477
|
+
// Independence invariant (matches fast-path semantics in
|
|
478
|
+
// tryBuildMultiExactEditBuffer): reject batches where one edit's
|
|
479
|
+
// new_string contains another edit's old_string. The slow path
|
|
480
|
+
// applies sequentially against a mutating buffer, so a later
|
|
481
|
+
// edit's old_string could match bytes that an earlier edit's
|
|
482
|
+
// new_string just synthesised (or, conversely, an earlier edit
|
|
483
|
+
// could destroy a later edit's anchor). Surfacing the invariant
|
|
484
|
+
// up-front makes batches deterministic and matches the fast-path
|
|
485
|
+
// contract that callers already see.
|
|
486
|
+
if (edits.length > 1) {
|
|
487
|
+
for (let a = 0; a < edits.length; a++) {
|
|
488
|
+
const ea = edits[a];
|
|
489
|
+
if (!ea || typeof ea.old_string !== 'string' || typeof ea.new_string !== 'string') continue;
|
|
490
|
+
for (let b = 0; b < edits.length; b++) {
|
|
491
|
+
if (a === b) continue;
|
|
492
|
+
const eb = edits[b];
|
|
493
|
+
if (!eb || typeof eb.old_string !== 'string') continue;
|
|
494
|
+
if (eb.old_string.length === 0) continue;
|
|
495
|
+
if (ea.new_string.indexOf(eb.old_string) !== -1) {
|
|
496
|
+
return { ok: false, error: `Error [code 12]: edits are not independent — edit ${a}'s new_string contains edit ${b}'s old_string in ${filePath}; split into separate edit() calls or reorder so no later edit matches bytes produced by an earlier edit.` };
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
for (let i = 0; i < edits.length; i++) {
|
|
502
|
+
const _contentBeforeEdit = content;
|
|
503
|
+
const entry = edits[i];
|
|
504
|
+
if (!entry || typeof entry.old_string !== 'string' || typeof entry.new_string !== 'string') {
|
|
505
|
+
return { ok: false, error: `Error: edit ${i} must have old_string and new_string` };
|
|
506
|
+
}
|
|
507
|
+
{
|
|
508
|
+
const _nulIdx = entry.new_string.indexOf('\u0000');
|
|
509
|
+
if (_nulIdx !== -1) {
|
|
510
|
+
return { ok: false, error: `Error [code 11]: edit ${i} — new_string contains NUL byte (U+0000) at offset ${_nulIdx} — source text must not contain NUL: ${filePath}` };
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
let { old_string: _origOld, new_string, replace_all } = entry;
|
|
514
|
+
// Same line-prefix recovery as the single-edit case (see edit
|
|
515
|
+
// handler). Sequential apply still validates each item, so a
|
|
516
|
+
// bad strip simply fails further down with code 8 instead of
|
|
517
|
+
// here.
|
|
518
|
+
if (typeof _origOld === 'string' && /^\s*\d+[\t│→]/.test(_origOld)) {
|
|
519
|
+
const _stripped = _maybeAutoStripLineNumberPrefixes(_origOld);
|
|
520
|
+
if (_stripped !== null) {
|
|
521
|
+
_editPathTrace('edit_auto_strip_line_numbers', filePath, { mode: 'multi', index: i });
|
|
522
|
+
_origOld = _stripped;
|
|
523
|
+
} else {
|
|
524
|
+
return { ok: false, error: `Error: edit ${i} — old_string mixes Read line-number-prefixed lines ("<n>│…") with raw lines — strip the prefix from every line (or none) before Edit: ${filePath}` };
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
// Same trailing whitespace normalization as the single-edit
|
|
528
|
+
// case. Markdown (`.md` / `.mdx`) skips the strip because two
|
|
529
|
+
// trailing spaces is the hard-line-break syntax. `_IS_MD_PATH`
|
|
530
|
+
// is hoisted above the loop so per-edit cost is one regex hit.
|
|
531
|
+
if (!_IS_MD_PATH) {
|
|
532
|
+
const _strippedNew = _stripTrailingWhitespaceForEdit(new_string, _origOld);
|
|
533
|
+
if (_strippedNew !== new_string) {
|
|
534
|
+
_editPathTrace('edit_trim_trailing_ws', filePath, { mode: 'multi', index: i });
|
|
535
|
+
new_string = _strippedNew;
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
if (_origOld.length === 0) {
|
|
539
|
+
return { ok: false, error: `Error: edit ${i} — old_string must be non-empty` };
|
|
540
|
+
}
|
|
541
|
+
if (new_string === _origOld) {
|
|
542
|
+
return { ok: false, error: `Error: edit ${i} — new_string must differ from old_string` };
|
|
543
|
+
}
|
|
544
|
+
const _matchInfo = {};
|
|
545
|
+
const _origLiteralOccurrence = _findLiteralOccurrenceState(content, _origOld);
|
|
546
|
+
// Move size gate to the FOLD-FALLBACK path: exact-unique
|
|
547
|
+
// byte matches are safe at any size, so the >=30-line
|
|
548
|
+
// code-10 wording only fires when we are about to leave
|
|
549
|
+
// exact and try fold/fuzzy. A unique exact hit
|
|
550
|
+
// (count === 1, or replace_all) bypasses the gate; an
|
|
551
|
+
// ambiguous exact hit (count > 1 without replace_all)
|
|
552
|
+
// surfaces via the standard code-9 path below.
|
|
553
|
+
if (_origLiteralOccurrence.count === 0) {
|
|
554
|
+
const _sizeErr = _validateEditChunkSize(_origOld, replace_all === true, false);
|
|
555
|
+
if (_sizeErr) return { ok: false, error: _sizeErr.replace('Error [code 10]:', `Error [code 10]: edit ${i} —`) };
|
|
556
|
+
}
|
|
557
|
+
const _matchedOld = _origLiteralOccurrence.count > 0
|
|
558
|
+
? _origOld
|
|
559
|
+
: (_findActualString(content, _origOld, _matchInfo) || _origOld);
|
|
560
|
+
if (_origLiteralOccurrence.count > 0) _matchInfo.stage = 'exact';
|
|
561
|
+
if (_matchInfo.stage && _matchInfo.stage !== 'exact') {
|
|
562
|
+
_stageCounts[_matchInfo.stage] = (_stageCounts[_matchInfo.stage] || 0) + 1;
|
|
563
|
+
}
|
|
564
|
+
// CC parity: typography preservation (see edit-normalize.mjs).
|
|
565
|
+
const _newAfterTypo = _preserveQuoteTypography(_origOld, _matchedOld, new_string);
|
|
566
|
+
if (_newAfterTypo !== new_string) {
|
|
567
|
+
_editPathTrace('edit_typography_preserve', filePath, { mode: 'multi', index: i });
|
|
568
|
+
new_string = _newAfterTypo;
|
|
569
|
+
}
|
|
570
|
+
const old_string = _absorbTrailingNewline(content, _matchedOld, new_string, replace_all === true);
|
|
571
|
+
const _shiftedSnapshot = _rollingSnapshot;
|
|
572
|
+
const _oldNL = (old_string.match(/\n/g) || []).length;
|
|
573
|
+
const _newNL = (new_string.match(/\n/g) || []).length;
|
|
574
|
+
const _perOccurrenceDelta = _newNL - _oldNL;
|
|
575
|
+
const _oldStringLiteralOccurrence = (old_string === _origOld && _origLiteralOccurrence.count > 0)
|
|
576
|
+
? _origLiteralOccurrence
|
|
577
|
+
: _findLiteralOccurrenceState(content, old_string);
|
|
578
|
+
if (replace_all === true) {
|
|
579
|
+
let _occurrences = 0;
|
|
580
|
+
if (_oldStringLiteralOccurrence.count > 0) {
|
|
581
|
+
let _occIdx = 0;
|
|
582
|
+
while ((_occIdx = content.indexOf(old_string, _occIdx)) !== -1) {
|
|
583
|
+
_occurrences++;
|
|
584
|
+
_occIdx += old_string.length;
|
|
585
|
+
}
|
|
586
|
+
}
|
|
587
|
+
if (_occurrences > 0) {
|
|
588
|
+
const _indentFixedNewAll = new_string;
|
|
589
|
+
// Pure-deletion (new_string === '' with no trailing line
|
|
590
|
+
// terminator on old): absorb a trailing \r\n / \n / \r
|
|
591
|
+
// PER OCCURRENCE. Mixed-suffix sites (some followed by
|
|
592
|
+
// a newline, some bare / at EOF) must all be removed;
|
|
593
|
+
// a single global eOldStr rewrite would skip every bare
|
|
594
|
+
// occurrence.
|
|
595
|
+
if (new_string === '' && !old_string.endsWith('\n') && !old_string.endsWith('\r')) {
|
|
596
|
+
const _ranges = [];
|
|
597
|
+
let _absorbedNewlines = 0;
|
|
598
|
+
let _scan = 0;
|
|
599
|
+
while ((_scan = content.indexOf(old_string, _scan)) !== -1) {
|
|
600
|
+
let _end = _scan + old_string.length;
|
|
601
|
+
if (content[_end] === '\r' && content[_end + 1] === '\n') { _end += 2; _absorbedNewlines += 1; }
|
|
602
|
+
else if (content[_end] === '\n') { _end += 1; _absorbedNewlines += 1; }
|
|
603
|
+
else if (content[_end] === '\r') { _end += 1; _absorbedNewlines += 1; }
|
|
604
|
+
_ranges.push({ start: _scan, end: _end });
|
|
605
|
+
_scan = _scan + old_string.length;
|
|
606
|
+
}
|
|
607
|
+
content = _replaceRangesFromOriginal(content, _ranges, '');
|
|
608
|
+
_bumpRollingSnapshot(_contentBeforeEdit, old_string, _perOccurrenceDelta * _ranges.length - _absorbedNewlines, replace_all);
|
|
609
|
+
continue;
|
|
610
|
+
}
|
|
611
|
+
content = content.split(old_string).join(_replacementForOriginalSlice(_indentFixedNewAll, old_string, content));
|
|
612
|
+
_bumpRollingSnapshot(_contentBeforeEdit, old_string, _perOccurrenceDelta * _occurrences, replace_all);
|
|
613
|
+
continue;
|
|
614
|
+
}
|
|
615
|
+
const crlfMatch = _findCrlfNormalisedMatches(content, old_string);
|
|
616
|
+
if (!crlfMatch || crlfMatch.ranges.length === 0) {
|
|
617
|
+
const _foldAmb = _foldTierAmbiguityError(content, old_string, filePath, `edit ${i} — `, [content, edits, i, _findCrlfNormalisedMatches]);
|
|
618
|
+
if (_foldAmb) return { ok: false, error: _foldAmb };
|
|
619
|
+
// Promote `not found` to code 7 (snapshot mismatch) when this
|
|
620
|
+
// session has already mutated the file: the old_string almost
|
|
621
|
+
// certainly targets pre-mutation bytes, and the caller should
|
|
622
|
+
// re-read before retrying instead of debugging fold tiers.
|
|
623
|
+
// Use fullPath — the generation cache is keyed by canonical
|
|
624
|
+
// resolved path (see baseMutationGeneration at function tail),
|
|
625
|
+
// not the raw filePath which can be relative.
|
|
626
|
+
// code 7 promotion removed: `_gen > 0` alone mis-classified a
|
|
627
|
+
// simply-wrong old_string (absent from both pre-mutation and
|
|
628
|
+
// current bytes) as a stale snapshot. Stay an honest code 8 —
|
|
629
|
+
// the code 8 hint below already covers the real pre-mutation case.
|
|
630
|
+
return { ok: false, error: `Error [code 8]: edit ${i} — old_string not found in ${filePath} (no exact/fold/nfc-fold/crlf-fold match).${_optionalEditMissDetails(content, old_string, _shiftedSnapshot, { path: filePath, newString: new_string, replaceAll: replace_all === true, editIndex: i }, [content, edits, i, _findCrlfNormalisedMatches])}` };
|
|
631
|
+
}
|
|
632
|
+
const _crlfOccurrences = crlfMatch.ranges.length;
|
|
633
|
+
content = _replaceRangesFromOriginal(content, crlfMatch.ranges, new_string);
|
|
634
|
+
_bumpRollingSnapshot(_contentBeforeEdit, crlfMatch.normalisedOld, _perOccurrenceDelta * _crlfOccurrences, replace_all);
|
|
635
|
+
_stageCounts['crlf-fold'] = (_stageCounts['crlf-fold'] || 0) + 1;
|
|
636
|
+
} else {
|
|
637
|
+
const occurrence = _oldStringLiteralOccurrence;
|
|
638
|
+
if (occurrence.count > 1) {
|
|
639
|
+
const count = _countLiteralOccurrences(content, old_string);
|
|
640
|
+
return { ok: false, error: `Error [code 9]: edit ${i} — old_string found ${count} times in ${filePath}${_formatStageInline(_matchInfo.stage)};${_formatMatchLines(_occurrenceLinesPlain(content, old_string), count)} set replace_all:true or provide more unique context${_diagnoseBatchPeers(content, edits, i, _findCrlfNormalisedMatches)}` };
|
|
641
|
+
}
|
|
642
|
+
if (occurrence.count === 1) {
|
|
643
|
+
const _indentFixedNewOne = new_string;
|
|
644
|
+
content = _replaceSingleLiteralAt(content, occurrence.index, old_string, _replacementForOriginalSlice(_indentFixedNewOne, old_string, content));
|
|
645
|
+
_bumpRollingSnapshot(_contentBeforeEdit, old_string, _perOccurrenceDelta, replace_all);
|
|
646
|
+
continue;
|
|
647
|
+
}
|
|
648
|
+
const crlfMatch = _findCrlfNormalisedMatches(content, old_string);
|
|
649
|
+
const crlfCount = crlfMatch ? crlfMatch.ranges.length : 0;
|
|
650
|
+
if (crlfCount === 0) {
|
|
651
|
+
const _foldAmb = _foldTierAmbiguityError(content, old_string, filePath, `edit ${i} — `, [content, edits, i, _findCrlfNormalisedMatches]);
|
|
652
|
+
if (_foldAmb) return { ok: false, error: _foldAmb };
|
|
653
|
+
// Snapshot-mismatch promotion (see replace_all branch above):
|
|
654
|
+
// a mutated file with a missing old_string is almost always
|
|
655
|
+
// a stale snapshot, not a typo. Use fullPath for the cache key.
|
|
656
|
+
// code 7 promotion removed: `_gen > 0` alone mis-classified a
|
|
657
|
+
// simply-wrong old_string (absent from both pre-mutation and
|
|
658
|
+
// current bytes) as a stale snapshot. Stay an honest code 8 —
|
|
659
|
+
// the code 8 hint below already covers the real pre-mutation case.
|
|
660
|
+
return { ok: false, error: `Error [code 8]: edit ${i} — old_string not found in ${filePath} (no exact/fold/nfc-fold/crlf-fold match).${_optionalEditMissDetails(content, old_string, _shiftedSnapshot, { path: filePath, newString: new_string, replaceAll: replace_all === true, editIndex: i }, [content, edits, i, _findCrlfNormalisedMatches])}` };
|
|
661
|
+
}
|
|
662
|
+
if (crlfCount > 1) return { ok: false, error: `Error [code 9]: edit ${i} — old_string found ${crlfCount} times in ${filePath} (via crlf-fold);${_formatMatchLines(_occurrenceLinesCrlf(content, crlfMatch.ranges), crlfCount)} set replace_all:true or provide more unique context${_diagnoseBatchPeers(content, edits, i, _findCrlfNormalisedMatches)}` };
|
|
663
|
+
content = _replaceRangesFromOriginal(content, crlfMatch.ranges, new_string);
|
|
664
|
+
_bumpRollingSnapshot(_contentBeforeEdit, crlfMatch.normalisedOld, _perOccurrenceDelta, replace_all);
|
|
665
|
+
_stageCounts['crlf-fold'] = (_stageCounts['crlf-fold'] || 0) + 1;
|
|
666
|
+
}
|
|
667
|
+
}
|
|
668
|
+
return { ok: true, filePath, fullPath, edits, snapshot: _rollingSnapshot, content, baseRawContent: rawContent, baseContentHash, baseMutationGeneration: _getPathMutationGeneration(fullPath), baseStatSnapshot, expectedTargetSnapshot, baseMode: mEditStat.mode & 0o777, stageCounts: _stageCounts };
|
|
669
|
+
} catch (err) {
|
|
670
|
+
return { ok: false, error: `Error: ${normalizeErrorMessage(err instanceof Error ? err.message : String(err))}` };
|
|
671
|
+
}
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
const _editCommitHooks = {
|
|
675
|
+
ioTraceStart: _ioTraceStart,
|
|
676
|
+
ioTraceDone: _ioTraceDone,
|
|
677
|
+
};
|
|
678
|
+
|
|
679
|
+
function _commitPreparedEditUnlocked(prepared, readStateScope, options = {}) {
|
|
680
|
+
return _commitPreparedEditUnlockedImpl(prepared, readStateScope, options, _editCommitHooks);
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
function _commitPreparedEditCheckedUnlocked(prepared, readStateScope, options = {}) {
|
|
684
|
+
return _commitPreparedEditCheckedUnlockedImpl(prepared, readStateScope, options, _editCommitHooks);
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
export async function runMultiEdit(args, workDir, readStateScope, _pathOpts, options = {}) {
|
|
688
|
+
const filePath = normalizePathAndStripLineCoordinate(args?.path, workDir);
|
|
689
|
+
if (!filePath) return 'Error: path is required';
|
|
690
|
+
// R12: Win32 component guard — reject trailing dot/space or NTFS ADS
|
|
691
|
+
// suffix (foo.txt:ads) and reserved device names before resolve so a
|
|
692
|
+
// relative path can't be coerced into a device alias.
|
|
693
|
+
if (typeof isWindowsDevicePath === 'function' && isWindowsDevicePath(filePath)) {
|
|
694
|
+
return `Error: cannot edit Windows device path (reserved name or raw-device namespace): ${normalizeOutputPath(filePath)}`;
|
|
695
|
+
}
|
|
696
|
+
if (typeof hasUnsafeWin32Component === 'function' && hasUnsafeWin32Component(filePath)) {
|
|
697
|
+
return `Error: cannot edit Windows path with trailing dot/space or NTFS ADS suffix (bypasses device guard): ${normalizeOutputPath(filePath)}`;
|
|
698
|
+
}
|
|
699
|
+
const fullPath = resolveAgainstCwd(filePath, workDir);
|
|
700
|
+
if (typeof isWindowsDevicePath === 'function' && isWindowsDevicePath(fullPath)) {
|
|
701
|
+
return `Error: cannot edit Windows device path (reserved name or raw-device namespace): ${normalizeOutputPath(filePath)}`;
|
|
702
|
+
}
|
|
703
|
+
if (typeof hasUnsafeWin32Component === 'function' && hasUnsafeWin32Component(fullPath)) {
|
|
704
|
+
return `Error: cannot edit Windows path with trailing dot/space or NTFS ADS suffix (bypasses device guard): ${normalizeOutputPath(filePath)}`;
|
|
705
|
+
}
|
|
706
|
+
return withBuiltinPathLocks([fullPath], () => withAdvisoryLocks([fullPath], async () => {
|
|
707
|
+
const prepared = await _prepareMultiEdit({ ...args, path: filePath }, workDir, readStateScope, _pathOpts, options);
|
|
708
|
+
if (!prepared.ok) return prepared.error;
|
|
709
|
+
try {
|
|
710
|
+
const commit = await _commitPreparedEditCheckedUnlocked(prepared, readStateScope, options);
|
|
711
|
+
if (!commit.ok) return commit.error;
|
|
712
|
+
} catch (err) {
|
|
713
|
+
return `Error: ${normalizeErrorMessage(err instanceof Error ? err.message : String(err))}`;
|
|
714
|
+
}
|
|
715
|
+
return `Edited: ${normalizeOutputPath(prepared.filePath)} (${prepared.edits.length} replacements applied)${_formatStageNote(prepared.stageCounts)}`;
|
|
716
|
+
}));
|
|
717
|
+
}
|
|
718
|
+
|
|
719
|
+
export async function runBatchEdit(args, workDir, readStateScope, _pathOpts, executeChildBuiltinTool, options = {}) {
|
|
720
|
+
const edits = Array.isArray(args.edits) ? args.edits : [];
|
|
721
|
+
if (edits.length === 0) return 'Error: edits array is required';
|
|
722
|
+
// Shallow-copy each entry so we don't mutate the caller's array in
|
|
723
|
+
// place. The previous shape rewrote `e.path` on the caller's object,
|
|
724
|
+
// which leaks the normalised path back out and breaks idempotent
|
|
725
|
+
// retries from the caller's perspective.
|
|
726
|
+
const normalizedEdits = edits.map((e) => (
|
|
727
|
+
e && typeof e === 'object'
|
|
728
|
+
? { ...e, path: normalizePathAndStripLineCoordinate(e.path, workDir) }
|
|
729
|
+
: e
|
|
730
|
+
));
|
|
731
|
+
const groups = new Map();
|
|
732
|
+
const missingPath = [];
|
|
733
|
+
for (const e of normalizedEdits) {
|
|
734
|
+
if (!e || !e.path) { missingPath.push(e); continue; }
|
|
735
|
+
if (!groups.has(e.path)) groups.set(e.path, []);
|
|
736
|
+
groups.get(e.path).push(e);
|
|
737
|
+
}
|
|
738
|
+
const parseLeadError = (body) => {
|
|
739
|
+
const first = String(body).split('\n')[0] || '';
|
|
740
|
+
if (!/^Error(\s|\[)/.test(first)) return null;
|
|
741
|
+
const colonIdx = first.indexOf(': ');
|
|
742
|
+
const msg = colonIdx !== -1 ? first.slice(colonIdx + 2) : first;
|
|
743
|
+
const retryHint = String(body).includes('snapshot recorded now') && !msg.includes('Retry the edit directly')
|
|
744
|
+
? ' (snapshot recorded; retry the same edit directly, no read needed)'
|
|
745
|
+
: '';
|
|
746
|
+
return `${msg}${retryHint}`;
|
|
747
|
+
};
|
|
748
|
+
const preparedResults = await Promise.all([...groups.entries()].map(async ([path, items]) => {
|
|
749
|
+
const prepared = await _prepareMultiEdit({
|
|
750
|
+
path,
|
|
751
|
+
edits: items.map(({ path: _p, ...rest }) => rest),
|
|
752
|
+
}, workDir, readStateScope, null, options);
|
|
753
|
+
if (!prepared.ok) {
|
|
754
|
+
const errMsg = parseLeadError(prepared.error) || prepared.error;
|
|
755
|
+
return { ok: false, path, line: `FAIL ${normalizeOutputPath(path)}: ${errMsg}` };
|
|
756
|
+
}
|
|
757
|
+
return { ok: true, path, items, prepared, line: `OK ${normalizeOutputPath(path)} (${items.length})${_formatStageNote(prepared.stageCounts)}` };
|
|
758
|
+
}));
|
|
759
|
+
const missingLines = missingPath.map(() => 'FAIL (missing-path): path is required');
|
|
760
|
+
const lines = [...preparedResults.map((result) => result.line), ...missingLines];
|
|
761
|
+
const failed = lines.filter((line) => line.startsWith('FAIL ')).length;
|
|
762
|
+
if (failed > 0) {
|
|
763
|
+
return `Error: batch edit preflight failed (${failed} of ${lines.length}); no changes written\n${lines.join('\n')}`;
|
|
764
|
+
}
|
|
765
|
+
const batchLockPaths = preparedResults.map((result) => result.prepared.fullPath);
|
|
766
|
+
return withBuiltinPathLocks(batchLockPaths, () => withAdvisoryLocks(batchLockPaths, async () => {
|
|
767
|
+
const lockPreparedResults = [];
|
|
768
|
+
for (const result of preparedResults) {
|
|
769
|
+
const prepared = await _prepareMultiEdit({
|
|
770
|
+
path: result.path,
|
|
771
|
+
edits: result.items.map(({ path: _p, ...rest }) => rest),
|
|
772
|
+
}, workDir, readStateScope, null, options);
|
|
773
|
+
if (!prepared.ok) {
|
|
774
|
+
const errMsg = parseLeadError(prepared.error) || prepared.error;
|
|
775
|
+
return `Error: batch edit lock prepare failed for ${normalizeOutputPath(result.path)}: ${errMsg}; no changes written`;
|
|
776
|
+
}
|
|
777
|
+
lockPreparedResults.push({
|
|
778
|
+
ok: true,
|
|
779
|
+
path: result.path,
|
|
780
|
+
items: result.items,
|
|
781
|
+
prepared,
|
|
782
|
+
line: `OK ${normalizeOutputPath(result.path)} (${result.items.length})${_formatStageNote(prepared.stageCounts)}`,
|
|
783
|
+
});
|
|
784
|
+
}
|
|
785
|
+
const prewriteResults = lockPreparedResults.map((result) => {
|
|
786
|
+
const err = _validatePreparedEditBase(result.prepared);
|
|
787
|
+
return err ? `FAIL ${normalizeOutputPath(result.path)}: ${err}` : result.line;
|
|
788
|
+
});
|
|
789
|
+
const prewriteFailed = prewriteResults.filter((line) => line.startsWith('FAIL ')).length;
|
|
790
|
+
if (prewriteFailed > 0) {
|
|
791
|
+
return `Error: batch edit prewrite check failed (${prewriteFailed} of ${lockPreparedResults.length}); no changes written\n${prewriteResults.join('\n')}`;
|
|
792
|
+
}
|
|
793
|
+
// Cross-file atomicity (capture-and-restore): snapshot every
|
|
794
|
+
// target's original bytes BEFORE any write. On the first commit
|
|
795
|
+
// failure, restore every already-committed file from its captured
|
|
796
|
+
// bytes so the batch is all-or-nothing. Partial state is reported
|
|
797
|
+
// only when a restore itself fails — an invariant-based recovery,
|
|
798
|
+
// no heuristic fallback.
|
|
799
|
+
const originals = new Map();
|
|
800
|
+
for (const result of lockPreparedResults) {
|
|
801
|
+
const captured = result.prepared.baseRawContent;
|
|
802
|
+
if (!Buffer.isBuffer(captured)) {
|
|
803
|
+
return `Error: batch edit pre-capture failed for ${normalizeOutputPath(result.path)}: missing base bytes from prepare; no changes written`;
|
|
804
|
+
}
|
|
805
|
+
originals.set(result.prepared.fullPath, captured);
|
|
806
|
+
}
|
|
807
|
+
const commitResults = [];
|
|
808
|
+
const committed = [];
|
|
809
|
+
let commitFailureIndex = -1;
|
|
810
|
+
for (let i = 0; i < lockPreparedResults.length; i++) {
|
|
811
|
+
const result = lockPreparedResults[i];
|
|
812
|
+
try {
|
|
813
|
+
await _commitPreparedEditUnlocked(result.prepared, readStateScope, options);
|
|
814
|
+
commitResults.push(result.line);
|
|
815
|
+
committed.push(result);
|
|
816
|
+
} catch (err) {
|
|
817
|
+
commitResults.push(`FAIL ${normalizeOutputPath(result.path)}: ${normalizeErrorMessage(err instanceof Error ? err.message : String(err))}`);
|
|
818
|
+
commitFailureIndex = i;
|
|
819
|
+
break;
|
|
820
|
+
}
|
|
821
|
+
}
|
|
822
|
+
if (commitFailureIndex !== -1) {
|
|
823
|
+
// Reviewer issue #1: include the FAILING target in the rollback
|
|
824
|
+
// set. _commitPreparedEditUnlocked can mutate the target before
|
|
825
|
+
// throwing — same-size in-place byte writes happen via
|
|
826
|
+
// tryWriteSameSizeByteReplacementsSync BEFORE the error throw,
|
|
827
|
+
// and the post-atomicWrite side effects (cache seed / snapshot
|
|
828
|
+
// record) can also throw after a successful rename. If we only
|
|
829
|
+
// restore `committed`, the failing target keeps the partial /
|
|
830
|
+
// post-atomic mutation while reporting "no changes written".
|
|
831
|
+
const restoreFailures = [];
|
|
832
|
+
const restoreTargets = [...committed, lockPreparedResults[commitFailureIndex]];
|
|
833
|
+
for (const prior of restoreTargets) {
|
|
834
|
+
const orig = originals.get(prior.prepared.fullPath);
|
|
835
|
+
try {
|
|
836
|
+
await atomicWrite(prior.prepared.fullPath, orig, {
|
|
837
|
+
sessionId: options?.sessionId,
|
|
838
|
+
mode: prior.prepared.baseMode,
|
|
839
|
+
expectedTargetSnapshot: _captureExpectedTargetSnapshot(prior.prepared.fullPath),
|
|
840
|
+
});
|
|
841
|
+
invalidateBuiltinResultCache([prior.prepared.fullPath]);
|
|
842
|
+
_seedRawContentCacheAfterWrite(prior.prepared.fullPath, orig);
|
|
843
|
+
markCodeGraphDirtyPaths([prior.prepared.fullPath]);
|
|
844
|
+
// Restore the read-snapshot too so subsequent reads
|
|
845
|
+
// observe the original bytes — otherwise a downstream
|
|
846
|
+
// edit/read would see the post-commit stale snapshot
|
|
847
|
+
// (recorded by _commitPreparedEditUnlocked on the
|
|
848
|
+
// already-rolled-back files) and fail code 7.
|
|
849
|
+
let _restoreStat = null;
|
|
850
|
+
try { _restoreStat = statSync(prior.prepared.fullPath); } catch {}
|
|
851
|
+
_recordReadSnapshot(prior.prepared.fullPath, _restoreStat || undefined, readStateScope, {
|
|
852
|
+
source: 'edit_rollback',
|
|
853
|
+
contentHash: _hashText(orig),
|
|
854
|
+
});
|
|
855
|
+
} catch (rerr) {
|
|
856
|
+
restoreFailures.push(`FAIL ${normalizeOutputPath(prior.path)} (restore): ${normalizeErrorMessage(rerr instanceof Error ? rerr.message : String(rerr))}`);
|
|
857
|
+
}
|
|
858
|
+
}
|
|
859
|
+
if (restoreFailures.length > 0) {
|
|
860
|
+
return `Error: batch edit write failed and rollback incomplete (${restoreFailures.length} of ${restoreTargets.length} file(s) left in mutated state); manual recovery required\n${commitResults.join('\n')}\n${restoreFailures.join('\n')}`;
|
|
861
|
+
}
|
|
862
|
+
return `Error: batch edit write failed (1 of ${lockPreparedResults.length}); rolled back ${restoreTargets.length} file(s) (including failing target); no changes written\n${commitResults.join('\n')}`;
|
|
863
|
+
}
|
|
864
|
+
return commitResults.join('\n');
|
|
865
|
+
}));
|
|
866
|
+
}
|
|
867
|
+
|
|
868
|
+
// --- Tool execution ---
|
|
869
|
+
export async function runSingleEdit(args, workDir, readStateScope, options = {}) {
|
|
870
|
+
args.path = normalizePathAndStripLineCoordinate(args.path, workDir);
|
|
871
|
+
const filePath = args.path;
|
|
872
|
+
let oldStr = args.old_string;
|
|
873
|
+
let newStr = args.new_string;
|
|
874
|
+
const replaceAll = args.replace_all === true;
|
|
875
|
+
if (!filePath || typeof oldStr !== 'string' || oldStr.length === 0)
|
|
876
|
+
return 'Error: path and non-empty old_string are required.';
|
|
877
|
+
if (typeof newStr !== 'string')
|
|
878
|
+
return 'Error: new_string must be a string';
|
|
879
|
+
{
|
|
880
|
+
const _nulIdx = newStr.indexOf('\u0000');
|
|
881
|
+
if (_nulIdx !== -1)
|
|
882
|
+
return `Error [code 11]: new_string contains NUL byte (U+0000) at offset ${_nulIdx} — source text must not contain NUL: ${filePath}`;
|
|
883
|
+
}
|
|
884
|
+
if (newStr === oldStr)
|
|
885
|
+
return 'Error: new_string must differ from old_string';
|
|
886
|
+
if (typeof isWindowsDevicePath === 'function' && isWindowsDevicePath(filePath)) {
|
|
887
|
+
return `Error: cannot edit Windows device path (reserved name or raw-device namespace): ${normalizeOutputPath(filePath)}`;
|
|
888
|
+
}
|
|
889
|
+
if (typeof hasUnsafeWin32Component === 'function' && hasUnsafeWin32Component(filePath)) {
|
|
890
|
+
return `Error: cannot edit Windows path with trailing dot/space or NTFS ADS suffix (bypasses device guard): ${normalizeOutputPath(filePath)}`;
|
|
891
|
+
}
|
|
892
|
+
// Size gate moved to the FOLD-FALLBACK path below
|
|
893
|
+
// (post-_tryBuildExactEditBuffer, before _findActualString).
|
|
894
|
+
// An exact-unique byte match is safe at any size; the
|
|
895
|
+
// >=30-line code-10 wording only fires once the buffered
|
|
896
|
+
// exact attempt has missed and we are about to enter the
|
|
897
|
+
// fragile fold/fuzzy tier.
|
|
898
|
+
// Line-prefix recovery: Read returns `<n>│<content>` (legacy `\t`
|
|
899
|
+
// also covered for muscle-memory pastes). If the model copies
|
|
900
|
+
// that rendering straight into old_string the on-disk file has
|
|
901
|
+
// no matching separator-prefixed line, so the match would
|
|
902
|
+
// silently fail. Auto-strip when every line carries a prefix;
|
|
903
|
+
// surface a guidance error when only some lines do, since
|
|
904
|
+
// mixing rendered + raw lines means we cannot infer intent.
|
|
905
|
+
if (/^\s*\d+[\t│→]/.test(oldStr)) {
|
|
906
|
+
const _stripped = _maybeAutoStripLineNumberPrefixes(oldStr);
|
|
907
|
+
if (_stripped !== null) {
|
|
908
|
+
_editPathTrace('edit_auto_strip_line_numbers', filePath, { mode: 'single' });
|
|
909
|
+
oldStr = _stripped;
|
|
910
|
+
if (newStr === oldStr)
|
|
911
|
+
return 'Error: new_string must differ from old_string (after auto-stripping Read line-number prefix from old_string)';
|
|
912
|
+
} else {
|
|
913
|
+
return `Error: old_string mixes Read line-number-prefixed lines ("<n>│…") with raw lines — strip the prefix from every line (or none) before Edit: ${filePath}`;
|
|
914
|
+
}
|
|
915
|
+
}
|
|
916
|
+
// CC parity (FileEditTool.stripTrailingWhitespace): drop trailing
|
|
917
|
+
// space/tab from each new_string line. Models routinely emit stray
|
|
918
|
+
// spaces at line ends; in source code that has no semantic meaning,
|
|
919
|
+
// so silent diffs from those bytes are pure noise. Markdown is the
|
|
920
|
+
// sole exception — `" \n"` is the hard-line-break syntax (`<br>`),
|
|
921
|
+
// altering it changes rendered output. Line terminators (LF / CRLF
|
|
922
|
+
// / lone CR) are preserved byte-exact.
|
|
923
|
+
if (!/\.(?:md|mdx)$/i.test(filePath)) {
|
|
924
|
+
const _strippedNew = _stripTrailingWhitespaceForEdit(newStr, oldStr);
|
|
925
|
+
if (_strippedNew !== newStr) {
|
|
926
|
+
_editPathTrace('edit_trim_trailing_ws', filePath, { mode: 'single' });
|
|
927
|
+
newStr = _strippedNew;
|
|
928
|
+
if (newStr === oldStr)
|
|
929
|
+
return 'Error: new_string must differ from old_string (after trimming trailing whitespace from new_string; rename target to .md/.mdx to preserve trailing spaces)';
|
|
930
|
+
}
|
|
931
|
+
}
|
|
932
|
+
const fullPath = resolveAgainstCwd(filePath, workDir);
|
|
933
|
+
// R1: short-circuit UNC/SMB paths before ANY stat/read on the
|
|
934
|
+
// edit target to prevent NTLM credential leakage via implicit
|
|
935
|
+
// network auth. Mirrors CC FileEditTool.ts:176.
|
|
936
|
+
if (fullPath.startsWith('\\\\') || fullPath.startsWith('//')) {
|
|
937
|
+
return `Error: UNC/SMB paths are not supported (R1: NTLM-leak prevention): ${filePath}`;
|
|
938
|
+
}
|
|
939
|
+
if (typeof isWindowsDevicePath === 'function' && isWindowsDevicePath(fullPath)) {
|
|
940
|
+
return `Error: cannot edit Windows device path (reserved name or raw-device namespace): ${normalizeOutputPath(filePath)}`;
|
|
941
|
+
}
|
|
942
|
+
if (typeof hasUnsafeWin32Component === 'function' && hasUnsafeWin32Component(fullPath)) {
|
|
943
|
+
return `Error: cannot edit Windows path with trailing dot/space or NTFS ADS suffix (bypasses device guard): ${normalizeOutputPath(filePath)}`;
|
|
944
|
+
}
|
|
945
|
+
// Fast path: when a snapshot already exists, skip the pre-lock stat.
|
|
946
|
+
// The lock-protected stat below still catches ENOENT and drift; the
|
|
947
|
+
// pre-lock stat is only needed to seed auto-snapshot on cold edits.
|
|
948
|
+
let _coldPrimedContent = null;
|
|
949
|
+
let _coldPrimedRawBuf = null;
|
|
950
|
+
let _coldPrimedStat = null;
|
|
951
|
+
{
|
|
952
|
+
let _preLockSnap = _getReadSnapshot(fullPath, readStateScope);
|
|
953
|
+
if (!_preLockSnap) {
|
|
954
|
+
let _preLockStat;
|
|
955
|
+
try { _preLockStat = statSync(fullPath); }
|
|
956
|
+
catch (err) {
|
|
957
|
+
if (err && err.code === 'ENOENT') {
|
|
958
|
+
const similar = findSimilarFile(fullPath);
|
|
959
|
+
const hint = similar ? ` Did you mean "${normalizeOutputPath(similar)}"?` : '';
|
|
960
|
+
return `Error [code 4]: file not found: ${filePath}${hint}`;
|
|
961
|
+
}
|
|
962
|
+
return `Error: ${normalizeErrorMessage(err instanceof Error ? err.message : String(err))}`;
|
|
963
|
+
}
|
|
964
|
+
// Auto-snapshot: same invariant as _runMultiEdit. Capture
|
|
965
|
+
// in-process, continue. Lock-protected re-snapshot inside
|
|
966
|
+
// _withPathLock below still catches concurrent external
|
|
967
|
+
// writes, so the auto path doesn't weaken CAS guarantees.
|
|
968
|
+
const _sPrimed = _primeReadSnapshotForEdit({
|
|
969
|
+
fullPath,
|
|
970
|
+
filePath,
|
|
971
|
+
st: _preLockStat,
|
|
972
|
+
scope: readStateScope,
|
|
973
|
+
oldStrings: [],
|
|
974
|
+
});
|
|
975
|
+
_preLockSnap = _getReadSnapshot(fullPath, readStateScope);
|
|
976
|
+
if (_sPrimed) {
|
|
977
|
+
_editPathTrace('edit_auto_snapshot', filePath, { mode: 'single' });
|
|
978
|
+
if (typeof _sPrimed.content === 'string' && Buffer.isBuffer(_sPrimed.rawBuf)) {
|
|
979
|
+
_coldPrimedContent = _sPrimed.content;
|
|
980
|
+
_coldPrimedRawBuf = _sPrimed.rawBuf;
|
|
981
|
+
_coldPrimedStat = _preLockStat;
|
|
982
|
+
}
|
|
983
|
+
} else if (!_preLockSnap) {
|
|
984
|
+
const _cold = _loadEditTargetBytes(fullPath);
|
|
985
|
+
if (!_cold) {
|
|
986
|
+
return `Error: failed to read edit target: ${filePath}`;
|
|
987
|
+
}
|
|
988
|
+
_coldPrimedContent = _cold.content;
|
|
989
|
+
_coldPrimedRawBuf = _cold.rawBuf;
|
|
990
|
+
_coldPrimedStat = _preLockStat;
|
|
991
|
+
_editPathTrace('edit_cold_no_snapshot', filePath, { mode: 'single' });
|
|
992
|
+
}
|
|
993
|
+
}
|
|
994
|
+
}
|
|
995
|
+
// CAS guard: serialise concurrent edits to the same path.
|
|
996
|
+
// After acquiring the lock, re-stat + re-hash to detect drift
|
|
997
|
+
// that occurred between the pre-lock snapshot check and now.
|
|
998
|
+
return _withPathLock(fullPath, () => withAdvisoryLocks([fullPath], async () => {
|
|
999
|
+
let editStat;
|
|
1000
|
+
try { editStat = statSync(fullPath); }
|
|
1001
|
+
catch (err) {
|
|
1002
|
+
if (err && err.code === 'ENOENT') {
|
|
1003
|
+
const similar = findSimilarFile(fullPath);
|
|
1004
|
+
const hint = similar ? ` Did you mean "${normalizeOutputPath(similar)}"?` : '';
|
|
1005
|
+
return `Error [code 4]: file not found: ${filePath}${hint}`;
|
|
1006
|
+
}
|
|
1007
|
+
return `Error: ${normalizeErrorMessage(err instanceof Error ? err.message : String(err))}`;
|
|
1008
|
+
}
|
|
1009
|
+
let editSnapshot = _getReadSnapshot(fullPath, readStateScope);
|
|
1010
|
+
// Error [code 7]: detect stale read via mtime drift (Anthropic
|
|
1011
|
+
// readFileState timestamp check parity). +1ms slack absorbs
|
|
1012
|
+
// filesystem timestamp resolution noise on NTFS/exFAT.
|
|
1013
|
+
let editPreloadedContent = null;
|
|
1014
|
+
let editPreloadedRawBuf = null;
|
|
1015
|
+
const editSnapshotReadCache = createMutationContentCache();
|
|
1016
|
+
if (!editSnapshot) {
|
|
1017
|
+
const _cold = _readEditTargetBytesUnderLock(fullPath, filePath, 'no_snapshot');
|
|
1018
|
+
if (!_cold) {
|
|
1019
|
+
return `Error: failed to read edit target: ${filePath}`;
|
|
1020
|
+
}
|
|
1021
|
+
editPreloadedContent = _cold.content;
|
|
1022
|
+
editPreloadedRawBuf = _cold.rawBuf;
|
|
1023
|
+
} else {
|
|
1024
|
+
const _editSnapStale = _isSnapshotStale(editStat, editSnapshot, fullPath, editSnapshotReadCache);
|
|
1025
|
+
if (!_editSnapStale && _coldPrimedContent !== null && editSnapshot
|
|
1026
|
+
&& typeof editSnapshot.contentHash === 'string'
|
|
1027
|
+
&& _coldPrimedStat && _statMatchesSnapshot(editStat, _coldPrimedStat)) {
|
|
1028
|
+
const _lockedPrimed = _loadEditTargetBytes(fullPath);
|
|
1029
|
+
if (_lockedPrimed) {
|
|
1030
|
+
if (Buffer.isBuffer(_coldPrimedRawBuf)
|
|
1031
|
+
&& _hashText(_lockedPrimed.rawBuf) !== _hashText(_coldPrimedRawBuf)) {
|
|
1032
|
+
_editPathTrace('edit_lock_cold_reread', filePath, {
|
|
1033
|
+
mode: 'single',
|
|
1034
|
+
reason: 'auto_snapshot_content_drift',
|
|
1035
|
+
});
|
|
1036
|
+
}
|
|
1037
|
+
if (_hashText(_lockedPrimed.rawBuf) === editSnapshot.contentHash) {
|
|
1038
|
+
editPreloadedContent = _lockedPrimed.content;
|
|
1039
|
+
editPreloadedRawBuf = _lockedPrimed.rawBuf;
|
|
1040
|
+
}
|
|
1041
|
+
}
|
|
1042
|
+
}
|
|
1043
|
+
if (_editSnapStale) {
|
|
1044
|
+
editPreloadedContent = _readContentIfSnapshotHashMatches(fullPath, editSnapshot, editSnapshotReadCache, editStat);
|
|
1045
|
+
if (editPreloadedContent !== null) {
|
|
1046
|
+
const cached = editSnapshotReadCache.getEntry(fullPath);
|
|
1047
|
+
if (Buffer.isBuffer(cached?.rawBuf)) editPreloadedRawBuf = cached.rawBuf;
|
|
1048
|
+
}
|
|
1049
|
+
if (editPreloadedContent === null) {
|
|
1050
|
+
const _staleRefresh = _tryStaleSnapshotAutoRefresh({
|
|
1051
|
+
fullPath,
|
|
1052
|
+
filePath,
|
|
1053
|
+
scope: readStateScope,
|
|
1054
|
+
stat: editStat,
|
|
1055
|
+
readRanges: editSnapshot?.ranges,
|
|
1056
|
+
oldStrings: [{ old_string: oldStr, replace_all: replaceAll }],
|
|
1057
|
+
readCache: editSnapshotReadCache,
|
|
1058
|
+
recordPreviewSnapshot: !replaceAll,
|
|
1059
|
+
});
|
|
1060
|
+
if (_staleRefresh?.error) return _staleRefresh.error;
|
|
1061
|
+
if (_staleRefresh?.content) {
|
|
1062
|
+
editPreloadedContent = _staleRefresh.content;
|
|
1063
|
+
editPreloadedRawBuf = _staleRefresh.rawBuf;
|
|
1064
|
+
editSnapshot = _getReadSnapshot(fullPath, readStateScope);
|
|
1065
|
+
} else {
|
|
1066
|
+
const recovery = _buildStaleEditRecovery({
|
|
1067
|
+
fullPath,
|
|
1068
|
+
scope: readStateScope,
|
|
1069
|
+
oldStrings: [oldStr],
|
|
1070
|
+
recordPreviewSnapshot: !replaceAll,
|
|
1071
|
+
});
|
|
1072
|
+
return `Error [code 7]: file modified since read (lint / formatter / external write) — read it again before editing: ${filePath}${recovery}`;
|
|
1073
|
+
}
|
|
1074
|
+
}
|
|
1075
|
+
} else {
|
|
1076
|
+
const cached = editSnapshotReadCache.getEntry(fullPath);
|
|
1077
|
+
if (typeof cached?.content === 'string' && Buffer.isBuffer(cached.rawBuf)) {
|
|
1078
|
+
editPreloadedContent = cached.content;
|
|
1079
|
+
editPreloadedRawBuf = cached.rawBuf;
|
|
1080
|
+
}
|
|
1081
|
+
}
|
|
1082
|
+
}
|
|
1083
|
+
try {
|
|
1084
|
+
if (editStat.size > 1073741824) {
|
|
1085
|
+
return `Error: edit refused: file too large (size: ${editStat.size}B, cap: 1GiB)`;
|
|
1086
|
+
}
|
|
1087
|
+
// Reviewer issue #3: validate encoding before the native
|
|
1088
|
+
// exact-edit dispatch too. Without this, the rust binary
|
|
1089
|
+
// would happily rewrite a Shift-JIS / Latin-1 / binary
|
|
1090
|
+
// file on the byte-exact code path while only the JS
|
|
1091
|
+
// fold path refused it. We read the buffer once, run
|
|
1092
|
+
// the shared guard, and reuse it downstream so non-
|
|
1093
|
+
// native paths don't double-read.
|
|
1094
|
+
let _preNativeRawBuf = editPreloadedRawBuf !== null
|
|
1095
|
+
? editPreloadedRawBuf
|
|
1096
|
+
: (_rawContentCacheGet(fullPath, editStat)
|
|
1097
|
+
|| (editPreloadedContent === null
|
|
1098
|
+
? await fsPromises.readFile(fullPath)
|
|
1099
|
+
: Buffer.from(editPreloadedContent, 'utf-8')));
|
|
1100
|
+
{
|
|
1101
|
+
const _utf8Err = _assertEditTargetUtf8(_preNativeRawBuf, filePath);
|
|
1102
|
+
if (_utf8Err) return _utf8Err;
|
|
1103
|
+
}
|
|
1104
|
+
if (_nativeEditShouldAttempt({ editStat, editSnapshot, oldStr, newStr, preloadedContent: editPreloadedContent, preloadedRawBuf: editPreloadedRawBuf })) {
|
|
1105
|
+
let nativeSignal = options?.signal || null;
|
|
1106
|
+
if (!nativeSignal && options?.sessionId) {
|
|
1107
|
+
try { nativeSignal = await getAbortSignalForSession(options.sessionId); } catch { nativeSignal = null; }
|
|
1108
|
+
}
|
|
1109
|
+
const nativeEdit = await _runNativeExactEdit({ fullPath, oldStr, newStr, replaceAll, signal: nativeSignal });
|
|
1110
|
+
if (nativeEdit?.ok) {
|
|
1111
|
+
invalidateBuiltinResultCache([fullPath]);
|
|
1112
|
+
markCodeGraphDirtyPaths([fullPath]);
|
|
1113
|
+
let writtenStat = null;
|
|
1114
|
+
try { writtenStat = statSync(fullPath); } catch {}
|
|
1115
|
+
_recordReadSnapshot(fullPath, writtenStat || undefined, readStateScope, {
|
|
1116
|
+
source: 'edit_native',
|
|
1117
|
+
contentHash: nativeEdit.contentHash,
|
|
1118
|
+
});
|
|
1119
|
+
_ioTrace('edit_native', {
|
|
1120
|
+
pathHash: _hashText(fullPath).slice(0, 12),
|
|
1121
|
+
replacements: nativeEdit.replacements,
|
|
1122
|
+
roundtripMs: Number(nativeEdit.roundtripMs.toFixed(3)),
|
|
1123
|
+
rustTotalMs: Number(nativeEdit.totalMs.toFixed(3)),
|
|
1124
|
+
readMs: Number(nativeEdit.readMs.toFixed(3)),
|
|
1125
|
+
applyMs: Number(nativeEdit.applyMs.toFixed(3)),
|
|
1126
|
+
writeMs: Number(nativeEdit.writeMs.toFixed(3)),
|
|
1127
|
+
});
|
|
1128
|
+
return `Edited: ${normalizeOutputPath(filePath)} (native)`;
|
|
1129
|
+
}
|
|
1130
|
+
if (nativeEdit && nativeEdit.fallback === false) {
|
|
1131
|
+
return `Error: native edit failed — ${normalizeErrorMessage(nativeEdit.error || 'unknown native edit error')}`;
|
|
1132
|
+
}
|
|
1133
|
+
}
|
|
1134
|
+
// D-R1-3: refuse edits on non-UTF-8 files before the
|
|
1135
|
+
// utf-8 decode round-trip silently corrupts bytes via
|
|
1136
|
+
// U+FFFD replacement. Use Buffer.isUtf8 (Node>=18) or
|
|
1137
|
+
// a byte-level walk as fallback.
|
|
1138
|
+
// Fix J-3: always read raw bytes and validate encoding,
|
|
1139
|
+
// even when editPreloadedContent was set via contentHash
|
|
1140
|
+
// preload — the cached string bypasses the guard otherwise.
|
|
1141
|
+
// I/O perf: single async Buffer read serves both UTF-8
|
|
1142
|
+
// validation AND content matching below; previous shape
|
|
1143
|
+
// did two sync readFileSync inside _withPathLock.
|
|
1144
|
+
const _rawBuf = _preNativeRawBuf;
|
|
1145
|
+
const _baseStatSnapshot = _captureStableBaseStatSnapshot(fullPath, editStat, _rawBuf);
|
|
1146
|
+
const _baseContentHash = _hashText(_rawBuf);
|
|
1147
|
+
const _baseMutationGen = _getPathMutationGeneration(fullPath);
|
|
1148
|
+
const _preWriteBaseCheck = () => _validatePreparedEditBase({
|
|
1149
|
+
fullPath,
|
|
1150
|
+
filePath,
|
|
1151
|
+
baseStatSnapshot: _baseStatSnapshot,
|
|
1152
|
+
baseContentHash: _baseContentHash,
|
|
1153
|
+
baseMutationGeneration: _baseMutationGen,
|
|
1154
|
+
});
|
|
1155
|
+
// UTF-8 already validated against _preNativeRawBuf above
|
|
1156
|
+
// (covers native dispatch + byte-exact + fold paths).
|
|
1157
|
+
// The shared `_assertEditTargetUtf8` helper enforces
|
|
1158
|
+
// the same error wording for every byte-exact write
|
|
1159
|
+
// path (single byte-exact buffer, multi byte-exact
|
|
1160
|
+
// buffer, native exact-edit).
|
|
1161
|
+
const _byteExactEdit = _tryBuildExactEditBuffer(_rawBuf, oldStr, newStr, replaceAll, editSnapshot, filePath);
|
|
1162
|
+
if (_byteExactEdit?.error) return _byteExactEdit.error;
|
|
1163
|
+
if (_byteExactEdit?.sameSize && Array.isArray(_byteExactEdit.replacements)) {
|
|
1164
|
+
const partial = _tryWriteSameSizeByteReplacementsSync(fullPath, _byteExactEdit.replacements, {
|
|
1165
|
+
baseStatSnapshot: _baseStatSnapshot,
|
|
1166
|
+
baseMutationGeneration: _baseMutationGen,
|
|
1167
|
+
baseContentHash: _baseContentHash,
|
|
1168
|
+
contentHash: _byteExactEdit.contentHash,
|
|
1169
|
+
fsync: options?.fsync,
|
|
1170
|
+
filePath,
|
|
1171
|
+
});
|
|
1172
|
+
if (partial?.error) return partial.error;
|
|
1173
|
+
if (partial?.ok) {
|
|
1174
|
+
invalidateBuiltinResultCache([fullPath]);
|
|
1175
|
+
markCodeGraphDirtyPaths([fullPath]);
|
|
1176
|
+
const _partialAfter = _materialiseByteReplacements(_rawBuf, _byteExactEdit.replacements);
|
|
1177
|
+
const _partialSnap = _postEditSnapshotMeta(editSnapshot, 'edit', _partialAfter, {
|
|
1178
|
+
contentBeforeEdit: _rawBuf,
|
|
1179
|
+
oldStr,
|
|
1180
|
+
newStr,
|
|
1181
|
+
replaceAll,
|
|
1182
|
+
});
|
|
1183
|
+
_recordReadSnapshot(fullPath, partial.stat || undefined, readStateScope, _partialSnap);
|
|
1184
|
+
return `Edited: ${normalizeOutputPath(filePath)}`;
|
|
1185
|
+
}
|
|
1186
|
+
}
|
|
1187
|
+
if (!Buffer.isBuffer(_byteExactEdit?.updated) && Array.isArray(_byteExactEdit?.replacements)) {
|
|
1188
|
+
_byteExactEdit.updated = _materialiseByteReplacements(_rawBuf, _byteExactEdit.replacements);
|
|
1189
|
+
}
|
|
1190
|
+
if (Buffer.isBuffer(_byteExactEdit?.updated)) {
|
|
1191
|
+
const _baseErr = _preWriteBaseCheck();
|
|
1192
|
+
if (_baseErr) return _baseErr;
|
|
1193
|
+
await atomicWrite(fullPath, _byteExactEdit.updated, {
|
|
1194
|
+
sessionId: options?.sessionId,
|
|
1195
|
+
mode: editStat.mode & 0o777,
|
|
1196
|
+
expectedTargetSnapshot: _captureExpectedTargetSnapshot(fullPath, editStat),
|
|
1197
|
+
});
|
|
1198
|
+
invalidateBuiltinResultCache([fullPath]);
|
|
1199
|
+
const writtenStat = _seedRawContentCacheAfterWrite(fullPath, _byteExactEdit.updated);
|
|
1200
|
+
markCodeGraphDirtyPaths([fullPath]);
|
|
1201
|
+
_recordReadSnapshot(fullPath, writtenStat || undefined, readStateScope, _postEditSnapshotMeta(editSnapshot, 'edit', _byteExactEdit.updated, {
|
|
1202
|
+
contentBeforeEdit: _rawBuf,
|
|
1203
|
+
oldStr,
|
|
1204
|
+
newStr,
|
|
1205
|
+
replaceAll,
|
|
1206
|
+
}));
|
|
1207
|
+
return `Edited: ${normalizeOutputPath(filePath)}`;
|
|
1208
|
+
}
|
|
1209
|
+
let content = editPreloadedContent === null
|
|
1210
|
+
? _rawBuf.toString('utf-8')
|
|
1211
|
+
: editPreloadedContent;
|
|
1212
|
+
// CC parity: pure deletion (newStr === '') swallows the
|
|
1213
|
+
// trailing newline that follows oldStr in the file so the
|
|
1214
|
+
// edit doesn't leave a stray empty line. Only the literal
|
|
1215
|
+
// match path absorbs; CRLF fallback keeps existing
|
|
1216
|
+
// semantics so range arithmetic stays simple.
|
|
1217
|
+
// CC parity: also try curly-quote normalization to find the
|
|
1218
|
+
// file's actual substring when the model emitted straight
|
|
1219
|
+
// quotes against a curly-quoted source (or vice versa).
|
|
1220
|
+
// _findActualString returns the byte-exact slice that the
|
|
1221
|
+
// file holds at the match position; downstream code keeps
|
|
1222
|
+
// operating on that exact slice.
|
|
1223
|
+
const _matchInfo = {};
|
|
1224
|
+
const _oldLiteralOccurrence = _findLiteralOccurrenceState(content, oldStr);
|
|
1225
|
+
// Fold-fallback size gate: only when the buffered
|
|
1226
|
+
// exact attempt has returned null AND no literal
|
|
1227
|
+
// hit survives in the decoded `content` view do we
|
|
1228
|
+
// refuse large chunks. An exact-unique landed edit
|
|
1229
|
+
// (handled above via _byteExactEdit.updated) never
|
|
1230
|
+
// reaches here.
|
|
1231
|
+
if (_oldLiteralOccurrence.count === 0) {
|
|
1232
|
+
const _foldSizeErr = _validateEditChunkSize(oldStr, replaceAll, false);
|
|
1233
|
+
if (_foldSizeErr) return _foldSizeErr;
|
|
1234
|
+
}
|
|
1235
|
+
const _matchedOldStr = _oldLiteralOccurrence.count > 0
|
|
1236
|
+
? oldStr
|
|
1237
|
+
: (_findActualString(content, oldStr, _matchInfo) || oldStr);
|
|
1238
|
+
if (_oldLiteralOccurrence.count > 0) _matchInfo.stage = 'exact';
|
|
1239
|
+
// CC parity: preserve the file's curly-quote typography when
|
|
1240
|
+
// the model wrote straight quotes and the matched bytes had
|
|
1241
|
+
// curly ones. Heuristic exception — see edit-normalize.mjs
|
|
1242
|
+
// preserveQuoteTypography for the carved-out justification.
|
|
1243
|
+
const _newStrAfterTypo = _preserveQuoteTypography(oldStr, _matchedOldStr, newStr);
|
|
1244
|
+
if (_newStrAfterTypo !== newStr) {
|
|
1245
|
+
_editPathTrace('edit_typography_preserve', filePath, { mode: 'single' });
|
|
1246
|
+
newStr = _newStrAfterTypo;
|
|
1247
|
+
}
|
|
1248
|
+
// Pure-deletion newline absorption.
|
|
1249
|
+
//
|
|
1250
|
+
// Single-occurrence path: extend the match over a trailing
|
|
1251
|
+
// LF/CRLF when present so the deletion doesn't leave a stray
|
|
1252
|
+
// empty line (CC parity). Previous shape used a global
|
|
1253
|
+
// `content.includes(X+'\n')` probe and rewrote eOldStr to
|
|
1254
|
+
// `X+'\n'`; under replace_all that left bare-X occurrences
|
|
1255
|
+
// (no following newline) unmatched. Switch to per-
|
|
1256
|
+
// occurrence range collection below when replace_all is
|
|
1257
|
+
// set, and keep the simple form for single replace.
|
|
1258
|
+
let eOldStr = _matchedOldStr;
|
|
1259
|
+
let _pureDeletionRanges = null;
|
|
1260
|
+
if (newStr === '' && !_matchedOldStr.endsWith('\n')) {
|
|
1261
|
+
if (replaceAll) {
|
|
1262
|
+
const ranges = [];
|
|
1263
|
+
let scan = 0;
|
|
1264
|
+
while ((scan = content.indexOf(_matchedOldStr, scan)) !== -1) {
|
|
1265
|
+
let end = scan + _matchedOldStr.length;
|
|
1266
|
+
if (content[end] === '\r' && content[end + 1] === '\n') end += 2;
|
|
1267
|
+
else if (content[end] === '\n') end += 1;
|
|
1268
|
+
ranges.push({ start: scan, end });
|
|
1269
|
+
scan = scan + _matchedOldStr.length;
|
|
1270
|
+
}
|
|
1271
|
+
if (ranges.length > 0) _pureDeletionRanges = ranges;
|
|
1272
|
+
} else {
|
|
1273
|
+
// Ambiguity must be judged on the ORIGINAL bare
|
|
1274
|
+
// _matchedOldStr occurrence count, not the newline-
|
|
1275
|
+
// absorbed eOldStr. Absorbing first can collapse a
|
|
1276
|
+
// >1-occurrence bare match into a unique extended
|
|
1277
|
+
// literal (e.g. 'X' present as 'X\r\n...X'),
|
|
1278
|
+
// silently single-deleting instead of surfacing the
|
|
1279
|
+
// ambiguous-match (code 9) error below. Only absorb
|
|
1280
|
+
// when the bare match is unique; otherwise leave
|
|
1281
|
+
// eOldStr as _matchedOldStr so the occurrence.count
|
|
1282
|
+
// > 1 ambiguous branch fires.
|
|
1283
|
+
const _bareOccurrence = _findLiteralOccurrenceState(content, _matchedOldStr);
|
|
1284
|
+
if (_bareOccurrence.count <= 1) {
|
|
1285
|
+
if (content.includes(_matchedOldStr + '\r\n')) {
|
|
1286
|
+
eOldStr = _matchedOldStr + '\r\n';
|
|
1287
|
+
} else if (content.includes(_matchedOldStr + '\n')) {
|
|
1288
|
+
eOldStr = _matchedOldStr + '\n';
|
|
1289
|
+
}
|
|
1290
|
+
}
|
|
1291
|
+
}
|
|
1292
|
+
}
|
|
1293
|
+
let updated;
|
|
1294
|
+
const occurrence = (eOldStr === oldStr && _oldLiteralOccurrence.count > 0)
|
|
1295
|
+
? _oldLiteralOccurrence
|
|
1296
|
+
: _findLiteralOccurrenceState(content, eOldStr);
|
|
1297
|
+
if (occurrence.count === 0) {
|
|
1298
|
+
const crlfMatch = _findCrlfNormalisedMatches(content, oldStr);
|
|
1299
|
+
const crlfCount = crlfMatch ? crlfMatch.ranges.length : 0;
|
|
1300
|
+
if (crlfCount === 0) {
|
|
1301
|
+
const _foldAmb = _foldTierAmbiguityError(content, oldStr, filePath);
|
|
1302
|
+
if (_foldAmb) return _foldAmb;
|
|
1303
|
+
return `Error [code 8]: old_string not found in ${filePath} (no exact/fold/nfc-fold/crlf-fold match).${_optionalEditMissDetails(content, oldStr, editSnapshot, { path: filePath, newString: newStr, replaceAll })}`;
|
|
1304
|
+
}
|
|
1305
|
+
if (crlfCount > 1 && !replaceAll)
|
|
1306
|
+
return `Error [code 9]: old_string found ${crlfCount} times in ${filePath} (via crlf-fold);${_formatMatchLines(_occurrenceLinesCrlf(content, crlfMatch.ranges), crlfCount)} set replace_all:true or provide more unique context`;
|
|
1307
|
+
updated = _replaceRangesFromOriginal(content, replaceAll ? crlfMatch.ranges : crlfMatch.ranges.slice(0, 1), newStr);
|
|
1308
|
+
_matchInfo.stage = 'crlf-fold';
|
|
1309
|
+
} else {
|
|
1310
|
+
if (occurrence.count > 1 && !replaceAll) {
|
|
1311
|
+
const count = _countLiteralOccurrences(content, eOldStr);
|
|
1312
|
+
return `Error [code 9]: old_string found ${count} times in ${filePath}${_formatStageInline(_matchInfo.stage)};${_formatMatchLines(_occurrenceLinesPlain(content, eOldStr), count)} set replace_all:true or provide more unique context`;
|
|
1313
|
+
}
|
|
1314
|
+
if (replaceAll && Array.isArray(_pureDeletionRanges) && _pureDeletionRanges.length > 0) {
|
|
1315
|
+
// Per-occurrence pure-deletion: extend each match
|
|
1316
|
+
// over its own trailing LF/CRLF instead of a
|
|
1317
|
+
// single global `eOldStr+'\n'` literal that would
|
|
1318
|
+
// miss bare-X occurrences. See range collection
|
|
1319
|
+
// above.
|
|
1320
|
+
updated = _replaceRangesFromOriginal(content, _pureDeletionRanges, newStr);
|
|
1321
|
+
} else {
|
|
1322
|
+
const _indentFixedNewStr = newStr;
|
|
1323
|
+
const _eolPreservedNewStr = _replacementForOriginalSlice(_indentFixedNewStr, eOldStr, content);
|
|
1324
|
+
updated = replaceAll
|
|
1325
|
+
? content.split(eOldStr).join(_eolPreservedNewStr)
|
|
1326
|
+
: _replaceSingleLiteralAt(content, occurrence.index, eOldStr, _eolPreservedNewStr);
|
|
1327
|
+
}
|
|
1328
|
+
}
|
|
1329
|
+
// Atomic write — see `write` handler for rationale.
|
|
1330
|
+
const _baseErrFinal = _preWriteBaseCheck();
|
|
1331
|
+
if (_baseErrFinal) return _baseErrFinal;
|
|
1332
|
+
await atomicWrite(fullPath, updated, {
|
|
1333
|
+
sessionId: options?.sessionId,
|
|
1334
|
+
mode: editStat.mode & 0o777,
|
|
1335
|
+
expectedTargetSnapshot: _captureExpectedTargetSnapshot(fullPath, editStat),
|
|
1336
|
+
});
|
|
1337
|
+
invalidateBuiltinResultCache([fullPath]);
|
|
1338
|
+
const writtenStat = _seedRawContentCacheAfterWrite(fullPath, updated);
|
|
1339
|
+
markCodeGraphDirtyPaths([fullPath]);
|
|
1340
|
+
// Refresh the snapshot to the post-write mtime so a chain
|
|
1341
|
+
// of edits against the same file doesn't trip the stale
|
|
1342
|
+
// check on the second hop. Keep partial-read coverage
|
|
1343
|
+
// partial; an edit should not imply the model saw the
|
|
1344
|
+
// whole file.
|
|
1345
|
+
_recordReadSnapshot(fullPath, writtenStat || undefined, readStateScope, _postEditSnapshotMeta(editSnapshot, 'edit', updated, {
|
|
1346
|
+
contentBeforeEdit: content,
|
|
1347
|
+
oldStr: eOldStr,
|
|
1348
|
+
newStr,
|
|
1349
|
+
replaceAll,
|
|
1350
|
+
}));
|
|
1351
|
+
// Stage note surfaces non-exact matches so the model
|
|
1352
|
+
// learns to send literal bytes next time and the user
|
|
1353
|
+
// can spot silent typography / whitespace drift. Exact
|
|
1354
|
+
// matches stay terse — they are the steady-state path.
|
|
1355
|
+
const _stageNote = (_matchInfo.stage && _matchInfo.stage !== 'exact')
|
|
1356
|
+
? _formatStageNote({ [_matchInfo.stage]: 1 })
|
|
1357
|
+
: '';
|
|
1358
|
+
return `Edited: ${normalizeOutputPath(filePath)}${_stageNote}`;
|
|
1359
|
+
}
|
|
1360
|
+
catch (err) {
|
|
1361
|
+
return `Error: ${normalizeErrorMessage(err instanceof Error ? err.message : String(err))}`;
|
|
1362
|
+
}
|
|
1363
|
+
}));
|
|
1364
|
+
}
|