apex-code 0.0.1-alpha.9 → 0.0.4
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/CHANGELOG.md +150 -2
- package/README.md +9 -8
- package/dist/cli/args.d.ts +25 -1
- package/dist/cli/args.d.ts.map +1 -1
- package/dist/cli/args.js +43 -1
- package/dist/cli/args.js.map +1 -1
- package/dist/cli/mcp-command.d.ts +9 -0
- package/dist/cli/mcp-command.d.ts.map +1 -0
- package/dist/cli/mcp-command.js +53 -0
- package/dist/cli/mcp-command.js.map +1 -0
- package/dist/cli/project-trust.d.ts.map +1 -1
- package/dist/cli/project-trust.js +3 -1
- package/dist/cli/project-trust.js.map +1 -1
- package/dist/cli/startup-ui.d.ts.map +1 -1
- package/dist/cli/startup-ui.js +2 -1
- package/dist/cli/startup-ui.js.map +1 -1
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +94 -5
- package/dist/cli.js.map +1 -1
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +24 -5
- package/dist/config.js.map +1 -1
- package/dist/core/agent-session-services.d.ts +3 -0
- package/dist/core/agent-session-services.d.ts.map +1 -1
- package/dist/core/agent-session-services.js +1 -0
- package/dist/core/agent-session-services.js.map +1 -1
- package/dist/core/agent-session.d.ts +157 -8
- package/dist/core/agent-session.d.ts.map +1 -1
- package/dist/core/agent-session.js +565 -22
- package/dist/core/agent-session.js.map +1 -1
- package/dist/core/bash-executor.d.ts.map +1 -1
- package/dist/core/bash-executor.js +7 -0
- package/dist/core/bash-executor.js.map +1 -1
- package/dist/core/checkpoints/git-checkpoints.d.ts +39 -0
- package/dist/core/checkpoints/git-checkpoints.d.ts.map +1 -0
- package/dist/core/checkpoints/git-checkpoints.js +251 -0
- package/dist/core/checkpoints/git-checkpoints.js.map +1 -0
- package/dist/core/checkpoints/session-checkpoints.d.ts +23 -0
- package/dist/core/checkpoints/session-checkpoints.d.ts.map +1 -0
- package/dist/core/checkpoints/session-checkpoints.js +36 -0
- package/dist/core/checkpoints/session-checkpoints.js.map +1 -0
- package/dist/core/compaction/branch-summarization.d.ts.map +1 -1
- package/dist/core/compaction/branch-summarization.js +4 -3
- package/dist/core/compaction/branch-summarization.js.map +1 -1
- package/dist/core/compaction/compaction.d.ts +5 -0
- package/dist/core/compaction/compaction.d.ts.map +1 -1
- package/dist/core/compaction/compaction.js +19 -5
- package/dist/core/compaction/compaction.js.map +1 -1
- package/dist/core/delegation/runtime.d.ts +6 -5
- package/dist/core/delegation/runtime.d.ts.map +1 -1
- package/dist/core/delegation/runtime.js +4 -3
- package/dist/core/delegation/runtime.js.map +1 -1
- package/dist/core/exec.d.ts.map +1 -1
- package/dist/core/exec.js +5 -2
- package/dist/core/exec.js.map +1 -1
- package/dist/core/extensions/index.d.ts +1 -1
- package/dist/core/extensions/index.d.ts.map +1 -1
- package/dist/core/extensions/index.js.map +1 -1
- package/dist/core/extensions/loader.d.ts.map +1 -1
- package/dist/core/extensions/loader.js +8 -2
- package/dist/core/extensions/loader.js.map +1 -1
- package/dist/core/extensions/runner.d.ts +5 -0
- package/dist/core/extensions/runner.d.ts.map +1 -1
- package/dist/core/extensions/runner.js +45 -1
- package/dist/core/extensions/runner.js.map +1 -1
- package/dist/core/extensions/source-runtime.d.ts +14 -0
- package/dist/core/extensions/source-runtime.d.ts.map +1 -0
- package/dist/core/extensions/source-runtime.js +56 -0
- package/dist/core/extensions/source-runtime.js.map +1 -0
- package/dist/core/extensions/types.d.ts +22 -2
- package/dist/core/extensions/types.d.ts.map +1 -1
- package/dist/core/extensions/types.js.map +1 -1
- package/dist/core/formatter-lifecycle.d.ts +69 -0
- package/dist/core/formatter-lifecycle.d.ts.map +1 -0
- package/dist/core/formatter-lifecycle.js +360 -0
- package/dist/core/formatter-lifecycle.js.map +1 -0
- package/dist/core/hooks/command-handler.d.ts +19 -0
- package/dist/core/hooks/command-handler.d.ts.map +1 -0
- package/dist/core/hooks/command-handler.js +112 -0
- package/dist/core/hooks/command-handler.js.map +1 -0
- package/dist/core/hooks/http-handler.d.ts +10 -0
- package/dist/core/hooks/http-handler.d.ts.map +1 -0
- package/dist/core/hooks/http-handler.js +36 -0
- package/dist/core/hooks/http-handler.js.map +1 -0
- package/dist/core/hooks/loader.d.ts +29 -0
- package/dist/core/hooks/loader.d.ts.map +1 -0
- package/dist/core/hooks/loader.js +139 -0
- package/dist/core/hooks/loader.js.map +1 -0
- package/dist/core/hooks/runtime.d.ts +13 -0
- package/dist/core/hooks/runtime.d.ts.map +1 -0
- package/dist/core/hooks/runtime.js +63 -0
- package/dist/core/hooks/runtime.js.map +1 -0
- package/dist/core/hooks/types.d.ts +83 -0
- package/dist/core/hooks/types.d.ts.map +1 -0
- package/dist/core/hooks/types.js +24 -0
- package/dist/core/hooks/types.js.map +1 -0
- package/dist/core/mcp/config.d.ts +16 -0
- package/dist/core/mcp/config.d.ts.map +1 -0
- package/dist/core/mcp/config.js +166 -0
- package/dist/core/mcp/config.js.map +1 -0
- package/dist/core/mcp/connector.d.ts +35 -0
- package/dist/core/mcp/connector.d.ts.map +1 -0
- package/dist/core/mcp/connector.js +105 -0
- package/dist/core/mcp/connector.js.map +1 -0
- package/dist/core/mcp/contract.d.ts +19 -0
- package/dist/core/mcp/contract.d.ts.map +1 -0
- package/dist/core/mcp/contract.js +89 -0
- package/dist/core/mcp/contract.js.map +1 -0
- package/dist/core/mcp/mcp-tool.d.ts +32 -0
- package/dist/core/mcp/mcp-tool.d.ts.map +1 -0
- package/dist/core/mcp/mcp-tool.js +171 -0
- package/dist/core/mcp/mcp-tool.js.map +1 -0
- package/dist/core/mcp/metadata-cache.d.ts +29 -0
- package/dist/core/mcp/metadata-cache.d.ts.map +1 -0
- package/dist/core/mcp/metadata-cache.js +99 -0
- package/dist/core/mcp/metadata-cache.js.map +1 -0
- package/dist/core/mcp/oauth/authorize.d.ts +21 -0
- package/dist/core/mcp/oauth/authorize.d.ts.map +1 -0
- package/dist/core/mcp/oauth/authorize.js +32 -0
- package/dist/core/mcp/oauth/authorize.js.map +1 -0
- package/dist/core/mcp/oauth/discover.d.ts +27 -0
- package/dist/core/mcp/oauth/discover.d.ts.map +1 -0
- package/dist/core/mcp/oauth/discover.js +116 -0
- package/dist/core/mcp/oauth/discover.js.map +1 -0
- package/dist/core/mcp/oauth/flow.d.ts +31 -0
- package/dist/core/mcp/oauth/flow.d.ts.map +1 -0
- package/dist/core/mcp/oauth/flow.js +175 -0
- package/dist/core/mcp/oauth/flow.js.map +1 -0
- package/dist/core/mcp/oauth/mcp-token.d.ts +51 -0
- package/dist/core/mcp/oauth/mcp-token.d.ts.map +1 -0
- package/dist/core/mcp/oauth/mcp-token.js +129 -0
- package/dist/core/mcp/oauth/mcp-token.js.map +1 -0
- package/dist/core/mcp/oauth/pkce.d.ts +12 -0
- package/dist/core/mcp/oauth/pkce.d.ts.map +1 -0
- package/dist/core/mcp/oauth/pkce.js +11 -0
- package/dist/core/mcp/oauth/pkce.js.map +1 -0
- package/dist/core/mcp/oauth/register.d.ts +16 -0
- package/dist/core/mcp/oauth/register.d.ts.map +1 -0
- package/dist/core/mcp/oauth/register.js +42 -0
- package/dist/core/mcp/oauth/register.js.map +1 -0
- package/dist/core/mcp/oauth/token-client.d.ts +33 -0
- package/dist/core/mcp/oauth/token-client.d.ts.map +1 -0
- package/dist/core/mcp/oauth/token-client.js +65 -0
- package/dist/core/mcp/oauth/token-client.js.map +1 -0
- package/dist/core/mcp/runtime.d.ts +20 -0
- package/dist/core/mcp/runtime.d.ts.map +1 -0
- package/dist/core/mcp/runtime.js +37 -0
- package/dist/core/mcp/runtime.js.map +1 -0
- package/dist/core/mcp/schema.d.ts +22 -0
- package/dist/core/mcp/schema.d.ts.map +1 -0
- package/dist/core/mcp/schema.js +20 -0
- package/dist/core/mcp/schema.js.map +1 -0
- package/dist/core/mcp/server-manager.d.ts +43 -0
- package/dist/core/mcp/server-manager.d.ts.map +1 -0
- package/dist/core/mcp/server-manager.js +142 -0
- package/dist/core/mcp/server-manager.js.map +1 -0
- package/dist/core/mcp/types.d.ts +100 -0
- package/dist/core/mcp/types.d.ts.map +1 -0
- package/dist/core/mcp/types.js +8 -0
- package/dist/core/mcp/types.js.map +1 -0
- package/dist/core/permissions/gate.d.ts.map +1 -1
- package/dist/core/permissions/gate.js +45 -4
- package/dist/core/permissions/gate.js.map +1 -1
- package/dist/core/permissions/operations.d.ts +20 -0
- package/dist/core/permissions/operations.d.ts.map +1 -0
- package/dist/core/permissions/operations.js +9 -0
- package/dist/core/permissions/operations.js.map +1 -0
- package/dist/core/permissions/policy-command.d.ts +42 -0
- package/dist/core/permissions/policy-command.d.ts.map +1 -0
- package/dist/core/permissions/policy-command.js +44 -0
- package/dist/core/permissions/policy-command.js.map +1 -0
- package/dist/core/permissions/responder.d.ts +49 -2
- package/dist/core/permissions/responder.d.ts.map +1 -1
- package/dist/core/permissions/responder.js +20 -8
- package/dist/core/permissions/responder.js.map +1 -1
- package/dist/core/permissions/rules.d.ts.map +1 -1
- package/dist/core/permissions/rules.js +11 -1
- package/dist/core/permissions/rules.js.map +1 -1
- package/dist/core/permissions/store.d.ts +18 -0
- package/dist/core/permissions/store.d.ts.map +1 -1
- package/dist/core/permissions/store.js +89 -8
- package/dist/core/permissions/store.js.map +1 -1
- package/dist/core/policy-executor.d.ts +47 -0
- package/dist/core/policy-executor.d.ts.map +1 -0
- package/dist/core/policy-executor.js +228 -0
- package/dist/core/policy-executor.js.map +1 -0
- package/dist/core/policy-loader.d.ts +72 -0
- package/dist/core/policy-loader.d.ts.map +1 -0
- package/dist/core/policy-loader.js +195 -0
- package/dist/core/policy-loader.js.map +1 -0
- package/dist/core/project-trust.d.ts +1 -1
- package/dist/core/project-trust.d.ts.map +1 -1
- package/dist/core/project-trust.js.map +1 -1
- package/dist/core/sandbox/bwrap-arguments.d.ts +41 -0
- package/dist/core/sandbox/bwrap-arguments.d.ts.map +1 -0
- package/dist/core/sandbox/bwrap-arguments.js +136 -0
- package/dist/core/sandbox/bwrap-arguments.js.map +1 -0
- package/dist/core/sandbox/child-entry.d.ts.map +1 -1
- package/dist/core/sandbox/child-entry.js +3 -1
- package/dist/core/sandbox/child-entry.js.map +1 -1
- package/dist/core/sandbox/cli-launch.d.ts +84 -4
- package/dist/core/sandbox/cli-launch.d.ts.map +1 -1
- package/dist/core/sandbox/cli-launch.js +115 -16
- package/dist/core/sandbox/cli-launch.js.map +1 -1
- package/dist/core/sandbox/cli-supervisor.d.ts +2 -0
- package/dist/core/sandbox/cli-supervisor.d.ts.map +1 -1
- package/dist/core/sandbox/cli-supervisor.js +50 -2
- package/dist/core/sandbox/cli-supervisor.js.map +1 -1
- package/dist/core/sandbox/full-access.d.ts +28 -0
- package/dist/core/sandbox/full-access.d.ts.map +1 -0
- package/dist/core/sandbox/full-access.js +63 -0
- package/dist/core/sandbox/full-access.js.map +1 -0
- package/dist/core/sandbox/git-identity.d.ts +57 -0
- package/dist/core/sandbox/git-identity.d.ts.map +1 -0
- package/dist/core/sandbox/git-identity.js +78 -0
- package/dist/core/sandbox/git-identity.js.map +1 -0
- package/dist/core/sandbox/host-approval.d.ts +42 -0
- package/dist/core/sandbox/host-approval.d.ts.map +1 -0
- package/dist/core/sandbox/host-approval.js +99 -0
- package/dist/core/sandbox/host-approval.js.map +1 -0
- package/dist/core/sandbox/linux-backend.d.ts +14 -0
- package/dist/core/sandbox/linux-backend.d.ts.map +1 -1
- package/dist/core/sandbox/linux-backend.js +158 -80
- package/dist/core/sandbox/linux-backend.js.map +1 -1
- package/dist/core/sandbox/macos-backend.d.ts +5 -0
- package/dist/core/sandbox/macos-backend.d.ts.map +1 -1
- package/dist/core/sandbox/macos-backend.js +164 -5
- package/dist/core/sandbox/macos-backend.js.map +1 -1
- package/dist/core/sandbox/network-proxy.d.ts +19 -0
- package/dist/core/sandbox/network-proxy.d.ts.map +1 -1
- package/dist/core/sandbox/network-proxy.js +93 -19
- package/dist/core/sandbox/network-proxy.js.map +1 -1
- package/dist/core/sandbox/policy.d.ts +8 -0
- package/dist/core/sandbox/policy.d.ts.map +1 -1
- package/dist/core/sandbox/policy.js +29 -12
- package/dist/core/sandbox/policy.js.map +1 -1
- package/dist/core/sandbox/profiles.d.ts +25 -0
- package/dist/core/sandbox/profiles.d.ts.map +1 -0
- package/dist/core/sandbox/profiles.js +23 -0
- package/dist/core/sandbox/profiles.js.map +1 -0
- package/dist/core/sandbox/rpc/command-client.d.ts +28 -0
- package/dist/core/sandbox/rpc/command-client.d.ts.map +1 -0
- package/dist/core/sandbox/rpc/command-client.js +86 -0
- package/dist/core/sandbox/rpc/command-client.js.map +1 -0
- package/dist/core/sandbox/rpc/command-proxy.d.ts +48 -0
- package/dist/core/sandbox/rpc/command-proxy.d.ts.map +1 -0
- package/dist/core/sandbox/rpc/command-proxy.js +120 -0
- package/dist/core/sandbox/rpc/command-proxy.js.map +1 -0
- package/dist/core/sandbox/rpc/credential-proxy.d.ts.map +1 -1
- package/dist/core/sandbox/rpc/credential-proxy.js +2 -96
- package/dist/core/sandbox/rpc/credential-proxy.js.map +1 -1
- package/dist/core/sandbox/rpc/framing.d.ts +35 -0
- package/dist/core/sandbox/rpc/framing.d.ts.map +1 -0
- package/dist/core/sandbox/rpc/framing.js +114 -0
- package/dist/core/sandbox/rpc/framing.js.map +1 -0
- package/dist/core/sandbox/rpc/git-credential-helper.d.ts +47 -0
- package/dist/core/sandbox/rpc/git-credential-helper.d.ts.map +1 -0
- package/dist/core/sandbox/rpc/git-credential-helper.js +222 -0
- package/dist/core/sandbox/rpc/git-credential-helper.js.map +1 -0
- package/dist/core/sandbox/rpc/git-credential-proxy.d.ts +66 -0
- package/dist/core/sandbox/rpc/git-credential-proxy.d.ts.map +1 -0
- package/dist/core/sandbox/rpc/git-credential-proxy.js +209 -0
- package/dist/core/sandbox/rpc/git-credential-proxy.js.map +1 -0
- package/dist/core/sandbox/supervisor-temp.d.ts +18 -0
- package/dist/core/sandbox/supervisor-temp.d.ts.map +1 -0
- package/dist/core/sandbox/supervisor-temp.js +21 -0
- package/dist/core/sandbox/supervisor-temp.js.map +1 -0
- package/dist/core/sandbox/supervisor.d.ts +12 -1
- package/dist/core/sandbox/supervisor.d.ts.map +1 -1
- package/dist/core/sandbox/supervisor.js +15 -1
- package/dist/core/sandbox/supervisor.js.map +1 -1
- package/dist/core/sandbox/terminal-handoff.d.ts +68 -0
- package/dist/core/sandbox/terminal-handoff.d.ts.map +1 -0
- package/dist/core/sandbox/terminal-handoff.js +221 -0
- package/dist/core/sandbox/terminal-handoff.js.map +1 -0
- package/dist/core/sandbox/terminal-size.d.ts +13 -0
- package/dist/core/sandbox/terminal-size.d.ts.map +1 -1
- package/dist/core/sandbox/terminal-size.js +23 -2
- package/dist/core/sandbox/terminal-size.js.map +1 -1
- package/dist/core/sdk.d.ts +22 -0
- package/dist/core/sdk.d.ts.map +1 -1
- package/dist/core/sdk.js +53 -1
- package/dist/core/sdk.js.map +1 -1
- package/dist/core/session-manager.d.ts +13 -1
- package/dist/core/session-manager.d.ts.map +1 -1
- package/dist/core/session-manager.js +32 -5
- package/dist/core/session-manager.js.map +1 -1
- package/dist/core/settings-manager.d.ts +163 -1
- package/dist/core/settings-manager.d.ts.map +1 -1
- package/dist/core/settings-manager.js +107 -0
- package/dist/core/settings-manager.js.map +1 -1
- package/dist/core/slash-commands.d.ts.map +1 -1
- package/dist/core/slash-commands.js +2 -0
- package/dist/core/slash-commands.js.map +1 -1
- package/dist/core/tools/background-shell.d.ts +39 -0
- package/dist/core/tools/background-shell.d.ts.map +1 -0
- package/dist/core/tools/background-shell.js +81 -0
- package/dist/core/tools/background-shell.js.map +1 -0
- package/dist/core/tools/bash-command-segments.d.ts.map +1 -1
- package/dist/core/tools/bash-command-segments.js +2 -0
- package/dist/core/tools/bash-command-segments.js.map +1 -1
- package/dist/core/tools/bash.d.ts +30 -15
- package/dist/core/tools/bash.d.ts.map +1 -1
- package/dist/core/tools/bash.js +228 -12
- package/dist/core/tools/bash.js.map +1 -1
- package/dist/core/tools/contract-snapshot.d.ts +58 -0
- package/dist/core/tools/contract-snapshot.d.ts.map +1 -0
- package/dist/core/tools/contract-snapshot.js +51 -0
- package/dist/core/tools/contract-snapshot.js.map +1 -0
- package/dist/core/tools/contract.d.ts +53 -5
- package/dist/core/tools/contract.d.ts.map +1 -1
- package/dist/core/tools/contract.js +22 -2
- package/dist/core/tools/contract.js.map +1 -1
- package/dist/core/tools/delegate.d.ts +6 -4
- package/dist/core/tools/delegate.d.ts.map +1 -1
- package/dist/core/tools/delegate.js +21 -11
- package/dist/core/tools/delegate.js.map +1 -1
- package/dist/core/tools/edit-diff.d.ts +48 -0
- package/dist/core/tools/edit-diff.d.ts.map +1 -1
- package/dist/core/tools/edit-diff.js +215 -3
- package/dist/core/tools/edit-diff.js.map +1 -1
- package/dist/core/tools/edit.d.ts +8 -0
- package/dist/core/tools/edit.d.ts.map +1 -1
- package/dist/core/tools/edit.js +53 -7
- package/dist/core/tools/edit.js.map +1 -1
- package/dist/core/tools/find.d.ts.map +1 -1
- package/dist/core/tools/find.js +5 -2
- package/dist/core/tools/find.js.map +1 -1
- package/dist/core/tools/grep.d.ts.map +1 -1
- package/dist/core/tools/grep.js +5 -2
- package/dist/core/tools/grep.js.map +1 -1
- package/dist/core/tools/index.d.ts +10 -1
- package/dist/core/tools/index.d.ts.map +1 -1
- package/dist/core/tools/index.js +3 -0
- package/dist/core/tools/index.js.map +1 -1
- package/dist/core/tools/ls.d.ts.map +1 -1
- package/dist/core/tools/ls.js +5 -2
- package/dist/core/tools/ls.js.map +1 -1
- package/dist/core/tools/lsp.d.ts +19 -1
- package/dist/core/tools/lsp.d.ts.map +1 -1
- package/dist/core/tools/lsp.js +163 -10
- package/dist/core/tools/lsp.js.map +1 -1
- package/dist/core/tools/path-permission.d.ts +3 -0
- package/dist/core/tools/path-permission.d.ts.map +1 -1
- package/dist/core/tools/path-permission.js +16 -4
- package/dist/core/tools/path-permission.js.map +1 -1
- package/dist/core/tools/path-utils.d.ts +27 -2
- package/dist/core/tools/path-utils.d.ts.map +1 -1
- package/dist/core/tools/path-utils.js +221 -4
- package/dist/core/tools/path-utils.js.map +1 -1
- package/dist/core/tools/read.d.ts.map +1 -1
- package/dist/core/tools/read.js +20 -11
- package/dist/core/tools/read.js.map +1 -1
- package/dist/core/tools/test.d.ts +54 -9
- package/dist/core/tools/test.d.ts.map +1 -1
- package/dist/core/tools/test.js +239 -19
- package/dist/core/tools/test.js.map +1 -1
- package/dist/core/tools/write.d.ts +7 -0
- package/dist/core/tools/write.d.ts.map +1 -1
- package/dist/core/tools/write.js +64 -9
- package/dist/core/tools/write.js.map +1 -1
- package/dist/core/verification-lifecycle.d.ts +77 -0
- package/dist/core/verification-lifecycle.d.ts.map +1 -0
- package/dist/core/verification-lifecycle.js +146 -0
- package/dist/core/verification-lifecycle.js.map +1 -0
- package/dist/core/workspace/artifacts.d.ts +45 -0
- package/dist/core/workspace/artifacts.d.ts.map +1 -0
- package/dist/core/workspace/artifacts.js +165 -0
- package/dist/core/workspace/artifacts.js.map +1 -0
- package/dist/core/workspace/comparison.d.ts +27 -0
- package/dist/core/workspace/comparison.d.ts.map +1 -0
- package/dist/core/workspace/comparison.js +94 -0
- package/dist/core/workspace/comparison.js.map +1 -0
- package/dist/core/workspace/git-observer.d.ts +48 -0
- package/dist/core/workspace/git-observer.d.ts.map +1 -0
- package/dist/core/workspace/git-observer.js +346 -0
- package/dist/core/workspace/git-observer.js.map +1 -0
- package/dist/core/workspace/projection.d.ts +3 -0
- package/dist/core/workspace/projection.d.ts.map +1 -0
- package/dist/core/workspace/projection.js +56 -0
- package/dist/core/workspace/projection.js.map +1 -0
- package/dist/core/workspace/state.d.ts +133 -0
- package/dist/core/workspace/state.d.ts.map +1 -0
- package/dist/core/workspace/state.js +96 -0
- package/dist/core/workspace/state.js.map +1 -0
- package/dist/extensions/llama/client.d.ts +6 -0
- package/dist/extensions/llama/client.d.ts.map +1 -1
- package/dist/extensions/llama/client.js +7 -0
- package/dist/extensions/llama/client.js.map +1 -1
- package/dist/extensions/llama/provider.d.ts +3 -1
- package/dist/extensions/llama/provider.d.ts.map +1 -1
- package/dist/extensions/llama/provider.js +27 -6
- package/dist/extensions/llama/provider.js.map +1 -1
- package/dist/index.d.ts +4 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -0
- package/dist/index.js.map +1 -1
- package/dist/main.d.ts +3 -1
- package/dist/main.d.ts.map +1 -1
- package/dist/main.js +41 -9
- package/dist/main.js.map +1 -1
- package/dist/modes/acp/server.d.ts +55 -0
- package/dist/modes/acp/server.d.ts.map +1 -0
- package/dist/modes/acp/server.js +244 -0
- package/dist/modes/acp/server.js.map +1 -0
- package/dist/modes/acp/translate.d.ts +37 -0
- package/dist/modes/acp/translate.d.ts.map +1 -0
- package/dist/modes/acp/translate.js +50 -0
- package/dist/modes/acp/translate.js.map +1 -0
- package/dist/modes/index.d.ts +1 -0
- package/dist/modes/index.d.ts.map +1 -1
- package/dist/modes/index.js +1 -0
- package/dist/modes/index.js.map +1 -1
- package/dist/modes/interactive/components/assistant-message.d.ts +29 -0
- package/dist/modes/interactive/components/assistant-message.d.ts.map +1 -1
- package/dist/modes/interactive/components/assistant-message.js +182 -23
- package/dist/modes/interactive/components/assistant-message.js.map +1 -1
- package/dist/modes/interactive/components/bash-execution.d.ts +7 -0
- package/dist/modes/interactive/components/bash-execution.d.ts.map +1 -1
- package/dist/modes/interactive/components/bash-execution.js +20 -0
- package/dist/modes/interactive/components/bash-execution.js.map +1 -1
- package/dist/modes/interactive/components/custom-editor.d.ts +9 -4
- package/dist/modes/interactive/components/custom-editor.d.ts.map +1 -1
- package/dist/modes/interactive/components/custom-editor.js +20 -9
- package/dist/modes/interactive/components/custom-editor.js.map +1 -1
- package/dist/modes/interactive/components/extension-selector.d.ts +7 -1
- package/dist/modes/interactive/components/extension-selector.d.ts.map +1 -1
- package/dist/modes/interactive/components/extension-selector.js +7 -3
- package/dist/modes/interactive/components/extension-selector.js.map +1 -1
- package/dist/modes/interactive/components/footer.d.ts +19 -0
- package/dist/modes/interactive/components/footer.d.ts.map +1 -1
- package/dist/modes/interactive/components/footer.js +70 -10
- package/dist/modes/interactive/components/footer.js.map +1 -1
- package/dist/modes/interactive/components/model-selector.d.ts.map +1 -1
- package/dist/modes/interactive/components/model-selector.js +6 -6
- package/dist/modes/interactive/components/model-selector.js.map +1 -1
- package/dist/modes/interactive/components/permission-preview.d.ts +15 -0
- package/dist/modes/interactive/components/permission-preview.d.ts.map +1 -0
- package/dist/modes/interactive/components/permission-preview.js +33 -0
- package/dist/modes/interactive/components/permission-preview.js.map +1 -0
- package/dist/modes/interactive/components/session-selector.d.ts.map +1 -1
- package/dist/modes/interactive/components/session-selector.js +2 -2
- package/dist/modes/interactive/components/session-selector.js.map +1 -1
- package/dist/modes/interactive/components/settings-selector.d.ts +2 -0
- package/dist/modes/interactive/components/settings-selector.d.ts.map +1 -1
- package/dist/modes/interactive/components/settings-selector.js +10 -0
- package/dist/modes/interactive/components/settings-selector.js.map +1 -1
- package/dist/modes/interactive/components/tool-execution.d.ts +23 -1
- package/dist/modes/interactive/components/tool-execution.d.ts.map +1 -1
- package/dist/modes/interactive/components/tool-execution.js +64 -10
- package/dist/modes/interactive/components/tool-execution.js.map +1 -1
- package/dist/modes/interactive/interactive-mode.d.ts +34 -0
- package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
- package/dist/modes/interactive/interactive-mode.js +231 -26
- package/dist/modes/interactive/interactive-mode.js.map +1 -1
- package/dist/modes/interactive/theme/theme.d.ts +12 -0
- package/dist/modes/interactive/theme/theme.d.ts.map +1 -1
- package/dist/modes/interactive/theme/theme.js +15 -5
- package/dist/modes/interactive/theme/theme.js.map +1 -1
- package/dist/modes/print-mode.d.ts.map +1 -1
- package/dist/modes/print-mode.js +27 -12
- package/dist/modes/print-mode.js.map +1 -1
- package/dist/modes/rpc/rpc-client.d.ts +7 -0
- package/dist/modes/rpc/rpc-client.d.ts.map +1 -1
- package/dist/modes/rpc/rpc-client.js +7 -0
- package/dist/modes/rpc/rpc-client.js.map +1 -1
- package/dist/modes/rpc/rpc-mode.d.ts.map +1 -1
- package/dist/modes/rpc/rpc-mode.js +3 -0
- package/dist/modes/rpc/rpc-mode.js.map +1 -1
- package/dist/modes/rpc/rpc-types.d.ts +12 -0
- package/dist/modes/rpc/rpc-types.d.ts.map +1 -1
- package/dist/modes/rpc/rpc-types.js.map +1 -1
- package/dist/package-manager-cli.d.ts.map +1 -1
- package/dist/package-manager-cli.js +0 -6
- package/dist/package-manager-cli.js.map +1 -1
- package/dist/utils/shell.d.ts.map +1 -1
- package/dist/utils/shell.js +6 -4
- package/dist/utils/shell.js.map +1 -1
- package/dist/utils/version-check.d.ts.map +1 -1
- package/dist/utils/version-check.js +6 -4
- package/dist/utils/version-check.js.map +1 -1
- package/docs/compaction.md +4 -0
- package/docs/custom-provider.md +7 -7
- package/docs/environment-variables.md +1 -1
- package/docs/extensions.md +18 -0
- package/docs/keybindings.md +1 -1
- package/docs/quickstart.md +1 -1
- package/docs/rpc.md +23 -0
- package/docs/sdk.md +25 -0
- package/docs/settings.md +12 -4
- package/docs/terminal-setup.md +12 -0
- package/docs/usage.md +4 -3
- package/examples/extensions/built-in-tool-renderer.ts +8 -4
- package/examples/extensions/custom-provider-anthropic/package-lock.json +2 -2
- package/examples/extensions/custom-provider-anthropic/package.json +1 -1
- package/examples/extensions/custom-provider-gitlab-duo/package.json +1 -1
- package/examples/extensions/git-checkpoint.ts +24 -36
- package/examples/extensions/gondolin/package-lock.json +2 -2
- package/examples/extensions/gondolin/package.json +1 -1
- package/examples/extensions/minimal-mode.ts +2 -2
- package/examples/extensions/permission-gate.ts +1 -1
- package/examples/extensions/plan-mode/index.ts +1 -1
- package/examples/extensions/rpc-demo.ts +1 -1
- package/examples/extensions/sandbox/package-lock.json +2 -2
- package/examples/extensions/sandbox/package.json +1 -1
- package/examples/extensions/with-deps/package-lock.json +2 -2
- package/examples/extensions/with-deps/package.json +1 -1
- package/npm-shrinkwrap.json +194 -46
- package/package.json +14 -12
- package/dist/server/create-harness.d.ts +0 -26
- package/dist/server/create-harness.d.ts.map +0 -1
- package/dist/server/create-harness.js +0 -107
- package/dist/server/create-harness.js.map +0 -1
|
@@ -60,6 +60,46 @@ export interface AppliedEditsResult {
|
|
|
60
60
|
* Unicode quotes/dashes normalized to ASCII).
|
|
61
61
|
*/
|
|
62
62
|
export declare function fuzzyFindText(content: string, oldText: string): FuzzyMatchResult;
|
|
63
|
+
/**
|
|
64
|
+
* Explicit budgets for the advisory edit-failure diagnostics below. Diagnostics
|
|
65
|
+
* are a best-effort hint, never an apply path, so every dimension of the scan
|
|
66
|
+
* is bounded and the ordinary failure is returned untouched when a bound is
|
|
67
|
+
* exceeded (spec 2026-09-01-tool-reliability-and-execution-budgets.md § 2).
|
|
68
|
+
*/
|
|
69
|
+
export declare const EDIT_DIAGNOSTIC_MAX_FILE_BYTES = 1048576;
|
|
70
|
+
export declare const EDIT_DIAGNOSTIC_MAX_TARGET_BYTES = 65536;
|
|
71
|
+
export declare const EDIT_DIAGNOSTIC_MAX_CANDIDATES = 3;
|
|
72
|
+
export declare const EDIT_DIAGNOSTIC_MAX_SCAN_WINDOWS = 20000;
|
|
73
|
+
export declare const EDIT_DIAGNOSTIC_MAX_OCCURRENCES = 5;
|
|
74
|
+
export declare const EDIT_DIAGNOSTIC_SNIPPET_LINE_CHARS = 160;
|
|
75
|
+
export interface EditCandidateLocation {
|
|
76
|
+
/** 1-based, inclusive. */
|
|
77
|
+
startLine: number;
|
|
78
|
+
/** 1-based, inclusive. */
|
|
79
|
+
endLine: number;
|
|
80
|
+
/** Bigram Dice similarity against the failed target, rounded to two decimals. */
|
|
81
|
+
similarity: number;
|
|
82
|
+
/** Bounded snippet of the candidate source lines, each line-numbered. */
|
|
83
|
+
snippet: string;
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* Advisory diagnostics attached to a failed edit match. These describe where a
|
|
87
|
+
* target almost matched or where duplicates live; they are never a replacement
|
|
88
|
+
* location and must never be applied.
|
|
89
|
+
*/
|
|
90
|
+
export interface EditFailureDiagnostics {
|
|
91
|
+
kind: "missing-match" | "duplicate-match";
|
|
92
|
+
candidates: EditCandidateLocation[];
|
|
93
|
+
/** 1-based source lines of up to EDIT_DIAGNOSTIC_MAX_OCCURRENCES duplicates. */
|
|
94
|
+
occurrenceLines: number[];
|
|
95
|
+
omittedOccurrences: number;
|
|
96
|
+
/** True when the scan stopped at a work or time bound before finishing. */
|
|
97
|
+
scanTruncated: boolean;
|
|
98
|
+
/** Set when scanning was unsafe or oversized; no candidates are reported. */
|
|
99
|
+
unavailableReason?: string;
|
|
100
|
+
}
|
|
101
|
+
/** Render advisory diagnostics as an error-message appendix. Empty when there is nothing to say. */
|
|
102
|
+
export declare function formatEditFailureDiagnostics(diagnostics: EditFailureDiagnostics | undefined): string;
|
|
63
103
|
/**
|
|
64
104
|
* Apply one or more exact-text replacements to LF-normalized content.
|
|
65
105
|
*
|
|
@@ -92,6 +132,14 @@ export interface EditDiffError {
|
|
|
92
132
|
* Used for preview rendering in the TUI before the tool executes.
|
|
93
133
|
*/
|
|
94
134
|
export declare function computeEditsDiff(path: string, edits: Edit[], cwd: string): Promise<EditDiffResult | EditDiffError>;
|
|
135
|
+
/**
|
|
136
|
+
* The half of `computeEditsDiff` that does not choose which bytes to read.
|
|
137
|
+
*
|
|
138
|
+
* The permission preview must describe the operation the gate authorized rather
|
|
139
|
+
* than whatever the pathname resolves to a second time, so it supplies content
|
|
140
|
+
* read through the prepared operation and calls this (ADR 0029).
|
|
141
|
+
*/
|
|
142
|
+
export declare function computeEditsDiffFromContent(path: string, rawContent: string, edits: Edit[]): EditDiffResult | EditDiffError;
|
|
95
143
|
/**
|
|
96
144
|
* Compute the diff for a single edit operation without applying it.
|
|
97
145
|
* Kept as a convenience wrapper for single-edit callers.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"edit-diff.d.ts","sourceRoot":"","sources":["../../../src/core/tools/edit-diff.ts"],"names":[],"mappings":"AAAA;;GAEG;AAQH,wBAAgB,gBAAgB,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAM/D;AAED,wBAAgB,aAAa,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAElD;AAED,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,IAAI,GAAG,MAAM,CAE9E;AAED;;;;;;GAMG;AACH,wBAAgB,sBAAsB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAqB3D;AAWD,UAAU,WAAW;IACpB,SAAS,EAAE,MAAM,CAAC;IAClB,UAAU,EAAE,MAAM,CAAC;IACnB,WAAW,EAAE,MAAM,CAAC;IACpB,OAAO,EAAE,MAAM,CAAC;CAChB;AAED,KAAK,eAAe,GAAG,IAAI,CAAC,WAAW,EAAE,YAAY,GAAG,aAAa,GAAG,SAAS,CAAC,CAAC;AAiDnF;;;;;;;;;GASG;AACH,wBAAgB,yCAAyC,CACxD,eAAe,EAAE,MAAM,EACvB,WAAW,EAAE,MAAM,EACnB,YAAY,EAAE,eAAe,EAAE,GAC7B,MAAM,CAqCR;AAED,MAAM,WAAW,gBAAgB;IAChC,gCAAgC;IAChC,KAAK,EAAE,OAAO,CAAC;IACf,4FAA4F;IAC5F,KAAK,EAAE,MAAM,CAAC;IACd,iCAAiC;IACjC,WAAW,EAAE,MAAM,CAAC;IACpB,4DAA4D;IAC5D,cAAc,EAAE,OAAO,CAAC;IACxB;;;OAGG;IACH,qBAAqB,EAAE,MAAM,CAAC;CAC9B;AAED,MAAM,WAAW,IAAI;IACpB,OAAO,EAAE,MAAM,CAAC;IAChB,OAAO,EAAE,MAAM,CAAC;CAChB;AAED,MAAM,WAAW,kBAAkB;IAClC,WAAW,EAAE,MAAM,CAAC;IACpB,UAAU,EAAE,MAAM,CAAC;CACnB;AAED;;;;;GAKG;AACH,wBAAgB,aAAa,CAAC,OAAO,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,gBAAgB,CAsChF;AA8CD;;;;;;;;GAQG;AACH,wBAAgB,6BAA6B,CAC5C,iBAAiB,EAAE,MAAM,EACzB,KAAK,EAAE,IAAI,EAAE,EACb,IAAI,EAAE,MAAM,GACV,kBAAkB,CA0DpB;AAED,yCAAyC;AACzC,wBAAgB,oBAAoB,CAAC,IAAI,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,EAAE,YAAY,SAAI,GAAG,MAAM,CAKnH;AAED;;;GAGG;AACH,wBAAgB,kBAAkB,CACjC,UAAU,EAAE,MAAM,EAClB,UAAU,EAAE,MAAM,EAClB,YAAY,SAAI,GACd;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,gBAAgB,EAAE,MAAM,GAAG,SAAS,CAAA;CAAE,CAuHxD;AAED,MAAM,WAAW,cAAc;IAC9B,IAAI,EAAE,MAAM,CAAC;IACb,gBAAgB,EAAE,MAAM,GAAG,SAAS,CAAC;CACrC;AAED,MAAM,WAAW,aAAa;IAC7B,KAAK,EAAE,MAAM,CAAC;CACd;AAED;;;GAGG;AACH,wBAAsB,gBAAgB,CACrC,IAAI,EAAE,MAAM,EACZ,KAAK,EAAE,IAAI,EAAE,EACb,GAAG,EAAE,MAAM,GACT,OAAO,CAAC,cAAc,GAAG,aAAa,CAAC,CAyBzC;AAED;;;GAGG;AACH,wBAAsB,eAAe,CACpC,IAAI,EAAE,MAAM,EACZ,OAAO,EAAE,MAAM,EACf,OAAO,EAAE,MAAM,EACf,GAAG,EAAE,MAAM,GACT,OAAO,CAAC,cAAc,GAAG,aAAa,CAAC,CAEzC","sourcesContent":["/**\n * Shared diff computation utilities for the edit and similar tools.\n */\n\nimport * as Diff from \"diff\";\nimport { constants } from \"fs\";\nimport { access, readFile } from \"fs/promises\";\nimport { splitBom } from \"../../utils/text.ts\";\nimport { resolveToCwd } from \"./path-utils.ts\";\n\nexport function detectLineEnding(content: string): \"\\r\\n\" | \"\\n\" {\n\tconst crlfIdx = content.indexOf(\"\\r\\n\");\n\tconst lfIdx = content.indexOf(\"\\n\");\n\tif (lfIdx === -1) return \"\\n\";\n\tif (crlfIdx === -1) return \"\\n\";\n\treturn crlfIdx < lfIdx ? \"\\r\\n\" : \"\\n\";\n}\n\nexport function normalizeToLF(text: string): string {\n\treturn text.replace(/\\r\\n/g, \"\\n\").replace(/\\r/g, \"\\n\");\n}\n\nexport function restoreLineEndings(text: string, ending: \"\\r\\n\" | \"\\n\"): string {\n\treturn ending === \"\\r\\n\" ? text.replace(/\\n/g, \"\\r\\n\") : text;\n}\n\n/**\n * Normalize text for fuzzy matching. Applies progressive transformations:\n * - Strip trailing whitespace from each line\n * - Normalize smart quotes to ASCII equivalents\n * - Normalize Unicode dashes/hyphens to ASCII hyphen\n * - Normalize special Unicode spaces to regular space\n */\nexport function normalizeForFuzzyMatch(text: string): string {\n\treturn (\n\t\ttext\n\t\t\t.normalize(\"NFKC\")\n\t\t\t// Strip trailing whitespace per line\n\t\t\t.split(\"\\n\")\n\t\t\t.map((line) => line.trimEnd())\n\t\t\t.join(\"\\n\")\n\t\t\t// Smart single quotes → '\n\t\t\t.replace(/[\\u2018\\u2019\\u201A\\u201B]/g, \"'\")\n\t\t\t// Smart double quotes → \"\n\t\t\t.replace(/[\\u201C\\u201D\\u201E\\u201F]/g, '\"')\n\t\t\t// Various dashes/hyphens → -\n\t\t\t// U+2010 hyphen, U+2011 non-breaking hyphen, U+2012 figure dash,\n\t\t\t// U+2013 en-dash, U+2014 em-dash, U+2015 horizontal bar, U+2212 minus\n\t\t\t.replace(/[\\u2010\\u2011\\u2012\\u2013\\u2014\\u2015\\u2212]/g, \"-\")\n\t\t\t// Special spaces → regular space\n\t\t\t// U+00A0 NBSP, U+2002-U+200A various spaces, U+202F narrow NBSP,\n\t\t\t// U+205F medium math space, U+3000 ideographic space\n\t\t\t.replace(/[\\u00A0\\u2002-\\u200A\\u202F\\u205F\\u3000]/g, \" \")\n\t);\n}\n\nfunction splitLinesWithEndings(content: string): string[] {\n\treturn content.match(/[^\\n]*\\n|[^\\n]+/g) ?? [];\n}\n\ninterface LineSpan {\n\tstart: number;\n\tend: number;\n}\n\ninterface MatchedEdit {\n\teditIndex: number;\n\tmatchIndex: number;\n\tmatchLength: number;\n\tnewText: string;\n}\n\ntype TextReplacement = Pick<MatchedEdit, \"matchIndex\" | \"matchLength\" | \"newText\">;\n\nfunction getLineSpans(content: string): LineSpan[] {\n\tlet offset = 0;\n\treturn splitLinesWithEndings(content).map((line) => {\n\t\tconst span = { start: offset, end: offset + line.length };\n\t\toffset = span.end;\n\t\treturn span;\n\t});\n}\n\nfunction getReplacementLineRange(lines: LineSpan[], replacement: TextReplacement) {\n\tconst replacementStart = replacement.matchIndex;\n\tconst replacementEnd = replacement.matchIndex + replacement.matchLength;\n\n\tlet startLine = -1;\n\tfor (let i = 0; i < lines.length; i++) {\n\t\tconst line = lines[i];\n\t\tif (replacementStart >= line.start && replacementStart < line.end) {\n\t\t\tstartLine = i;\n\t\t\tbreak;\n\t\t}\n\t}\n\tif (startLine === -1) {\n\t\tthrow new Error(\"Replacement range is outside the base content.\");\n\t}\n\n\tlet endLine = startLine;\n\twhile (endLine < lines.length && lines[endLine].end < replacementEnd) {\n\t\tendLine++;\n\t}\n\tif (endLine >= lines.length) {\n\t\tthrow new Error(\"Replacement range is outside the base content.\");\n\t}\n\n\treturn { startLine, endLine: endLine + 1 };\n}\n\nfunction applyReplacements(content: string, replacements: TextReplacement[], offset = 0): string {\n\tlet result = content;\n\tfor (let i = replacements.length - 1; i >= 0; i--) {\n\t\tconst replacement = replacements[i];\n\t\tconst matchIndex = replacement.matchIndex - offset;\n\t\tresult =\n\t\t\tresult.substring(0, matchIndex) + replacement.newText + result.substring(matchIndex + replacement.matchLength);\n\t}\n\treturn result;\n}\n\n/**\n * Apply replacements matched against `baseContent` to `originalContent` while\n * preserving unchanged line blocks from the original.\n *\n * This is useful when `baseContent` is a normalized view of the original. Each\n * replacement is widened to the lines it actually touches, those touched lines\n * are rewritten from the normalized base, and all other lines are copied back\n * from `originalContent`. The actual replacement ranges drive preservation so\n * duplicate normalized lines cannot be aligned to the wrong occurrence.\n */\nexport function applyReplacementsPreservingUnchangedLines(\n\toriginalContent: string,\n\tbaseContent: string,\n\treplacements: TextReplacement[],\n): string {\n\tconst originalLines = splitLinesWithEndings(originalContent);\n\tconst baseLines = getLineSpans(baseContent);\n\tif (originalLines.length !== baseLines.length) {\n\t\tthrow new Error(\"Cannot preserve unchanged lines because the base content has a different line count.\");\n\t}\n\n\tconst groups: Array<{ startLine: number; endLine: number; replacements: TextReplacement[] }> = [];\n\tconst sortedReplacements = [...replacements].sort((a, b) => a.matchIndex - b.matchIndex);\n\tfor (const replacement of sortedReplacements) {\n\t\tconst range = getReplacementLineRange(baseLines, replacement);\n\t\tconst current = groups[groups.length - 1];\n\t\tif (current && range.startLine < current.endLine) {\n\t\t\tcurrent.endLine = Math.max(current.endLine, range.endLine);\n\t\t\tcurrent.replacements.push(replacement);\n\t\t\tcontinue;\n\t\t}\n\t\tgroups.push({ ...range, replacements: [replacement] });\n\t}\n\n\tlet originalLineIndex = 0;\n\tlet result = \"\";\n\tfor (const group of groups) {\n\t\tresult += originalLines.slice(originalLineIndex, group.startLine).join(\"\");\n\n\t\tconst groupStartOffset = baseLines[group.startLine].start;\n\t\tconst groupEndOffset = baseLines[group.endLine - 1].end;\n\t\tresult += applyReplacements(\n\t\t\tbaseContent.slice(groupStartOffset, groupEndOffset),\n\t\t\tgroup.replacements,\n\t\t\tgroupStartOffset,\n\t\t);\n\t\toriginalLineIndex = group.endLine;\n\t}\n\tresult += originalLines.slice(originalLineIndex).join(\"\");\n\n\treturn result;\n}\n\nexport interface FuzzyMatchResult {\n\t/** Whether a match was found */\n\tfound: boolean;\n\t/** The index where the match starts (in the content that should be used for replacement) */\n\tindex: number;\n\t/** Length of the matched text */\n\tmatchLength: number;\n\t/** Whether fuzzy matching was used (false = exact match) */\n\tusedFuzzyMatch: boolean;\n\t/**\n\t * The content to use for replacement operations.\n\t * When exact match: original content. When fuzzy match: normalized content.\n\t */\n\tcontentForReplacement: string;\n}\n\nexport interface Edit {\n\toldText: string;\n\tnewText: string;\n}\n\nexport interface AppliedEditsResult {\n\tbaseContent: string;\n\tnewContent: string;\n}\n\n/**\n * Find oldText in content, trying exact match first, then fuzzy match.\n * When fuzzy matching is used, the returned contentForReplacement is the\n * fuzzy-normalized version of the content (trailing whitespace stripped,\n * Unicode quotes/dashes normalized to ASCII).\n */\nexport function fuzzyFindText(content: string, oldText: string): FuzzyMatchResult {\n\t// Try exact match first\n\tconst exactIndex = content.indexOf(oldText);\n\tif (exactIndex !== -1) {\n\t\treturn {\n\t\t\tfound: true,\n\t\t\tindex: exactIndex,\n\t\t\tmatchLength: oldText.length,\n\t\t\tusedFuzzyMatch: false,\n\t\t\tcontentForReplacement: content,\n\t\t};\n\t}\n\n\t// Try fuzzy match - work entirely in normalized space\n\tconst fuzzyContent = normalizeForFuzzyMatch(content);\n\tconst fuzzyOldText = normalizeForFuzzyMatch(oldText);\n\tconst fuzzyIndex = fuzzyContent.indexOf(fuzzyOldText);\n\n\tif (fuzzyIndex === -1) {\n\t\treturn {\n\t\t\tfound: false,\n\t\t\tindex: -1,\n\t\t\tmatchLength: 0,\n\t\t\tusedFuzzyMatch: false,\n\t\t\tcontentForReplacement: content,\n\t\t};\n\t}\n\n\t// When fuzzy matching, return offsets in normalized space. Callers can use\n\t// the normalized content to compute replacements, then decide how much of\n\t// that normalized output should be written back.\n\treturn {\n\t\tfound: true,\n\t\tindex: fuzzyIndex,\n\t\tmatchLength: fuzzyOldText.length,\n\t\tusedFuzzyMatch: true,\n\t\tcontentForReplacement: fuzzyContent,\n\t};\n}\n\nfunction countOccurrences(content: string, oldText: string): number {\n\tconst fuzzyContent = normalizeForFuzzyMatch(content);\n\tconst fuzzyOldText = normalizeForFuzzyMatch(oldText);\n\treturn fuzzyContent.split(fuzzyOldText).length - 1;\n}\n\nfunction getNotFoundError(path: string, editIndex: number, totalEdits: number): Error {\n\tif (totalEdits === 1) {\n\t\treturn new Error(\n\t\t\t`Could not find the exact text in ${path}. The old text must match exactly including all whitespace and newlines.`,\n\t\t);\n\t}\n\treturn new Error(\n\t\t`Could not find edits[${editIndex}] in ${path}. The oldText must match exactly including all whitespace and newlines.`,\n\t);\n}\n\nfunction getDuplicateError(path: string, editIndex: number, totalEdits: number, occurrences: number): Error {\n\tif (totalEdits === 1) {\n\t\treturn new Error(\n\t\t\t`Found ${occurrences} occurrences of the text in ${path}. The text must be unique. Please provide more context to make it unique.`,\n\t\t);\n\t}\n\treturn new Error(\n\t\t`Found ${occurrences} occurrences of edits[${editIndex}] in ${path}. Each oldText must be unique. Please provide more context to make it unique.`,\n\t);\n}\n\nfunction getEmptyOldTextError(path: string, editIndex: number, totalEdits: number): Error {\n\tif (totalEdits === 1) {\n\t\treturn new Error(`oldText must not be empty in ${path}.`);\n\t}\n\treturn new Error(`edits[${editIndex}].oldText must not be empty in ${path}.`);\n}\n\nfunction getNoChangeError(path: string, totalEdits: number): Error {\n\tif (totalEdits === 1) {\n\t\treturn new Error(\n\t\t\t`No changes made to ${path}. The replacement produced identical content. This might indicate an issue with special characters or the text not existing as expected.`,\n\t\t);\n\t}\n\treturn new Error(`No changes made to ${path}. The replacements produced identical content.`);\n}\n\n/**\n * Apply one or more exact-text replacements to LF-normalized content.\n *\n * All edits are matched against the same original content. Replacements are\n * then applied in reverse order so offsets remain stable. If any edit needs\n * fuzzy matching, the operation runs in fuzzy-normalized content space and then\n * overlays those line-level changes onto the original content so unchanged line\n * blocks keep their original bytes.\n */\nexport function applyEditsToNormalizedContent(\n\tnormalizedContent: string,\n\tedits: Edit[],\n\tpath: string,\n): AppliedEditsResult {\n\tconst normalizedEdits = edits.map((edit) => ({\n\t\toldText: normalizeToLF(edit.oldText),\n\t\tnewText: normalizeToLF(edit.newText),\n\t}));\n\n\tfor (let i = 0; i < normalizedEdits.length; i++) {\n\t\tif (normalizedEdits[i].oldText.length === 0) {\n\t\t\tthrow getEmptyOldTextError(path, i, normalizedEdits.length);\n\t\t}\n\t}\n\n\tconst initialMatches = normalizedEdits.map((edit) => fuzzyFindText(normalizedContent, edit.oldText));\n\tconst usedFuzzyMatch = initialMatches.some((match) => match.usedFuzzyMatch);\n\tconst replacementBaseContent = usedFuzzyMatch ? normalizeForFuzzyMatch(normalizedContent) : normalizedContent;\n\n\tconst matchedEdits: MatchedEdit[] = [];\n\tfor (let i = 0; i < normalizedEdits.length; i++) {\n\t\tconst edit = normalizedEdits[i];\n\t\tconst matchResult = fuzzyFindText(replacementBaseContent, edit.oldText);\n\t\tif (!matchResult.found) {\n\t\t\tthrow getNotFoundError(path, i, normalizedEdits.length);\n\t\t}\n\n\t\tconst occurrences = countOccurrences(replacementBaseContent, edit.oldText);\n\t\tif (occurrences > 1) {\n\t\t\tthrow getDuplicateError(path, i, normalizedEdits.length, occurrences);\n\t\t}\n\n\t\tmatchedEdits.push({\n\t\t\teditIndex: i,\n\t\t\tmatchIndex: matchResult.index,\n\t\t\tmatchLength: matchResult.matchLength,\n\t\t\tnewText: edit.newText,\n\t\t});\n\t}\n\n\tmatchedEdits.sort((a, b) => a.matchIndex - b.matchIndex);\n\tfor (let i = 1; i < matchedEdits.length; i++) {\n\t\tconst previous = matchedEdits[i - 1];\n\t\tconst current = matchedEdits[i];\n\t\tif (previous.matchIndex + previous.matchLength > current.matchIndex) {\n\t\t\tthrow new Error(\n\t\t\t\t`edits[${previous.editIndex}] and edits[${current.editIndex}] overlap in ${path}. Merge them into one edit or target disjoint regions.`,\n\t\t\t);\n\t\t}\n\t}\n\n\tconst baseContent = normalizedContent;\n\tconst newContent = usedFuzzyMatch\n\t\t? applyReplacementsPreservingUnchangedLines(normalizedContent, replacementBaseContent, matchedEdits)\n\t\t: applyReplacements(replacementBaseContent, matchedEdits);\n\n\tif (baseContent === newContent) {\n\t\tthrow getNoChangeError(path, normalizedEdits.length);\n\t}\n\n\treturn { baseContent, newContent };\n}\n\n/** Generate a standard unified patch. */\nexport function generateUnifiedPatch(path: string, oldContent: string, newContent: string, contextLines = 4): string {\n\treturn Diff.createTwoFilesPatch(path, path, oldContent, newContent, undefined, undefined, {\n\t\tcontext: contextLines,\n\t\theaderOptions: Diff.FILE_HEADERS_ONLY,\n\t});\n}\n\n/**\n * Generate a display-oriented diff string with line numbers and context.\n * Returns both the diff string and the first changed line number (in the new file).\n */\nexport function generateDiffString(\n\toldContent: string,\n\tnewContent: string,\n\tcontextLines = 4,\n): { diff: string; firstChangedLine: number | undefined } {\n\tconst parts = Diff.diffLines(oldContent, newContent);\n\tconst output: string[] = [];\n\n\tconst oldLines = oldContent.split(\"\\n\");\n\tconst newLines = newContent.split(\"\\n\");\n\tconst maxLineNum = Math.max(oldLines.length, newLines.length);\n\tconst lineNumWidth = String(maxLineNum).length;\n\n\tlet oldLineNum = 1;\n\tlet newLineNum = 1;\n\tlet lastWasChange = false;\n\tlet firstChangedLine: number | undefined;\n\n\tfor (let i = 0; i < parts.length; i++) {\n\t\tconst part = parts[i];\n\t\tconst raw = part.value.split(\"\\n\");\n\t\tif (raw[raw.length - 1] === \"\") {\n\t\t\traw.pop();\n\t\t}\n\n\t\tif (part.added || part.removed) {\n\t\t\t// Capture the first changed line (in the new file)\n\t\t\tif (firstChangedLine === undefined) {\n\t\t\t\tfirstChangedLine = newLineNum;\n\t\t\t}\n\n\t\t\t// Show the change\n\t\t\tfor (const line of raw) {\n\t\t\t\tif (part.added) {\n\t\t\t\t\tconst lineNum = String(newLineNum).padStart(lineNumWidth, \" \");\n\t\t\t\t\toutput.push(`+${lineNum} ${line}`);\n\t\t\t\t\tnewLineNum++;\n\t\t\t\t} else {\n\t\t\t\t\t// removed\n\t\t\t\t\tconst lineNum = String(oldLineNum).padStart(lineNumWidth, \" \");\n\t\t\t\t\toutput.push(`-${lineNum} ${line}`);\n\t\t\t\t\toldLineNum++;\n\t\t\t\t}\n\t\t\t}\n\t\t\tlastWasChange = true;\n\t\t} else {\n\t\t\t// Context lines - only show a few before/after changes\n\t\t\tconst nextPartIsChange = i < parts.length - 1 && (parts[i + 1].added || parts[i + 1].removed);\n\t\t\tconst hasLeadingChange = lastWasChange;\n\t\t\tconst hasTrailingChange = nextPartIsChange;\n\n\t\t\tif (hasLeadingChange && hasTrailingChange) {\n\t\t\t\tif (raw.length <= contextLines * 2) {\n\t\t\t\t\tfor (const line of raw) {\n\t\t\t\t\t\tconst lineNum = String(oldLineNum).padStart(lineNumWidth, \" \");\n\t\t\t\t\t\toutput.push(` ${lineNum} ${line}`);\n\t\t\t\t\t\toldLineNum++;\n\t\t\t\t\t\tnewLineNum++;\n\t\t\t\t\t}\n\t\t\t\t} else {\n\t\t\t\t\tconst leadingLines = raw.slice(0, contextLines);\n\t\t\t\t\tconst trailingLines = raw.slice(raw.length - contextLines);\n\t\t\t\t\tconst skippedLines = raw.length - leadingLines.length - trailingLines.length;\n\n\t\t\t\t\tfor (const line of leadingLines) {\n\t\t\t\t\t\tconst lineNum = String(oldLineNum).padStart(lineNumWidth, \" \");\n\t\t\t\t\t\toutput.push(` ${lineNum} ${line}`);\n\t\t\t\t\t\toldLineNum++;\n\t\t\t\t\t\tnewLineNum++;\n\t\t\t\t\t}\n\n\t\t\t\t\toutput.push(` ${\"\".padStart(lineNumWidth, \" \")} ...`);\n\t\t\t\t\toldLineNum += skippedLines;\n\t\t\t\t\tnewLineNum += skippedLines;\n\n\t\t\t\t\tfor (const line of trailingLines) {\n\t\t\t\t\t\tconst lineNum = String(oldLineNum).padStart(lineNumWidth, \" \");\n\t\t\t\t\t\toutput.push(` ${lineNum} ${line}`);\n\t\t\t\t\t\toldLineNum++;\n\t\t\t\t\t\tnewLineNum++;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t} else if (hasLeadingChange) {\n\t\t\t\tconst shownLines = raw.slice(0, contextLines);\n\t\t\t\tconst skippedLines = raw.length - shownLines.length;\n\n\t\t\t\tfor (const line of shownLines) {\n\t\t\t\t\tconst lineNum = String(oldLineNum).padStart(lineNumWidth, \" \");\n\t\t\t\t\toutput.push(` ${lineNum} ${line}`);\n\t\t\t\t\toldLineNum++;\n\t\t\t\t\tnewLineNum++;\n\t\t\t\t}\n\n\t\t\t\tif (skippedLines > 0) {\n\t\t\t\t\toutput.push(` ${\"\".padStart(lineNumWidth, \" \")} ...`);\n\t\t\t\t\toldLineNum += skippedLines;\n\t\t\t\t\tnewLineNum += skippedLines;\n\t\t\t\t}\n\t\t\t} else if (hasTrailingChange) {\n\t\t\t\tconst skippedLines = Math.max(0, raw.length - contextLines);\n\t\t\t\tif (skippedLines > 0) {\n\t\t\t\t\toutput.push(` ${\"\".padStart(lineNumWidth, \" \")} ...`);\n\t\t\t\t\toldLineNum += skippedLines;\n\t\t\t\t\tnewLineNum += skippedLines;\n\t\t\t\t}\n\n\t\t\t\tfor (const line of raw.slice(skippedLines)) {\n\t\t\t\t\tconst lineNum = String(oldLineNum).padStart(lineNumWidth, \" \");\n\t\t\t\t\toutput.push(` ${lineNum} ${line}`);\n\t\t\t\t\toldLineNum++;\n\t\t\t\t\tnewLineNum++;\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\t// Skip these context lines entirely\n\t\t\t\toldLineNum += raw.length;\n\t\t\t\tnewLineNum += raw.length;\n\t\t\t}\n\n\t\t\tlastWasChange = false;\n\t\t}\n\t}\n\n\treturn { diff: output.join(\"\\n\"), firstChangedLine };\n}\n\nexport interface EditDiffResult {\n\tdiff: string;\n\tfirstChangedLine: number | undefined;\n}\n\nexport interface EditDiffError {\n\terror: string;\n}\n\n/**\n * Compute the diff for one or more edit operations without applying them.\n * Used for preview rendering in the TUI before the tool executes.\n */\nexport async function computeEditsDiff(\n\tpath: string,\n\tedits: Edit[],\n\tcwd: string,\n): Promise<EditDiffResult | EditDiffError> {\n\tconst absolutePath = resolveToCwd(path, cwd);\n\n\ttry {\n\t\t// Check if file exists and is readable\n\t\ttry {\n\t\t\tawait access(absolutePath, constants.R_OK);\n\t\t} catch (error: unknown) {\n\t\t\tconst errorMessage = error instanceof Error && \"code\" in error ? `Error code: ${error.code}` : String(error);\n\t\t\treturn { error: `Could not edit file: ${path}. ${errorMessage}.` };\n\t\t}\n\n\t\t// Read the file\n\t\tconst rawContent = await readFile(absolutePath, \"utf-8\");\n\n\t\t// Strip BOM before matching (LLM won't include invisible BOM in oldText)\n\t\tconst { text: content } = splitBom(rawContent);\n\t\tconst normalizedContent = normalizeToLF(content);\n\t\tconst { baseContent, newContent } = applyEditsToNormalizedContent(normalizedContent, edits, path);\n\n\t\t// Generate the diff\n\t\treturn generateDiffString(baseContent, newContent);\n\t} catch (err) {\n\t\treturn { error: err instanceof Error ? err.message : String(err) };\n\t}\n}\n\n/**\n * Compute the diff for a single edit operation without applying it.\n * Kept as a convenience wrapper for single-edit callers.\n */\nexport async function computeEditDiff(\n\tpath: string,\n\toldText: string,\n\tnewText: string,\n\tcwd: string,\n): Promise<EditDiffResult | EditDiffError> {\n\treturn computeEditsDiff(path, [{ oldText, newText }], cwd);\n}\n"]}
|
|
1
|
+
{"version":3,"file":"edit-diff.d.ts","sourceRoot":"","sources":["../../../src/core/tools/edit-diff.ts"],"names":[],"mappings":"AAAA;;GAEG;AAQH,wBAAgB,gBAAgB,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAM/D;AAED,wBAAgB,aAAa,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAElD;AAED,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,IAAI,GAAG,MAAM,CAE9E;AAED;;;;;;GAMG;AACH,wBAAgB,sBAAsB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAqB3D;AAWD,UAAU,WAAW;IACpB,SAAS,EAAE,MAAM,CAAC;IAClB,UAAU,EAAE,MAAM,CAAC;IACnB,WAAW,EAAE,MAAM,CAAC;IACpB,OAAO,EAAE,MAAM,CAAC;CAChB;AAED,KAAK,eAAe,GAAG,IAAI,CAAC,WAAW,EAAE,YAAY,GAAG,aAAa,GAAG,SAAS,CAAC,CAAC;AAiDnF;;;;;;;;;GASG;AACH,wBAAgB,yCAAyC,CACxD,eAAe,EAAE,MAAM,EACvB,WAAW,EAAE,MAAM,EACnB,YAAY,EAAE,eAAe,EAAE,GAC7B,MAAM,CAqCR;AAED,MAAM,WAAW,gBAAgB;IAChC,gCAAgC;IAChC,KAAK,EAAE,OAAO,CAAC;IACf,4FAA4F;IAC5F,KAAK,EAAE,MAAM,CAAC;IACd,iCAAiC;IACjC,WAAW,EAAE,MAAM,CAAC;IACpB,4DAA4D;IAC5D,cAAc,EAAE,OAAO,CAAC;IACxB;;;OAGG;IACH,qBAAqB,EAAE,MAAM,CAAC;CAC9B;AAED,MAAM,WAAW,IAAI;IACpB,OAAO,EAAE,MAAM,CAAC;IAChB,OAAO,EAAE,MAAM,CAAC;CAChB;AAED,MAAM,WAAW,kBAAkB;IAClC,WAAW,EAAE,MAAM,CAAC;IACpB,UAAU,EAAE,MAAM,CAAC;CACnB;AAED;;;;;GAKG;AACH,wBAAgB,aAAa,CAAC,OAAO,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,gBAAgB,CAsChF;AAQD;;;;;GAKG;AACH,eAAO,MAAM,8BAA8B,UAAY,CAAC;AACxD,eAAO,MAAM,gCAAgC,QAAS,CAAC;AACvD,eAAO,MAAM,8BAA8B,IAAI,CAAC;AAChD,eAAO,MAAM,gCAAgC,QAAS,CAAC;AACvD,eAAO,MAAM,+BAA+B,IAAI,CAAC;AACjD,eAAO,MAAM,kCAAkC,MAAM,CAAC;AAMtD,MAAM,WAAW,qBAAqB;IACrC,0BAA0B;IAC1B,SAAS,EAAE,MAAM,CAAC;IAClB,0BAA0B;IAC1B,OAAO,EAAE,MAAM,CAAC;IAChB,iFAAiF;IACjF,UAAU,EAAE,MAAM,CAAC;IACnB,yEAAyE;IACzE,OAAO,EAAE,MAAM,CAAC;CAChB;AAED;;;;GAIG;AACH,MAAM,WAAW,sBAAsB;IACtC,IAAI,EAAE,eAAe,GAAG,iBAAiB,CAAC;IAC1C,UAAU,EAAE,qBAAqB,EAAE,CAAC;IACpC,gFAAgF;IAChF,eAAe,EAAE,MAAM,EAAE,CAAC;IAC1B,kBAAkB,EAAE,MAAM,CAAC;IAC3B,2EAA2E;IAC3E,aAAa,EAAE,OAAO,CAAC;IACvB,6EAA6E;IAC7E,iBAAiB,CAAC,EAAE,MAAM,CAAC;CAC3B;AAoKD,oGAAoG;AACpG,wBAAgB,4BAA4B,CAAC,WAAW,EAAE,sBAAsB,GAAG,SAAS,GAAG,MAAM,CA4BpG;AA6CD;;;;;;;;GAQG;AACH,wBAAgB,6BAA6B,CAC5C,iBAAiB,EAAE,MAAM,EACzB,KAAK,EAAE,IAAI,EAAE,EACb,IAAI,EAAE,MAAM,GACV,kBAAkB,CAoEpB;AAED,yCAAyC;AACzC,wBAAgB,oBAAoB,CAAC,IAAI,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,EAAE,YAAY,SAAI,GAAG,MAAM,CAKnH;AAED;;;GAGG;AACH,wBAAgB,kBAAkB,CACjC,UAAU,EAAE,MAAM,EAClB,UAAU,EAAE,MAAM,EAClB,YAAY,SAAI,GACd;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,gBAAgB,EAAE,MAAM,GAAG,SAAS,CAAA;CAAE,CAuHxD;AAED,MAAM,WAAW,cAAc;IAC9B,IAAI,EAAE,MAAM,CAAC;IACb,gBAAgB,EAAE,MAAM,GAAG,SAAS,CAAC;CACrC;AAED,MAAM,WAAW,aAAa;IAC7B,KAAK,EAAE,MAAM,CAAC;CACd;AAED;;;GAGG;AACH,wBAAsB,gBAAgB,CACrC,IAAI,EAAE,MAAM,EACZ,KAAK,EAAE,IAAI,EAAE,EACb,GAAG,EAAE,MAAM,GACT,OAAO,CAAC,cAAc,GAAG,aAAa,CAAC,CAkBzC;AAED;;;;;;GAMG;AACH,wBAAgB,2BAA2B,CAC1C,IAAI,EAAE,MAAM,EACZ,UAAU,EAAE,MAAM,EAClB,KAAK,EAAE,IAAI,EAAE,GACX,cAAc,GAAG,aAAa,CAUhC;AAED;;;GAGG;AACH,wBAAsB,eAAe,CACpC,IAAI,EAAE,MAAM,EACZ,OAAO,EAAE,MAAM,EACf,OAAO,EAAE,MAAM,EACf,GAAG,EAAE,MAAM,GACT,OAAO,CAAC,cAAc,GAAG,aAAa,CAAC,CAEzC","sourcesContent":["/**\n * Shared diff computation utilities for the edit and similar tools.\n */\n\nimport * as Diff from \"diff\";\nimport { constants } from \"fs\";\nimport { access, readFile } from \"fs/promises\";\nimport { splitBom } from \"../../utils/text.ts\";\nimport { resolveToCwd } from \"./path-utils.ts\";\n\nexport function detectLineEnding(content: string): \"\\r\\n\" | \"\\n\" {\n\tconst crlfIdx = content.indexOf(\"\\r\\n\");\n\tconst lfIdx = content.indexOf(\"\\n\");\n\tif (lfIdx === -1) return \"\\n\";\n\tif (crlfIdx === -1) return \"\\n\";\n\treturn crlfIdx < lfIdx ? \"\\r\\n\" : \"\\n\";\n}\n\nexport function normalizeToLF(text: string): string {\n\treturn text.replace(/\\r\\n/g, \"\\n\").replace(/\\r/g, \"\\n\");\n}\n\nexport function restoreLineEndings(text: string, ending: \"\\r\\n\" | \"\\n\"): string {\n\treturn ending === \"\\r\\n\" ? text.replace(/\\n/g, \"\\r\\n\") : text;\n}\n\n/**\n * Normalize text for fuzzy matching. Applies progressive transformations:\n * - Strip trailing whitespace from each line\n * - Normalize smart quotes to ASCII equivalents\n * - Normalize Unicode dashes/hyphens to ASCII hyphen\n * - Normalize special Unicode spaces to regular space\n */\nexport function normalizeForFuzzyMatch(text: string): string {\n\treturn (\n\t\ttext\n\t\t\t.normalize(\"NFKC\")\n\t\t\t// Strip trailing whitespace per line\n\t\t\t.split(\"\\n\")\n\t\t\t.map((line) => line.trimEnd())\n\t\t\t.join(\"\\n\")\n\t\t\t// Smart single quotes → '\n\t\t\t.replace(/[\\u2018\\u2019\\u201A\\u201B]/g, \"'\")\n\t\t\t// Smart double quotes → \"\n\t\t\t.replace(/[\\u201C\\u201D\\u201E\\u201F]/g, '\"')\n\t\t\t// Various dashes/hyphens → -\n\t\t\t// U+2010 hyphen, U+2011 non-breaking hyphen, U+2012 figure dash,\n\t\t\t// U+2013 en-dash, U+2014 em-dash, U+2015 horizontal bar, U+2212 minus\n\t\t\t.replace(/[\\u2010\\u2011\\u2012\\u2013\\u2014\\u2015\\u2212]/g, \"-\")\n\t\t\t// Special spaces → regular space\n\t\t\t// U+00A0 NBSP, U+2002-U+200A various spaces, U+202F narrow NBSP,\n\t\t\t// U+205F medium math space, U+3000 ideographic space\n\t\t\t.replace(/[\\u00A0\\u2002-\\u200A\\u202F\\u205F\\u3000]/g, \" \")\n\t);\n}\n\nfunction splitLinesWithEndings(content: string): string[] {\n\treturn content.match(/[^\\n]*\\n|[^\\n]+/g) ?? [];\n}\n\ninterface LineSpan {\n\tstart: number;\n\tend: number;\n}\n\ninterface MatchedEdit {\n\teditIndex: number;\n\tmatchIndex: number;\n\tmatchLength: number;\n\tnewText: string;\n}\n\ntype TextReplacement = Pick<MatchedEdit, \"matchIndex\" | \"matchLength\" | \"newText\">;\n\nfunction getLineSpans(content: string): LineSpan[] {\n\tlet offset = 0;\n\treturn splitLinesWithEndings(content).map((line) => {\n\t\tconst span = { start: offset, end: offset + line.length };\n\t\toffset = span.end;\n\t\treturn span;\n\t});\n}\n\nfunction getReplacementLineRange(lines: LineSpan[], replacement: TextReplacement) {\n\tconst replacementStart = replacement.matchIndex;\n\tconst replacementEnd = replacement.matchIndex + replacement.matchLength;\n\n\tlet startLine = -1;\n\tfor (let i = 0; i < lines.length; i++) {\n\t\tconst line = lines[i];\n\t\tif (replacementStart >= line.start && replacementStart < line.end) {\n\t\t\tstartLine = i;\n\t\t\tbreak;\n\t\t}\n\t}\n\tif (startLine === -1) {\n\t\tthrow new Error(\"Replacement range is outside the base content.\");\n\t}\n\n\tlet endLine = startLine;\n\twhile (endLine < lines.length && lines[endLine].end < replacementEnd) {\n\t\tendLine++;\n\t}\n\tif (endLine >= lines.length) {\n\t\tthrow new Error(\"Replacement range is outside the base content.\");\n\t}\n\n\treturn { startLine, endLine: endLine + 1 };\n}\n\nfunction applyReplacements(content: string, replacements: TextReplacement[], offset = 0): string {\n\tlet result = content;\n\tfor (let i = replacements.length - 1; i >= 0; i--) {\n\t\tconst replacement = replacements[i];\n\t\tconst matchIndex = replacement.matchIndex - offset;\n\t\tresult =\n\t\t\tresult.substring(0, matchIndex) + replacement.newText + result.substring(matchIndex + replacement.matchLength);\n\t}\n\treturn result;\n}\n\n/**\n * Apply replacements matched against `baseContent` to `originalContent` while\n * preserving unchanged line blocks from the original.\n *\n * This is useful when `baseContent` is a normalized view of the original. Each\n * replacement is widened to the lines it actually touches, those touched lines\n * are rewritten from the normalized base, and all other lines are copied back\n * from `originalContent`. The actual replacement ranges drive preservation so\n * duplicate normalized lines cannot be aligned to the wrong occurrence.\n */\nexport function applyReplacementsPreservingUnchangedLines(\n\toriginalContent: string,\n\tbaseContent: string,\n\treplacements: TextReplacement[],\n): string {\n\tconst originalLines = splitLinesWithEndings(originalContent);\n\tconst baseLines = getLineSpans(baseContent);\n\tif (originalLines.length !== baseLines.length) {\n\t\tthrow new Error(\"Cannot preserve unchanged lines because the base content has a different line count.\");\n\t}\n\n\tconst groups: Array<{ startLine: number; endLine: number; replacements: TextReplacement[] }> = [];\n\tconst sortedReplacements = [...replacements].sort((a, b) => a.matchIndex - b.matchIndex);\n\tfor (const replacement of sortedReplacements) {\n\t\tconst range = getReplacementLineRange(baseLines, replacement);\n\t\tconst current = groups[groups.length - 1];\n\t\tif (current && range.startLine < current.endLine) {\n\t\t\tcurrent.endLine = Math.max(current.endLine, range.endLine);\n\t\t\tcurrent.replacements.push(replacement);\n\t\t\tcontinue;\n\t\t}\n\t\tgroups.push({ ...range, replacements: [replacement] });\n\t}\n\n\tlet originalLineIndex = 0;\n\tlet result = \"\";\n\tfor (const group of groups) {\n\t\tresult += originalLines.slice(originalLineIndex, group.startLine).join(\"\");\n\n\t\tconst groupStartOffset = baseLines[group.startLine].start;\n\t\tconst groupEndOffset = baseLines[group.endLine - 1].end;\n\t\tresult += applyReplacements(\n\t\t\tbaseContent.slice(groupStartOffset, groupEndOffset),\n\t\t\tgroup.replacements,\n\t\t\tgroupStartOffset,\n\t\t);\n\t\toriginalLineIndex = group.endLine;\n\t}\n\tresult += originalLines.slice(originalLineIndex).join(\"\");\n\n\treturn result;\n}\n\nexport interface FuzzyMatchResult {\n\t/** Whether a match was found */\n\tfound: boolean;\n\t/** The index where the match starts (in the content that should be used for replacement) */\n\tindex: number;\n\t/** Length of the matched text */\n\tmatchLength: number;\n\t/** Whether fuzzy matching was used (false = exact match) */\n\tusedFuzzyMatch: boolean;\n\t/**\n\t * The content to use for replacement operations.\n\t * When exact match: original content. When fuzzy match: normalized content.\n\t */\n\tcontentForReplacement: string;\n}\n\nexport interface Edit {\n\toldText: string;\n\tnewText: string;\n}\n\nexport interface AppliedEditsResult {\n\tbaseContent: string;\n\tnewContent: string;\n}\n\n/**\n * Find oldText in content, trying exact match first, then fuzzy match.\n * When fuzzy matching is used, the returned contentForReplacement is the\n * fuzzy-normalized version of the content (trailing whitespace stripped,\n * Unicode quotes/dashes normalized to ASCII).\n */\nexport function fuzzyFindText(content: string, oldText: string): FuzzyMatchResult {\n\t// Try exact match first\n\tconst exactIndex = content.indexOf(oldText);\n\tif (exactIndex !== -1) {\n\t\treturn {\n\t\t\tfound: true,\n\t\t\tindex: exactIndex,\n\t\t\tmatchLength: oldText.length,\n\t\t\tusedFuzzyMatch: false,\n\t\t\tcontentForReplacement: content,\n\t\t};\n\t}\n\n\t// Try fuzzy match - work entirely in normalized space\n\tconst fuzzyContent = normalizeForFuzzyMatch(content);\n\tconst fuzzyOldText = normalizeForFuzzyMatch(oldText);\n\tconst fuzzyIndex = fuzzyContent.indexOf(fuzzyOldText);\n\n\tif (fuzzyIndex === -1) {\n\t\treturn {\n\t\t\tfound: false,\n\t\t\tindex: -1,\n\t\t\tmatchLength: 0,\n\t\t\tusedFuzzyMatch: false,\n\t\t\tcontentForReplacement: content,\n\t\t};\n\t}\n\n\t// When fuzzy matching, return offsets in normalized space. Callers can use\n\t// the normalized content to compute replacements, then decide how much of\n\t// that normalized output should be written back.\n\treturn {\n\t\tfound: true,\n\t\tindex: fuzzyIndex,\n\t\tmatchLength: fuzzyOldText.length,\n\t\tusedFuzzyMatch: true,\n\t\tcontentForReplacement: fuzzyContent,\n\t};\n}\n\nfunction countOccurrences(content: string, oldText: string): number {\n\tconst fuzzyContent = normalizeForFuzzyMatch(content);\n\tconst fuzzyOldText = normalizeForFuzzyMatch(oldText);\n\treturn fuzzyContent.split(fuzzyOldText).length - 1;\n}\n\n/**\n * Explicit budgets for the advisory edit-failure diagnostics below. Diagnostics\n * are a best-effort hint, never an apply path, so every dimension of the scan\n * is bounded and the ordinary failure is returned untouched when a bound is\n * exceeded (spec 2026-09-01-tool-reliability-and-execution-budgets.md § 2).\n */\nexport const EDIT_DIAGNOSTIC_MAX_FILE_BYTES = 1_048_576;\nexport const EDIT_DIAGNOSTIC_MAX_TARGET_BYTES = 65_536;\nexport const EDIT_DIAGNOSTIC_MAX_CANDIDATES = 3;\nexport const EDIT_DIAGNOSTIC_MAX_SCAN_WINDOWS = 20_000;\nexport const EDIT_DIAGNOSTIC_MAX_OCCURRENCES = 5;\nexport const EDIT_DIAGNOSTIC_SNIPPET_LINE_CHARS = 160;\n\nconst EDIT_DIAGNOSTIC_MIN_SIMILARITY = 0.2;\nconst EDIT_DIAGNOSTIC_SCAN_BUDGET_MS = 50;\nconst EDIT_DIAGNOSTIC_MAX_OCCURRENCE_STEPS = 10_000;\n\nexport interface EditCandidateLocation {\n\t/** 1-based, inclusive. */\n\tstartLine: number;\n\t/** 1-based, inclusive. */\n\tendLine: number;\n\t/** Bigram Dice similarity against the failed target, rounded to two decimals. */\n\tsimilarity: number;\n\t/** Bounded snippet of the candidate source lines, each line-numbered. */\n\tsnippet: string;\n}\n\n/**\n * Advisory diagnostics attached to a failed edit match. These describe where a\n * target almost matched or where duplicates live; they are never a replacement\n * location and must never be applied.\n */\nexport interface EditFailureDiagnostics {\n\tkind: \"missing-match\" | \"duplicate-match\";\n\tcandidates: EditCandidateLocation[];\n\t/** 1-based source lines of up to EDIT_DIAGNOSTIC_MAX_OCCURRENCES duplicates. */\n\toccurrenceLines: number[];\n\tomittedOccurrences: number;\n\t/** True when the scan stopped at a work or time bound before finishing. */\n\tscanTruncated: boolean;\n\t/** Set when scanning was unsafe or oversized; no candidates are reported. */\n\tunavailableReason?: string;\n}\n\nfunction unavailableDiagnostics(kind: EditFailureDiagnostics[\"kind\"], reason: string): EditFailureDiagnostics {\n\treturn {\n\t\tkind,\n\t\tcandidates: [],\n\t\toccurrenceLines: [],\n\t\tomittedOccurrences: 0,\n\t\tscanTruncated: false,\n\t\tunavailableReason: reason,\n\t};\n}\n\nfunction isDiagnosticScannable(\n\tkind: EditFailureDiagnostics[\"kind\"],\n\tcontent: string,\n\ttarget: string,\n): EditFailureDiagnostics | undefined {\n\tif (Buffer.byteLength(content, \"utf-8\") > EDIT_DIAGNOSTIC_MAX_FILE_BYTES) {\n\t\treturn unavailableDiagnostics(\n\t\t\tkind,\n\t\t\t`advisory diagnostic scan skipped: file exceeds the ${EDIT_DIAGNOSTIC_MAX_FILE_BYTES}-byte diagnostic budget`,\n\t\t);\n\t}\n\tif (Buffer.byteLength(target, \"utf-8\") > EDIT_DIAGNOSTIC_MAX_TARGET_BYTES) {\n\t\treturn unavailableDiagnostics(\n\t\t\tkind,\n\t\t\t`advisory diagnostic scan skipped: oldText exceeds the ${EDIT_DIAGNOSTIC_MAX_TARGET_BYTES}-byte diagnostic budget`,\n\t\t);\n\t}\n\treturn undefined;\n}\n\nfunction bigrams(text: string): Set<string> {\n\tconst result = new Set<string>();\n\tfor (let i = 0; i < text.length - 1; i++) {\n\t\tresult.add(text.slice(i, i + 2));\n\t}\n\treturn result;\n}\n\nfunction diceSimilarity(a: Set<string>, b: Set<string>): number {\n\tif (a.size === 0 && b.size === 0) return 1;\n\tif (a.size === 0 || b.size === 0) return 0;\n\tlet intersection = 0;\n\tfor (const gram of a) {\n\t\tif (b.has(gram)) intersection++;\n\t}\n\treturn (2 * intersection) / (a.size + b.size);\n}\n\nfunction truncateSnippetLine(line: string): string {\n\tif (line.length <= EDIT_DIAGNOSTIC_SNIPPET_LINE_CHARS) return line;\n\treturn `${line.slice(0, EDIT_DIAGNOSTIC_SNIPPET_LINE_CHARS)}…`;\n}\n\nfunction buildSnippet(lines: string[], startLine: number, lineCount: number): string {\n\tconst shown = Math.min(lineCount, 5);\n\tconst parts: string[] = [];\n\tfor (let i = 0; i < shown; i++) {\n\t\tparts.push(`${startLine + i + 1} | ${truncateSnippetLine(lines[startLine + i] ?? \"\")}`);\n\t}\n\treturn parts.join(\"\\n\");\n}\n\n/**\n * Find the closest line windows to a failed target. Purely advisory: the result\n * names locations so the caller can retry with better context, and is never a\n * replacement specification.\n */\nfunction scanMissingMatchCandidates(content: string, target: string): EditFailureDiagnostics {\n\tconst blocked = isDiagnosticScannable(\"missing-match\", content, target);\n\tif (blocked) return blocked;\n\n\tconst lines = content.split(\"\\n\");\n\tconst windowSize = Math.max(1, target.split(\"\\n\").length);\n\tconst windowCount = lines.length - windowSize + 1;\n\tconst deadline = Date.now() + EDIT_DIAGNOSTIC_SCAN_BUDGET_MS;\n\tconst targetGrams = bigrams(target);\n\n\tconst best: Array<{ startLine: number; similarity: number }> = [];\n\tlet windowsScanned = 0;\n\tlet scanTruncated = false;\n\tfor (let start = 0; start < windowCount; start++) {\n\t\tif (windowsScanned >= EDIT_DIAGNOSTIC_MAX_SCAN_WINDOWS || (start % 512 === 0 && Date.now() > deadline)) {\n\t\t\tscanTruncated = true;\n\t\t\tbreak;\n\t\t}\n\t\twindowsScanned++;\n\t\tconst windowGrams = bigrams(lines.slice(start, start + windowSize).join(\"\\n\"));\n\t\tconst similarity = diceSimilarity(targetGrams, windowGrams);\n\t\tif (similarity < EDIT_DIAGNOSTIC_MIN_SIMILARITY) continue;\n\t\tif (best.length < EDIT_DIAGNOSTIC_MAX_CANDIDATES || similarity > best[best.length - 1].similarity) {\n\t\t\tbest.push({ startLine: start, similarity });\n\t\t\tbest.sort((a, b) => b.similarity - a.similarity);\n\t\t\tif (best.length > EDIT_DIAGNOSTIC_MAX_CANDIDATES) best.pop();\n\t\t}\n\t}\n\n\treturn {\n\t\tkind: \"missing-match\",\n\t\tcandidates: best.map((entry) => ({\n\t\t\tstartLine: entry.startLine + 1,\n\t\t\tendLine: entry.startLine + windowSize,\n\t\t\tsimilarity: Math.round(entry.similarity * 100) / 100,\n\t\t\tsnippet: buildSnippet(lines, entry.startLine, windowSize),\n\t\t})),\n\t\toccurrenceLines: [],\n\t\tomittedOccurrences: 0,\n\t\tscanTruncated,\n\t};\n}\n\nfunction lineNumberForOffset(lineStarts: number[], offset: number): number {\n\tlet low = 0;\n\tlet high = lineStarts.length - 1;\n\twhile (low < high) {\n\t\tconst mid = Math.ceil((low + high) / 2);\n\t\tif (lineStarts[mid] <= offset) low = mid;\n\t\telse high = mid - 1;\n\t}\n\treturn low + 1;\n}\n\n/** List bounded 1-based source lines for duplicate occurrences. Advisory only. */\nfunction scanDuplicateOccurrences(content: string, target: string): EditFailureDiagnostics {\n\tconst blocked = isDiagnosticScannable(\"duplicate-match\", content, target);\n\tif (blocked) return blocked;\n\n\tconst lines = content.split(\"\\n\");\n\tconst lineStarts: number[] = [];\n\tlet offset = 0;\n\tfor (const line of lines) {\n\t\tlineStarts.push(offset);\n\t\toffset += line.length + 1;\n\t}\n\n\tconst occurrenceLines: number[] = [];\n\tlet total = 0;\n\tlet truncated = false;\n\tlet searchFrom = 0;\n\twhile (target.length > 0) {\n\t\tconst index = content.indexOf(target, searchFrom);\n\t\tif (index === -1) break;\n\t\ttotal++;\n\t\tif (occurrenceLines.length < EDIT_DIAGNOSTIC_MAX_OCCURRENCES) {\n\t\t\toccurrenceLines.push(lineNumberForOffset(lineStarts, index));\n\t\t}\n\t\tsearchFrom = index + target.length;\n\t\tif (total >= EDIT_DIAGNOSTIC_MAX_OCCURRENCE_STEPS) {\n\t\t\ttruncated = true;\n\t\t\tbreak;\n\t\t}\n\t}\n\n\treturn {\n\t\tkind: \"duplicate-match\",\n\t\tcandidates: [],\n\t\toccurrenceLines,\n\t\tomittedOccurrences: Math.max(0, total - occurrenceLines.length),\n\t\tscanTruncated: truncated,\n\t};\n}\n\n/** Render advisory diagnostics as an error-message appendix. Empty when there is nothing to say. */\nexport function formatEditFailureDiagnostics(diagnostics: EditFailureDiagnostics | undefined): string {\n\tif (!diagnostics) return \"\";\n\tif (diagnostics.unavailableReason) {\n\t\treturn `\\n\\n(${diagnostics.unavailableReason}.)`;\n\t}\n\tconst sections: string[] = [];\n\tif (diagnostics.kind === \"duplicate-match\" && diagnostics.occurrenceLines.length > 0) {\n\t\tconst more = diagnostics.omittedOccurrences > 0 ? ` (+${diagnostics.omittedOccurrences} more)` : \"\";\n\t\tsections.push(`Advisory occurrence lines (not applied): ${diagnostics.occurrenceLines.join(\", \")}${more}`);\n\t}\n\tif (diagnostics.kind === \"missing-match\" && diagnostics.candidates.length > 0) {\n\t\tconst parts = [\"Advisory closest matches (not applied):\"];\n\t\tfor (const candidate of diagnostics.candidates) {\n\t\t\tparts.push(\n\t\t\t\t` lines ${candidate.startLine}-${candidate.endLine} (${Math.round(candidate.similarity * 100)}% similar)`,\n\t\t\t);\n\t\t\tfor (const line of candidate.snippet.split(\"\\n\")) {\n\t\t\t\tparts.push(` ${line}`);\n\t\t\t}\n\t\t}\n\t\tsections.push(parts.join(\"\\n\"));\n\t}\n\tif (diagnostics.scanTruncated) {\n\t\tsections.push(\n\t\t\t`(Advisory location scan truncated after ${EDIT_DIAGNOSTIC_MAX_SCAN_WINDOWS} windows; remaining locations were not examined.)`,\n\t\t);\n\t}\n\treturn sections.length > 0 ? `\\n\\n${sections.join(\"\\n\")}` : \"\";\n}\n\nfunction appendEditDiagnostics(error: Error, diagnostics: EditFailureDiagnostics): Error {\n\terror.message += formatEditFailureDiagnostics(diagnostics);\n\treturn error;\n}\n\nfunction getNotFoundError(path: string, editIndex: number, totalEdits: number): Error {\n\tif (totalEdits === 1) {\n\t\treturn new Error(\n\t\t\t`Could not find the exact text in ${path}. The old text must match exactly including all whitespace and newlines.`,\n\t\t);\n\t}\n\treturn new Error(\n\t\t`Could not find edits[${editIndex}] in ${path}. The oldText must match exactly including all whitespace and newlines.`,\n\t);\n}\n\nfunction getDuplicateError(path: string, editIndex: number, totalEdits: number, occurrences: number): Error {\n\tif (totalEdits === 1) {\n\t\treturn new Error(\n\t\t\t`Found ${occurrences} occurrences of the text in ${path}. The text must be unique. Please provide more context to make it unique.`,\n\t\t);\n\t}\n\treturn new Error(\n\t\t`Found ${occurrences} occurrences of edits[${editIndex}] in ${path}. Each oldText must be unique. Please provide more context to make it unique.`,\n\t);\n}\n\nfunction getEmptyOldTextError(path: string, editIndex: number, totalEdits: number): Error {\n\tif (totalEdits === 1) {\n\t\treturn new Error(`oldText must not be empty in ${path}.`);\n\t}\n\treturn new Error(`edits[${editIndex}].oldText must not be empty in ${path}.`);\n}\n\nfunction getNoChangeError(path: string, totalEdits: number): Error {\n\tif (totalEdits === 1) {\n\t\treturn new Error(\n\t\t\t`No changes made to ${path}. The replacement produced identical content. This might indicate an issue with special characters or the text not existing as expected.`,\n\t\t);\n\t}\n\treturn new Error(`No changes made to ${path}. The replacements produced identical content.`);\n}\n\n/**\n * Apply one or more exact-text replacements to LF-normalized content.\n *\n * All edits are matched against the same original content. Replacements are\n * then applied in reverse order so offsets remain stable. If any edit needs\n * fuzzy matching, the operation runs in fuzzy-normalized content space and then\n * overlays those line-level changes onto the original content so unchanged line\n * blocks keep their original bytes.\n */\nexport function applyEditsToNormalizedContent(\n\tnormalizedContent: string,\n\tedits: Edit[],\n\tpath: string,\n): AppliedEditsResult {\n\tconst normalizedEdits = edits.map((edit) => ({\n\t\toldText: normalizeToLF(edit.oldText),\n\t\tnewText: normalizeToLF(edit.newText),\n\t}));\n\n\tfor (let i = 0; i < normalizedEdits.length; i++) {\n\t\tif (normalizedEdits[i].oldText.length === 0) {\n\t\t\tthrow getEmptyOldTextError(path, i, normalizedEdits.length);\n\t\t}\n\t}\n\n\tconst initialMatches = normalizedEdits.map((edit) => fuzzyFindText(normalizedContent, edit.oldText));\n\tconst usedFuzzyMatch = initialMatches.some((match) => match.usedFuzzyMatch);\n\tconst replacementBaseContent = usedFuzzyMatch ? normalizeForFuzzyMatch(normalizedContent) : normalizedContent;\n\n\tconst matchedEdits: MatchedEdit[] = [];\n\tfor (let i = 0; i < normalizedEdits.length; i++) {\n\t\tconst edit = normalizedEdits[i];\n\t\tconst matchResult = fuzzyFindText(replacementBaseContent, edit.oldText);\n\t\tif (!matchResult.found) {\n\t\t\t// Advisory only: candidates name near-miss locations but never supply a\n\t\t\t// replacement. Matching above remains the only apply path.\n\t\t\tconst diagnosticTarget = usedFuzzyMatch ? normalizeForFuzzyMatch(edit.oldText) : edit.oldText;\n\t\t\tthrow appendEditDiagnostics(\n\t\t\t\tgetNotFoundError(path, i, normalizedEdits.length),\n\t\t\t\tscanMissingMatchCandidates(replacementBaseContent, diagnosticTarget),\n\t\t\t);\n\t\t}\n\n\t\tconst occurrences = countOccurrences(replacementBaseContent, edit.oldText);\n\t\tif (occurrences > 1) {\n\t\t\tconst diagnosticTarget = usedFuzzyMatch ? normalizeForFuzzyMatch(edit.oldText) : edit.oldText;\n\t\t\tthrow appendEditDiagnostics(\n\t\t\t\tgetDuplicateError(path, i, normalizedEdits.length, occurrences),\n\t\t\t\tscanDuplicateOccurrences(replacementBaseContent, diagnosticTarget),\n\t\t\t);\n\t\t}\n\n\t\tmatchedEdits.push({\n\t\t\teditIndex: i,\n\t\t\tmatchIndex: matchResult.index,\n\t\t\tmatchLength: matchResult.matchLength,\n\t\t\tnewText: edit.newText,\n\t\t});\n\t}\n\n\tmatchedEdits.sort((a, b) => a.matchIndex - b.matchIndex);\n\tfor (let i = 1; i < matchedEdits.length; i++) {\n\t\tconst previous = matchedEdits[i - 1];\n\t\tconst current = matchedEdits[i];\n\t\tif (previous.matchIndex + previous.matchLength > current.matchIndex) {\n\t\t\tthrow new Error(\n\t\t\t\t`edits[${previous.editIndex}] and edits[${current.editIndex}] overlap in ${path}. Merge them into one edit or target disjoint regions.`,\n\t\t\t);\n\t\t}\n\t}\n\n\tconst baseContent = normalizedContent;\n\tconst newContent = usedFuzzyMatch\n\t\t? applyReplacementsPreservingUnchangedLines(normalizedContent, replacementBaseContent, matchedEdits)\n\t\t: applyReplacements(replacementBaseContent, matchedEdits);\n\n\tif (baseContent === newContent) {\n\t\tthrow getNoChangeError(path, normalizedEdits.length);\n\t}\n\n\treturn { baseContent, newContent };\n}\n\n/** Generate a standard unified patch. */\nexport function generateUnifiedPatch(path: string, oldContent: string, newContent: string, contextLines = 4): string {\n\treturn Diff.createTwoFilesPatch(path, path, oldContent, newContent, undefined, undefined, {\n\t\tcontext: contextLines,\n\t\theaderOptions: Diff.FILE_HEADERS_ONLY,\n\t});\n}\n\n/**\n * Generate a display-oriented diff string with line numbers and context.\n * Returns both the diff string and the first changed line number (in the new file).\n */\nexport function generateDiffString(\n\toldContent: string,\n\tnewContent: string,\n\tcontextLines = 4,\n): { diff: string; firstChangedLine: number | undefined } {\n\tconst parts = Diff.diffLines(oldContent, newContent);\n\tconst output: string[] = [];\n\n\tconst oldLines = oldContent.split(\"\\n\");\n\tconst newLines = newContent.split(\"\\n\");\n\tconst maxLineNum = Math.max(oldLines.length, newLines.length);\n\tconst lineNumWidth = String(maxLineNum).length;\n\n\tlet oldLineNum = 1;\n\tlet newLineNum = 1;\n\tlet lastWasChange = false;\n\tlet firstChangedLine: number | undefined;\n\n\tfor (let i = 0; i < parts.length; i++) {\n\t\tconst part = parts[i];\n\t\tconst raw = part.value.split(\"\\n\");\n\t\tif (raw[raw.length - 1] === \"\") {\n\t\t\traw.pop();\n\t\t}\n\n\t\tif (part.added || part.removed) {\n\t\t\t// Capture the first changed line (in the new file)\n\t\t\tif (firstChangedLine === undefined) {\n\t\t\t\tfirstChangedLine = newLineNum;\n\t\t\t}\n\n\t\t\t// Show the change\n\t\t\tfor (const line of raw) {\n\t\t\t\tif (part.added) {\n\t\t\t\t\tconst lineNum = String(newLineNum).padStart(lineNumWidth, \" \");\n\t\t\t\t\toutput.push(`+${lineNum} ${line}`);\n\t\t\t\t\tnewLineNum++;\n\t\t\t\t} else {\n\t\t\t\t\t// removed\n\t\t\t\t\tconst lineNum = String(oldLineNum).padStart(lineNumWidth, \" \");\n\t\t\t\t\toutput.push(`-${lineNum} ${line}`);\n\t\t\t\t\toldLineNum++;\n\t\t\t\t}\n\t\t\t}\n\t\t\tlastWasChange = true;\n\t\t} else {\n\t\t\t// Context lines - only show a few before/after changes\n\t\t\tconst nextPartIsChange = i < parts.length - 1 && (parts[i + 1].added || parts[i + 1].removed);\n\t\t\tconst hasLeadingChange = lastWasChange;\n\t\t\tconst hasTrailingChange = nextPartIsChange;\n\n\t\t\tif (hasLeadingChange && hasTrailingChange) {\n\t\t\t\tif (raw.length <= contextLines * 2) {\n\t\t\t\t\tfor (const line of raw) {\n\t\t\t\t\t\tconst lineNum = String(oldLineNum).padStart(lineNumWidth, \" \");\n\t\t\t\t\t\toutput.push(` ${lineNum} ${line}`);\n\t\t\t\t\t\toldLineNum++;\n\t\t\t\t\t\tnewLineNum++;\n\t\t\t\t\t}\n\t\t\t\t} else {\n\t\t\t\t\tconst leadingLines = raw.slice(0, contextLines);\n\t\t\t\t\tconst trailingLines = raw.slice(raw.length - contextLines);\n\t\t\t\t\tconst skippedLines = raw.length - leadingLines.length - trailingLines.length;\n\n\t\t\t\t\tfor (const line of leadingLines) {\n\t\t\t\t\t\tconst lineNum = String(oldLineNum).padStart(lineNumWidth, \" \");\n\t\t\t\t\t\toutput.push(` ${lineNum} ${line}`);\n\t\t\t\t\t\toldLineNum++;\n\t\t\t\t\t\tnewLineNum++;\n\t\t\t\t\t}\n\n\t\t\t\t\toutput.push(` ${\"\".padStart(lineNumWidth, \" \")} ...`);\n\t\t\t\t\toldLineNum += skippedLines;\n\t\t\t\t\tnewLineNum += skippedLines;\n\n\t\t\t\t\tfor (const line of trailingLines) {\n\t\t\t\t\t\tconst lineNum = String(oldLineNum).padStart(lineNumWidth, \" \");\n\t\t\t\t\t\toutput.push(` ${lineNum} ${line}`);\n\t\t\t\t\t\toldLineNum++;\n\t\t\t\t\t\tnewLineNum++;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t} else if (hasLeadingChange) {\n\t\t\t\tconst shownLines = raw.slice(0, contextLines);\n\t\t\t\tconst skippedLines = raw.length - shownLines.length;\n\n\t\t\t\tfor (const line of shownLines) {\n\t\t\t\t\tconst lineNum = String(oldLineNum).padStart(lineNumWidth, \" \");\n\t\t\t\t\toutput.push(` ${lineNum} ${line}`);\n\t\t\t\t\toldLineNum++;\n\t\t\t\t\tnewLineNum++;\n\t\t\t\t}\n\n\t\t\t\tif (skippedLines > 0) {\n\t\t\t\t\toutput.push(` ${\"\".padStart(lineNumWidth, \" \")} ...`);\n\t\t\t\t\toldLineNum += skippedLines;\n\t\t\t\t\tnewLineNum += skippedLines;\n\t\t\t\t}\n\t\t\t} else if (hasTrailingChange) {\n\t\t\t\tconst skippedLines = Math.max(0, raw.length - contextLines);\n\t\t\t\tif (skippedLines > 0) {\n\t\t\t\t\toutput.push(` ${\"\".padStart(lineNumWidth, \" \")} ...`);\n\t\t\t\t\toldLineNum += skippedLines;\n\t\t\t\t\tnewLineNum += skippedLines;\n\t\t\t\t}\n\n\t\t\t\tfor (const line of raw.slice(skippedLines)) {\n\t\t\t\t\tconst lineNum = String(oldLineNum).padStart(lineNumWidth, \" \");\n\t\t\t\t\toutput.push(` ${lineNum} ${line}`);\n\t\t\t\t\toldLineNum++;\n\t\t\t\t\tnewLineNum++;\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\t// Skip these context lines entirely\n\t\t\t\toldLineNum += raw.length;\n\t\t\t\tnewLineNum += raw.length;\n\t\t\t}\n\n\t\t\tlastWasChange = false;\n\t\t}\n\t}\n\n\treturn { diff: output.join(\"\\n\"), firstChangedLine };\n}\n\nexport interface EditDiffResult {\n\tdiff: string;\n\tfirstChangedLine: number | undefined;\n}\n\nexport interface EditDiffError {\n\terror: string;\n}\n\n/**\n * Compute the diff for one or more edit operations without applying them.\n * Used for preview rendering in the TUI before the tool executes.\n */\nexport async function computeEditsDiff(\n\tpath: string,\n\tedits: Edit[],\n\tcwd: string,\n): Promise<EditDiffResult | EditDiffError> {\n\tconst absolutePath = resolveToCwd(path, cwd);\n\n\ttry {\n\t\t// Check if file exists and is readable\n\t\ttry {\n\t\t\tawait access(absolutePath, constants.R_OK);\n\t\t} catch (error: unknown) {\n\t\t\tconst errorMessage = error instanceof Error && \"code\" in error ? `Error code: ${error.code}` : String(error);\n\t\t\treturn { error: `Could not edit file: ${path}. ${errorMessage}.` };\n\t\t}\n\n\t\t// Read the file\n\t\tconst rawContent = await readFile(absolutePath, \"utf-8\");\n\t\treturn computeEditsDiffFromContent(path, rawContent, edits);\n\t} catch (err) {\n\t\treturn { error: err instanceof Error ? err.message : String(err) };\n\t}\n}\n\n/**\n * The half of `computeEditsDiff` that does not choose which bytes to read.\n *\n * The permission preview must describe the operation the gate authorized rather\n * than whatever the pathname resolves to a second time, so it supplies content\n * read through the prepared operation and calls this (ADR 0029).\n */\nexport function computeEditsDiffFromContent(\n\tpath: string,\n\trawContent: string,\n\tedits: Edit[],\n): EditDiffResult | EditDiffError {\n\ttry {\n\t\t// Strip BOM before matching (LLM won't include invisible BOM in oldText)\n\t\tconst { text: content } = splitBom(rawContent);\n\t\tconst normalizedContent = normalizeToLF(content);\n\t\tconst { baseContent, newContent } = applyEditsToNormalizedContent(normalizedContent, edits, path);\n\t\treturn generateDiffString(baseContent, newContent);\n\t} catch (err) {\n\t\treturn { error: err instanceof Error ? err.message : String(err) };\n\t}\n}\n\n/**\n * Compute the diff for a single edit operation without applying it.\n * Kept as a convenience wrapper for single-edit callers.\n */\nexport async function computeEditDiff(\n\tpath: string,\n\toldText: string,\n\tnewText: string,\n\tcwd: string,\n): Promise<EditDiffResult | EditDiffError> {\n\treturn computeEditsDiff(path, [{ oldText, newText }], cwd);\n}\n"]}
|
|
@@ -179,6 +179,200 @@ function countOccurrences(content, oldText) {
|
|
|
179
179
|
const fuzzyOldText = normalizeForFuzzyMatch(oldText);
|
|
180
180
|
return fuzzyContent.split(fuzzyOldText).length - 1;
|
|
181
181
|
}
|
|
182
|
+
/**
|
|
183
|
+
* Explicit budgets for the advisory edit-failure diagnostics below. Diagnostics
|
|
184
|
+
* are a best-effort hint, never an apply path, so every dimension of the scan
|
|
185
|
+
* is bounded and the ordinary failure is returned untouched when a bound is
|
|
186
|
+
* exceeded (spec 2026-09-01-tool-reliability-and-execution-budgets.md § 2).
|
|
187
|
+
*/
|
|
188
|
+
export const EDIT_DIAGNOSTIC_MAX_FILE_BYTES = 1_048_576;
|
|
189
|
+
export const EDIT_DIAGNOSTIC_MAX_TARGET_BYTES = 65_536;
|
|
190
|
+
export const EDIT_DIAGNOSTIC_MAX_CANDIDATES = 3;
|
|
191
|
+
export const EDIT_DIAGNOSTIC_MAX_SCAN_WINDOWS = 20_000;
|
|
192
|
+
export const EDIT_DIAGNOSTIC_MAX_OCCURRENCES = 5;
|
|
193
|
+
export const EDIT_DIAGNOSTIC_SNIPPET_LINE_CHARS = 160;
|
|
194
|
+
const EDIT_DIAGNOSTIC_MIN_SIMILARITY = 0.2;
|
|
195
|
+
const EDIT_DIAGNOSTIC_SCAN_BUDGET_MS = 50;
|
|
196
|
+
const EDIT_DIAGNOSTIC_MAX_OCCURRENCE_STEPS = 10_000;
|
|
197
|
+
function unavailableDiagnostics(kind, reason) {
|
|
198
|
+
return {
|
|
199
|
+
kind,
|
|
200
|
+
candidates: [],
|
|
201
|
+
occurrenceLines: [],
|
|
202
|
+
omittedOccurrences: 0,
|
|
203
|
+
scanTruncated: false,
|
|
204
|
+
unavailableReason: reason,
|
|
205
|
+
};
|
|
206
|
+
}
|
|
207
|
+
function isDiagnosticScannable(kind, content, target) {
|
|
208
|
+
if (Buffer.byteLength(content, "utf-8") > EDIT_DIAGNOSTIC_MAX_FILE_BYTES) {
|
|
209
|
+
return unavailableDiagnostics(kind, `advisory diagnostic scan skipped: file exceeds the ${EDIT_DIAGNOSTIC_MAX_FILE_BYTES}-byte diagnostic budget`);
|
|
210
|
+
}
|
|
211
|
+
if (Buffer.byteLength(target, "utf-8") > EDIT_DIAGNOSTIC_MAX_TARGET_BYTES) {
|
|
212
|
+
return unavailableDiagnostics(kind, `advisory diagnostic scan skipped: oldText exceeds the ${EDIT_DIAGNOSTIC_MAX_TARGET_BYTES}-byte diagnostic budget`);
|
|
213
|
+
}
|
|
214
|
+
return undefined;
|
|
215
|
+
}
|
|
216
|
+
function bigrams(text) {
|
|
217
|
+
const result = new Set();
|
|
218
|
+
for (let i = 0; i < text.length - 1; i++) {
|
|
219
|
+
result.add(text.slice(i, i + 2));
|
|
220
|
+
}
|
|
221
|
+
return result;
|
|
222
|
+
}
|
|
223
|
+
function diceSimilarity(a, b) {
|
|
224
|
+
if (a.size === 0 && b.size === 0)
|
|
225
|
+
return 1;
|
|
226
|
+
if (a.size === 0 || b.size === 0)
|
|
227
|
+
return 0;
|
|
228
|
+
let intersection = 0;
|
|
229
|
+
for (const gram of a) {
|
|
230
|
+
if (b.has(gram))
|
|
231
|
+
intersection++;
|
|
232
|
+
}
|
|
233
|
+
return (2 * intersection) / (a.size + b.size);
|
|
234
|
+
}
|
|
235
|
+
function truncateSnippetLine(line) {
|
|
236
|
+
if (line.length <= EDIT_DIAGNOSTIC_SNIPPET_LINE_CHARS)
|
|
237
|
+
return line;
|
|
238
|
+
return `${line.slice(0, EDIT_DIAGNOSTIC_SNIPPET_LINE_CHARS)}…`;
|
|
239
|
+
}
|
|
240
|
+
function buildSnippet(lines, startLine, lineCount) {
|
|
241
|
+
const shown = Math.min(lineCount, 5);
|
|
242
|
+
const parts = [];
|
|
243
|
+
for (let i = 0; i < shown; i++) {
|
|
244
|
+
parts.push(`${startLine + i + 1} | ${truncateSnippetLine(lines[startLine + i] ?? "")}`);
|
|
245
|
+
}
|
|
246
|
+
return parts.join("\n");
|
|
247
|
+
}
|
|
248
|
+
/**
|
|
249
|
+
* Find the closest line windows to a failed target. Purely advisory: the result
|
|
250
|
+
* names locations so the caller can retry with better context, and is never a
|
|
251
|
+
* replacement specification.
|
|
252
|
+
*/
|
|
253
|
+
function scanMissingMatchCandidates(content, target) {
|
|
254
|
+
const blocked = isDiagnosticScannable("missing-match", content, target);
|
|
255
|
+
if (blocked)
|
|
256
|
+
return blocked;
|
|
257
|
+
const lines = content.split("\n");
|
|
258
|
+
const windowSize = Math.max(1, target.split("\n").length);
|
|
259
|
+
const windowCount = lines.length - windowSize + 1;
|
|
260
|
+
const deadline = Date.now() + EDIT_DIAGNOSTIC_SCAN_BUDGET_MS;
|
|
261
|
+
const targetGrams = bigrams(target);
|
|
262
|
+
const best = [];
|
|
263
|
+
let windowsScanned = 0;
|
|
264
|
+
let scanTruncated = false;
|
|
265
|
+
for (let start = 0; start < windowCount; start++) {
|
|
266
|
+
if (windowsScanned >= EDIT_DIAGNOSTIC_MAX_SCAN_WINDOWS || (start % 512 === 0 && Date.now() > deadline)) {
|
|
267
|
+
scanTruncated = true;
|
|
268
|
+
break;
|
|
269
|
+
}
|
|
270
|
+
windowsScanned++;
|
|
271
|
+
const windowGrams = bigrams(lines.slice(start, start + windowSize).join("\n"));
|
|
272
|
+
const similarity = diceSimilarity(targetGrams, windowGrams);
|
|
273
|
+
if (similarity < EDIT_DIAGNOSTIC_MIN_SIMILARITY)
|
|
274
|
+
continue;
|
|
275
|
+
if (best.length < EDIT_DIAGNOSTIC_MAX_CANDIDATES || similarity > best[best.length - 1].similarity) {
|
|
276
|
+
best.push({ startLine: start, similarity });
|
|
277
|
+
best.sort((a, b) => b.similarity - a.similarity);
|
|
278
|
+
if (best.length > EDIT_DIAGNOSTIC_MAX_CANDIDATES)
|
|
279
|
+
best.pop();
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
return {
|
|
283
|
+
kind: "missing-match",
|
|
284
|
+
candidates: best.map((entry) => ({
|
|
285
|
+
startLine: entry.startLine + 1,
|
|
286
|
+
endLine: entry.startLine + windowSize,
|
|
287
|
+
similarity: Math.round(entry.similarity * 100) / 100,
|
|
288
|
+
snippet: buildSnippet(lines, entry.startLine, windowSize),
|
|
289
|
+
})),
|
|
290
|
+
occurrenceLines: [],
|
|
291
|
+
omittedOccurrences: 0,
|
|
292
|
+
scanTruncated,
|
|
293
|
+
};
|
|
294
|
+
}
|
|
295
|
+
function lineNumberForOffset(lineStarts, offset) {
|
|
296
|
+
let low = 0;
|
|
297
|
+
let high = lineStarts.length - 1;
|
|
298
|
+
while (low < high) {
|
|
299
|
+
const mid = Math.ceil((low + high) / 2);
|
|
300
|
+
if (lineStarts[mid] <= offset)
|
|
301
|
+
low = mid;
|
|
302
|
+
else
|
|
303
|
+
high = mid - 1;
|
|
304
|
+
}
|
|
305
|
+
return low + 1;
|
|
306
|
+
}
|
|
307
|
+
/** List bounded 1-based source lines for duplicate occurrences. Advisory only. */
|
|
308
|
+
function scanDuplicateOccurrences(content, target) {
|
|
309
|
+
const blocked = isDiagnosticScannable("duplicate-match", content, target);
|
|
310
|
+
if (blocked)
|
|
311
|
+
return blocked;
|
|
312
|
+
const lines = content.split("\n");
|
|
313
|
+
const lineStarts = [];
|
|
314
|
+
let offset = 0;
|
|
315
|
+
for (const line of lines) {
|
|
316
|
+
lineStarts.push(offset);
|
|
317
|
+
offset += line.length + 1;
|
|
318
|
+
}
|
|
319
|
+
const occurrenceLines = [];
|
|
320
|
+
let total = 0;
|
|
321
|
+
let truncated = false;
|
|
322
|
+
let searchFrom = 0;
|
|
323
|
+
while (target.length > 0) {
|
|
324
|
+
const index = content.indexOf(target, searchFrom);
|
|
325
|
+
if (index === -1)
|
|
326
|
+
break;
|
|
327
|
+
total++;
|
|
328
|
+
if (occurrenceLines.length < EDIT_DIAGNOSTIC_MAX_OCCURRENCES) {
|
|
329
|
+
occurrenceLines.push(lineNumberForOffset(lineStarts, index));
|
|
330
|
+
}
|
|
331
|
+
searchFrom = index + target.length;
|
|
332
|
+
if (total >= EDIT_DIAGNOSTIC_MAX_OCCURRENCE_STEPS) {
|
|
333
|
+
truncated = true;
|
|
334
|
+
break;
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
return {
|
|
338
|
+
kind: "duplicate-match",
|
|
339
|
+
candidates: [],
|
|
340
|
+
occurrenceLines,
|
|
341
|
+
omittedOccurrences: Math.max(0, total - occurrenceLines.length),
|
|
342
|
+
scanTruncated: truncated,
|
|
343
|
+
};
|
|
344
|
+
}
|
|
345
|
+
/** Render advisory diagnostics as an error-message appendix. Empty when there is nothing to say. */
|
|
346
|
+
export function formatEditFailureDiagnostics(diagnostics) {
|
|
347
|
+
if (!diagnostics)
|
|
348
|
+
return "";
|
|
349
|
+
if (diagnostics.unavailableReason) {
|
|
350
|
+
return `\n\n(${diagnostics.unavailableReason}.)`;
|
|
351
|
+
}
|
|
352
|
+
const sections = [];
|
|
353
|
+
if (diagnostics.kind === "duplicate-match" && diagnostics.occurrenceLines.length > 0) {
|
|
354
|
+
const more = diagnostics.omittedOccurrences > 0 ? ` (+${diagnostics.omittedOccurrences} more)` : "";
|
|
355
|
+
sections.push(`Advisory occurrence lines (not applied): ${diagnostics.occurrenceLines.join(", ")}${more}`);
|
|
356
|
+
}
|
|
357
|
+
if (diagnostics.kind === "missing-match" && diagnostics.candidates.length > 0) {
|
|
358
|
+
const parts = ["Advisory closest matches (not applied):"];
|
|
359
|
+
for (const candidate of diagnostics.candidates) {
|
|
360
|
+
parts.push(` lines ${candidate.startLine}-${candidate.endLine} (${Math.round(candidate.similarity * 100)}% similar)`);
|
|
361
|
+
for (const line of candidate.snippet.split("\n")) {
|
|
362
|
+
parts.push(` ${line}`);
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
sections.push(parts.join("\n"));
|
|
366
|
+
}
|
|
367
|
+
if (diagnostics.scanTruncated) {
|
|
368
|
+
sections.push(`(Advisory location scan truncated after ${EDIT_DIAGNOSTIC_MAX_SCAN_WINDOWS} windows; remaining locations were not examined.)`);
|
|
369
|
+
}
|
|
370
|
+
return sections.length > 0 ? `\n\n${sections.join("\n")}` : "";
|
|
371
|
+
}
|
|
372
|
+
function appendEditDiagnostics(error, diagnostics) {
|
|
373
|
+
error.message += formatEditFailureDiagnostics(diagnostics);
|
|
374
|
+
return error;
|
|
375
|
+
}
|
|
182
376
|
function getNotFoundError(path, editIndex, totalEdits) {
|
|
183
377
|
if (totalEdits === 1) {
|
|
184
378
|
return new Error(`Could not find the exact text in ${path}. The old text must match exactly including all whitespace and newlines.`);
|
|
@@ -230,11 +424,15 @@ export function applyEditsToNormalizedContent(normalizedContent, edits, path) {
|
|
|
230
424
|
const edit = normalizedEdits[i];
|
|
231
425
|
const matchResult = fuzzyFindText(replacementBaseContent, edit.oldText);
|
|
232
426
|
if (!matchResult.found) {
|
|
233
|
-
|
|
427
|
+
// Advisory only: candidates name near-miss locations but never supply a
|
|
428
|
+
// replacement. Matching above remains the only apply path.
|
|
429
|
+
const diagnosticTarget = usedFuzzyMatch ? normalizeForFuzzyMatch(edit.oldText) : edit.oldText;
|
|
430
|
+
throw appendEditDiagnostics(getNotFoundError(path, i, normalizedEdits.length), scanMissingMatchCandidates(replacementBaseContent, diagnosticTarget));
|
|
234
431
|
}
|
|
235
432
|
const occurrences = countOccurrences(replacementBaseContent, edit.oldText);
|
|
236
433
|
if (occurrences > 1) {
|
|
237
|
-
|
|
434
|
+
const diagnosticTarget = usedFuzzyMatch ? normalizeForFuzzyMatch(edit.oldText) : edit.oldText;
|
|
435
|
+
throw appendEditDiagnostics(getDuplicateError(path, i, normalizedEdits.length, occurrences), scanDuplicateOccurrences(replacementBaseContent, diagnosticTarget));
|
|
238
436
|
}
|
|
239
437
|
matchedEdits.push({
|
|
240
438
|
editIndex: i,
|
|
@@ -400,11 +598,25 @@ export async function computeEditsDiff(path, edits, cwd) {
|
|
|
400
598
|
}
|
|
401
599
|
// Read the file
|
|
402
600
|
const rawContent = await readFile(absolutePath, "utf-8");
|
|
601
|
+
return computeEditsDiffFromContent(path, rawContent, edits);
|
|
602
|
+
}
|
|
603
|
+
catch (err) {
|
|
604
|
+
return { error: err instanceof Error ? err.message : String(err) };
|
|
605
|
+
}
|
|
606
|
+
}
|
|
607
|
+
/**
|
|
608
|
+
* The half of `computeEditsDiff` that does not choose which bytes to read.
|
|
609
|
+
*
|
|
610
|
+
* The permission preview must describe the operation the gate authorized rather
|
|
611
|
+
* than whatever the pathname resolves to a second time, so it supplies content
|
|
612
|
+
* read through the prepared operation and calls this (ADR 0029).
|
|
613
|
+
*/
|
|
614
|
+
export function computeEditsDiffFromContent(path, rawContent, edits) {
|
|
615
|
+
try {
|
|
403
616
|
// Strip BOM before matching (LLM won't include invisible BOM in oldText)
|
|
404
617
|
const { text: content } = splitBom(rawContent);
|
|
405
618
|
const normalizedContent = normalizeToLF(content);
|
|
406
619
|
const { baseContent, newContent } = applyEditsToNormalizedContent(normalizedContent, edits, path);
|
|
407
|
-
// Generate the diff
|
|
408
620
|
return generateDiffString(baseContent, newContent);
|
|
409
621
|
}
|
|
410
622
|
catch (err) {
|