mixdog 0.7.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/marketplace.json +31 -0
- package/.claude-plugin/plugin.json +20 -0
- package/.gitattributes +34 -0
- package/.mcp.json +14 -0
- package/ARCHITECTURE.md +77 -0
- package/CHANGELOG.md +7 -0
- package/CONTRIBUTING.md +45 -0
- package/DATA-FLOW.md +79 -0
- package/LICENSE +21 -0
- package/README.md +389 -0
- package/SECURITY.md +138 -0
- package/UNINSTALL.md +112 -0
- package/agents/maintenance.md +5 -0
- package/agents/memory-classification.md +30 -0
- package/agents/scheduler-task.md +18 -0
- package/agents/webhook-handler.md +27 -0
- package/agents/worker.md +24 -0
- package/bin/bridge +133 -0
- package/bin/statusline-launcher.mjs +78 -0
- package/bin/statusline-lib.mjs +550 -0
- package/bin/statusline.mjs +607 -0
- package/bun.lock +802 -0
- package/commands/config.md +16 -0
- package/commands/doctor.md +13 -0
- package/commands/setup.md +17 -0
- package/defaults/cycle3-review-prompt.md +90 -0
- package/defaults/hidden-roles.json +65 -0
- package/defaults/memory-chunk-prompt.md +63 -0
- package/defaults/memory-promote-prompt.md +135 -0
- package/defaults/mixdog-config.template.json +27 -0
- package/defaults/user-workflow.json +8 -0
- package/defaults/user-workflow.md +12 -0
- package/hooks/hooks.json +73 -0
- package/hooks/lib/active-instance.cjs +77 -0
- package/hooks/lib/permission-evaluator.cjs +411 -0
- package/hooks/lib/permission-route.cjs +63 -0
- package/hooks/lib/permission-rules.cjs +170 -0
- package/hooks/lib/settings-loader.cjs +116 -0
- package/hooks/post-tool-use.cjs +84 -0
- package/hooks/pre-mcp-sandbox.cjs +158 -0
- package/hooks/pre-tool-subagent.cjs +253 -0
- package/hooks/session-start.cjs +1372 -0
- package/hooks/turn-timer.cjs +82 -0
- package/lib/claude-md-writer.cjs +386 -0
- package/lib/config-cjs.cjs +61 -0
- package/lib/hook-pipe-path.cjs +10 -0
- package/lib/keychain-cjs.cjs +263 -0
- package/lib/plugin-paths.cjs +61 -0
- package/lib/rules-builder.cjs +241 -0
- package/lib/text-utils.cjs +61 -0
- package/native/README.md +117 -0
- package/native/prebuilt/linux-aarch64/mixdog-shim +0 -0
- package/native/prebuilt/linux-x86_64/mixdog-shim +0 -0
- package/native/prebuilt/macos-aarch64/mixdog-shim +0 -0
- package/native/prebuilt/macos-x86_64/mixdog-shim +0 -0
- package/native/prebuilt/windows-x86_64/mixdog-shim.exe +0 -0
- package/package.json +107 -0
- package/prompts/code-review.txt +16 -0
- package/prompts/security-audit.txt +17 -0
- package/rules/bridge/00-common.md +39 -0
- package/rules/bridge/20-skip-protocol.md +18 -0
- package/rules/bridge/30-explorer.md +33 -0
- package/rules/bridge/40-cycle1-agent.md +52 -0
- package/rules/bridge/41-cycle2-agent.md +62 -0
- package/rules/bridge/42-cycle3-agent.md +44 -0
- package/rules/lead/00-tool-lead.md +61 -0
- package/rules/lead/01-general.md +23 -0
- package/rules/lead/02-channels.md +49 -0
- package/rules/lead/03-team.md +27 -0
- package/rules/lead/04-workflow.md +20 -0
- package/rules/shared/00-language.md +14 -0
- package/rules/shared/01-tool.md +138 -0
- package/scripts/bootstrap.mjs +184 -0
- package/scripts/bridge-unify-smoke.mjs +308 -0
- package/scripts/build-runtime-linux.sh +348 -0
- package/scripts/build-runtime-macos.sh +217 -0
- package/scripts/build-runtime-windows.ps1 +242 -0
- package/scripts/builtin-utils-smoke.mjs +392 -0
- package/scripts/check-json.mjs +45 -0
- package/scripts/check-syntax-changed.mjs +102 -0
- package/scripts/check-syntax.mjs +58 -0
- package/scripts/code-graph-batch.test.mjs +33 -0
- package/scripts/config-preserve-smoke.mjs +180 -0
- package/scripts/doctor.mjs +484 -0
- package/scripts/edit-normalize-fuzz.mjs +130 -0
- package/scripts/edit-normalize-smoke.mjs +401 -0
- package/scripts/edit-operation-smoke.mjs +369 -0
- package/scripts/edit2-smoke.mjs +63 -0
- package/scripts/fuzzy-e2e.mjs +28 -0
- package/scripts/fuzzy-smoke.mjs +26 -0
- package/scripts/generate-runtime-manifest.mjs +166 -0
- package/scripts/guard-smoke.mjs +66 -0
- package/scripts/hidden-role-schema-smoke.mjs +162 -0
- package/scripts/hook-routing-smoke.mjs +29 -0
- package/scripts/inject-input.ps1 +204 -0
- package/scripts/io-complex-smoke.mjs +667 -0
- package/scripts/io-explore-bench.mjs +424 -0
- package/scripts/io-guardrails-smoke.mjs +205 -0
- package/scripts/io-mini-bench-baseline.json +11 -0
- package/scripts/io-mini-bench.mjs +216 -0
- package/scripts/io-route-harness.mjs +933 -0
- package/scripts/io-telemetry-report.mjs +691 -0
- package/scripts/mutation-bench.mjs +564 -0
- package/scripts/mutation-io-smoke.mjs +1081 -0
- package/scripts/native-patch-bridge-smoke.mjs +288 -0
- package/scripts/native-patch-smoke.mjs +304 -0
- package/scripts/patch-interior-context-smoke.mjs +49 -0
- package/scripts/patch-newline-utf8-smoke.mjs +157 -0
- package/scripts/perf-hook-smoke.mjs +71 -0
- package/scripts/permission-eval-smoke.mjs +426 -0
- package/scripts/prep-patch.mjs +53 -0
- package/scripts/prep-shim.mjs +96 -0
- package/scripts/provider-cache-smoke.mjs +687 -0
- package/scripts/report-runtime-health.mjs +132 -0
- package/scripts/run-mcp.mjs +1547 -0
- package/scripts/salvage-v4a-shatter.test.mjs +58 -0
- package/scripts/scoped-cache-io-smoke.mjs +103 -0
- package/scripts/shell-policy-round3-smoke.mjs +46 -0
- package/scripts/smoke-runtime-negative.ps1 +100 -0
- package/scripts/smoke-runtime-negative.sh +95 -0
- package/scripts/stall-policy-smoke.mjs +50 -0
- package/scripts/start-memory-worker.mjs +23 -0
- package/scripts/statusline-launcher-smoke.mjs +82 -0
- package/scripts/stress-atomic-write.mjs +1028 -0
- package/scripts/test-config-rmw-restore.mjs +122 -0
- package/scripts/test-fault-inject.mjs +164 -0
- package/scripts/test-large-file.mjs +174 -0
- package/scripts/tool-edge-smoke.mjs +209 -0
- package/scripts/uninstall.mjs +201 -0
- package/scripts/webhook-selfheal-smoke.mjs +29 -0
- package/scripts/write-overwrite-guard-smoke.mjs +56 -0
- package/server-main.mjs +3055 -0
- package/server.mjs +468 -0
- package/setup/config-merge.mjs +254 -0
- package/setup/install.mjs +120 -0
- package/setup/launch-core.mjs +507 -0
- package/setup/launch.mjs +101 -0
- package/setup/setup-server.mjs +3206 -0
- package/setup/setup.html +3693 -0
- package/skills/retro-skill-proposer/SKILL.md +92 -0
- package/skills/schedule-add/SKILL.md +77 -0
- package/skills/setup/SKILL.md +346 -0
- package/skills/webhook-add/SKILL.md +81 -0
- package/src/agent/bridge-stall-watchdog.mjs +337 -0
- package/src/agent/index.mjs +2138 -0
- package/src/agent/orchestrator/activity-bus.mjs +38 -0
- package/src/agent/orchestrator/ai-wrapped-dispatch.mjs +1010 -0
- package/src/agent/orchestrator/bridge-retry.mjs +220 -0
- package/src/agent/orchestrator/bridge-trace.mjs +583 -0
- package/src/agent/orchestrator/cache-mtime.mjs +58 -0
- package/src/agent/orchestrator/config.mjs +358 -0
- package/src/agent/orchestrator/context/collect.mjs +651 -0
- package/src/agent/orchestrator/dispatch-persist.mjs +549 -0
- package/src/agent/orchestrator/drain-registry.mjs +50 -0
- package/src/agent/orchestrator/explore-validator.mjs +8 -0
- package/src/agent/orchestrator/internal-roles.mjs +118 -0
- package/src/agent/orchestrator/internal-tools.mjs +88 -0
- package/src/agent/orchestrator/jobs.mjs +116 -0
- package/src/agent/orchestrator/mcp/client.mjs +364 -0
- package/src/agent/orchestrator/providers/anthropic-betas.mjs +21 -0
- package/src/agent/orchestrator/providers/anthropic-oauth.mjs +1745 -0
- package/src/agent/orchestrator/providers/anthropic.mjs +437 -0
- package/src/agent/orchestrator/providers/gemini.mjs +1175 -0
- package/src/agent/orchestrator/providers/grok-oauth.mjs +782 -0
- package/src/agent/orchestrator/providers/model-catalog.mjs +241 -0
- package/src/agent/orchestrator/providers/openai-compat.mjs +1467 -0
- package/src/agent/orchestrator/providers/openai-oauth-ws.mjs +1890 -0
- package/src/agent/orchestrator/providers/openai-oauth.mjs +1307 -0
- package/src/agent/orchestrator/providers/openai-ws.mjs +104 -0
- package/src/agent/orchestrator/providers/registry.mjs +192 -0
- package/src/agent/orchestrator/providers/retry-classifier.mjs +325 -0
- package/src/agent/orchestrator/session/abort-lookup.mjs +13 -0
- package/src/agent/orchestrator/session/cache/post-edit-marks.mjs +42 -0
- package/src/agent/orchestrator/session/cache/prefetch-cache.mjs +142 -0
- package/src/agent/orchestrator/session/cache/read-cache.mjs +319 -0
- package/src/agent/orchestrator/session/cache/scoped-cache-outcome.mjs +11 -0
- package/src/agent/orchestrator/session/cache/scoped-cache.mjs +361 -0
- package/src/agent/orchestrator/session/cache/util.mjs +49 -0
- package/src/agent/orchestrator/session/loop.mjs +1478 -0
- package/src/agent/orchestrator/session/manager.mjs +1975 -0
- package/src/agent/orchestrator/session/read-dedup.mjs +6 -0
- package/src/agent/orchestrator/session/result-classification.mjs +65 -0
- package/src/agent/orchestrator/session/save-session-worker.mjs +18 -0
- package/src/agent/orchestrator/session/store.mjs +624 -0
- package/src/agent/orchestrator/session/stream-watchdog.mjs +130 -0
- package/src/agent/orchestrator/session/tool-result-offload.mjs +166 -0
- package/src/agent/orchestrator/session/trim.mjs +491 -0
- package/src/agent/orchestrator/smart-bridge/CACHE-SHARD.md +115 -0
- package/src/agent/orchestrator/smart-bridge/bridge-llm.mjs +327 -0
- package/src/agent/orchestrator/smart-bridge/cache-obs.mjs +150 -0
- package/src/agent/orchestrator/smart-bridge/cache-strategy.mjs +228 -0
- package/src/agent/orchestrator/smart-bridge/index.mjs +215 -0
- package/src/agent/orchestrator/smart-bridge/profiles.mjs +37 -0
- package/src/agent/orchestrator/smart-bridge/registry.mjs +348 -0
- package/src/agent/orchestrator/smart-bridge/session-builder.mjs +116 -0
- package/src/agent/orchestrator/stall-policy.mjs +195 -0
- package/src/agent/orchestrator/tool-loop-guard.mjs +75 -0
- package/src/agent/orchestrator/tools/bash-policy-scan.mjs +77 -0
- package/src/agent/orchestrator/tools/bash-session.mjs +721 -0
- package/src/agent/orchestrator/tools/builtin/advisory-lock.mjs +171 -0
- package/src/agent/orchestrator/tools/builtin/arg-guard.mjs +455 -0
- package/src/agent/orchestrator/tools/builtin/atomic-write.mjs +236 -0
- package/src/agent/orchestrator/tools/builtin/bash-tool.mjs +480 -0
- package/src/agent/orchestrator/tools/builtin/binary-file.mjs +76 -0
- package/src/agent/orchestrator/tools/builtin/builtin-tools.mjs +256 -0
- package/src/agent/orchestrator/tools/builtin/cache-layers.mjs +386 -0
- package/src/agent/orchestrator/tools/builtin/cwd-utils.mjs +37 -0
- package/src/agent/orchestrator/tools/builtin/device-paths.mjs +154 -0
- package/src/agent/orchestrator/tools/builtin/diagnostics-tool.mjs +292 -0
- package/src/agent/orchestrator/tools/builtin/diff-utils.mjs +109 -0
- package/src/agent/orchestrator/tools/builtin/edit-base-guard.mjs +58 -0
- package/src/agent/orchestrator/tools/builtin/edit-byte-plan.mjs +240 -0
- package/src/agent/orchestrator/tools/builtin/edit-byte-utils.mjs +113 -0
- package/src/agent/orchestrator/tools/builtin/edit-commit.mjs +74 -0
- package/src/agent/orchestrator/tools/builtin/edit-context-utils.mjs +242 -0
- package/src/agent/orchestrator/tools/builtin/edit-diagnostics.mjs +211 -0
- package/src/agent/orchestrator/tools/builtin/edit-engine.mjs +1364 -0
- package/src/agent/orchestrator/tools/builtin/edit-failure-context.mjs +126 -0
- package/src/agent/orchestrator/tools/builtin/edit-hint.mjs +141 -0
- package/src/agent/orchestrator/tools/builtin/edit-match-utils.mjs +194 -0
- package/src/agent/orchestrator/tools/builtin/edit-partial-write.mjs +60 -0
- package/src/agent/orchestrator/tools/builtin/edit-stale-refresh.mjs +168 -0
- package/src/agent/orchestrator/tools/builtin/edit-tool.mjs +173 -0
- package/src/agent/orchestrator/tools/builtin/edit-utf8-guard.mjs +48 -0
- package/src/agent/orchestrator/tools/builtin/fs-reachability.mjs +48 -0
- package/src/agent/orchestrator/tools/builtin/fuzzy-match.mjs +99 -0
- package/src/agent/orchestrator/tools/builtin/glob-walk.mjs +170 -0
- package/src/agent/orchestrator/tools/builtin/grep-formatting.mjs +113 -0
- package/src/agent/orchestrator/tools/builtin/hash-utils.mjs +6 -0
- package/src/agent/orchestrator/tools/builtin/list-formatting.mjs +7 -0
- package/src/agent/orchestrator/tools/builtin/list-tool.mjs +593 -0
- package/src/agent/orchestrator/tools/builtin/native-edit-runner.mjs +89 -0
- package/src/agent/orchestrator/tools/builtin/notebook-edit-tool.mjs +300 -0
- package/src/agent/orchestrator/tools/builtin/open-config-tool.mjs +26 -0
- package/src/agent/orchestrator/tools/builtin/path-diagnostics.mjs +152 -0
- package/src/agent/orchestrator/tools/builtin/path-locks.mjs +35 -0
- package/src/agent/orchestrator/tools/builtin/path-utils.mjs +201 -0
- package/src/agent/orchestrator/tools/builtin/read-args.mjs +103 -0
- package/src/agent/orchestrator/tools/builtin/read-batch.mjs +172 -0
- package/src/agent/orchestrator/tools/builtin/read-constants.mjs +40 -0
- package/src/agent/orchestrator/tools/builtin/read-formatting.mjs +118 -0
- package/src/agent/orchestrator/tools/builtin/read-image-resize.mjs +189 -0
- package/src/agent/orchestrator/tools/builtin/read-image.mjs +88 -0
- package/src/agent/orchestrator/tools/builtin/read-lines.mjs +12 -0
- package/src/agent/orchestrator/tools/builtin/read-mode-tool.mjs +455 -0
- package/src/agent/orchestrator/tools/builtin/read-open.mjs +190 -0
- package/src/agent/orchestrator/tools/builtin/read-range-index.mjs +271 -0
- package/src/agent/orchestrator/tools/builtin/read-ranges.mjs +26 -0
- package/src/agent/orchestrator/tools/builtin/read-single-tool.mjs +728 -0
- package/src/agent/orchestrator/tools/builtin/read-snapshot-runtime.mjs +173 -0
- package/src/agent/orchestrator/tools/builtin/read-special-files.mjs +268 -0
- package/src/agent/orchestrator/tools/builtin/read-streaming.mjs +602 -0
- package/src/agent/orchestrator/tools/builtin/read-tool.mjs +530 -0
- package/src/agent/orchestrator/tools/builtin/read-windows.mjs +107 -0
- package/src/agent/orchestrator/tools/builtin/rename-tool.mjs +196 -0
- package/src/agent/orchestrator/tools/builtin/rg-runner.mjs +422 -0
- package/src/agent/orchestrator/tools/builtin/search-builders.mjs +158 -0
- package/src/agent/orchestrator/tools/builtin/search-tool.mjs +869 -0
- package/src/agent/orchestrator/tools/builtin/shell-analysis.mjs +653 -0
- package/src/agent/orchestrator/tools/builtin/shell-jobs.mjs +936 -0
- package/src/agent/orchestrator/tools/builtin/shell-output.mjs +36 -0
- package/src/agent/orchestrator/tools/builtin/shell-runtime.mjs +214 -0
- package/src/agent/orchestrator/tools/builtin/snapshot-helpers.mjs +143 -0
- package/src/agent/orchestrator/tools/builtin/snapshot-store.mjs +206 -0
- package/src/agent/orchestrator/tools/builtin/snapshot-validation.mjs +98 -0
- package/src/agent/orchestrator/tools/builtin/text-stats.mjs +69 -0
- package/src/agent/orchestrator/tools/builtin/windows-roots.mjs +23 -0
- package/src/agent/orchestrator/tools/builtin/write-tool.mjs +401 -0
- package/src/agent/orchestrator/tools/builtin.mjs +500 -0
- package/src/agent/orchestrator/tools/code-graph-prewarm-worker.mjs +39 -0
- package/src/agent/orchestrator/tools/code-graph-tool-defs.mjs +24 -0
- package/src/agent/orchestrator/tools/code-graph.mjs +4095 -0
- package/src/agent/orchestrator/tools/cwd-tool.mjs +298 -0
- package/src/agent/orchestrator/tools/destructive-warning.mjs +323 -0
- package/src/agent/orchestrator/tools/edit-normalize.mjs +603 -0
- package/src/agent/orchestrator/tools/env-scrub.mjs +100 -0
- package/src/agent/orchestrator/tools/graph-binary-fetcher.mjs +144 -0
- package/src/agent/orchestrator/tools/graph-manifest.json +26 -0
- package/src/agent/orchestrator/tools/host-input.mjs +204 -0
- package/src/agent/orchestrator/tools/mutation-content-cache.mjs +67 -0
- package/src/agent/orchestrator/tools/mutation-planner.mjs +75 -0
- package/src/agent/orchestrator/tools/next-call-utils.mjs +48 -0
- package/src/agent/orchestrator/tools/patch-binary-fetcher.mjs +133 -0
- package/src/agent/orchestrator/tools/patch-manifest.json +26 -0
- package/src/agent/orchestrator/tools/patch-tool-defs.mjs +20 -0
- package/src/agent/orchestrator/tools/patch.mjs +2754 -0
- package/src/agent/orchestrator/tools/progress-message.mjs +118 -0
- package/src/agent/orchestrator/tools/result-compression.mjs +279 -0
- package/src/agent/orchestrator/tools/shell-command.mjs +865 -0
- package/src/agent/orchestrator/tools/shell-exec-policy.mjs +89 -0
- package/src/agent/orchestrator/tools/shell-policy-danger-target.mjs +27 -0
- package/src/agent/orchestrator/tools/shell-policy-imports.mjs +7 -0
- package/src/agent/orchestrator/tools/shell-policy.mjs +345 -0
- package/src/agent/orchestrator/tools/shell-snapshot.mjs +313 -0
- package/src/agent/orchestrator/workflow-store.mjs +93 -0
- package/src/agent/tool-defs.mjs +103 -0
- package/src/channels/backends/discord.mjs +784 -0
- package/src/channels/data/voice-runtime-manifest.json +138 -0
- package/src/channels/index.mjs +3229 -0
- package/src/channels/lib/cli-worker-host.mjs +12 -0
- package/src/channels/lib/config-lock.mjs +13 -0
- package/src/channels/lib/config.mjs +292 -0
- package/src/channels/lib/drop-trace.mjs +71 -0
- package/src/channels/lib/event-pipeline.mjs +81 -0
- package/src/channels/lib/event-queue.mjs +345 -0
- package/src/channels/lib/executor.mjs +168 -0
- package/src/channels/lib/format.mjs +188 -0
- package/src/channels/lib/holidays.mjs +138 -0
- package/src/channels/lib/hook-pipe-server.mjs +802 -0
- package/src/channels/lib/interaction-workflows.mjs +184 -0
- package/src/channels/lib/memory-client.mjs +149 -0
- package/src/channels/lib/output-forwarder.mjs +765 -0
- package/src/channels/lib/runtime-paths.mjs +479 -0
- package/src/channels/lib/scheduler.mjs +723 -0
- package/src/channels/lib/session-control.mjs +36 -0
- package/src/channels/lib/session-discovery.mjs +103 -0
- package/src/channels/lib/settings.mjs +11 -0
- package/src/channels/lib/state-file.mjs +68 -0
- package/src/channels/lib/status-snapshot.mjs +219 -0
- package/src/channels/lib/tool-format.mjs +140 -0
- package/src/channels/lib/transcript-discovery.mjs +195 -0
- package/src/channels/lib/voice-runtime-fetcher.mjs +734 -0
- package/src/channels/lib/webhook.mjs +1179 -0
- package/src/channels/lib/whisper-server.mjs +477 -0
- package/src/channels/tool-defs.mjs +170 -0
- package/src/daemon/host.mjs +118 -0
- package/src/daemon/mcp-transport.mjs +47 -0
- package/src/daemon/session.mjs +100 -0
- package/src/daemon/thin-client.mjs +71 -0
- package/src/daemon/transport.mjs +163 -0
- package/src/memory/data/runtime-manifest.json +40 -0
- package/src/memory/index.mjs +3305 -0
- package/src/memory/lib/agent-ipc.mjs +93 -0
- package/src/memory/lib/bridge-trace-queries.mjs +120 -0
- package/src/memory/lib/core-memory-store.mjs +330 -0
- package/src/memory/lib/embedding-provider.mjs +269 -0
- package/src/memory/lib/embedding-worker.mjs +323 -0
- package/src/memory/lib/llm-worker-host.mjs +17 -0
- package/src/memory/lib/memory-cycle.mjs +11 -0
- package/src/memory/lib/memory-cycle1.mjs +641 -0
- package/src/memory/lib/memory-cycle2.mjs +1284 -0
- package/src/memory/lib/memory-cycle3.mjs +540 -0
- package/src/memory/lib/memory-embed.mjs +299 -0
- package/src/memory/lib/memory-extraction.mjs +5 -0
- package/src/memory/lib/memory-maintenance-store.mjs +32 -0
- package/src/memory/lib/memory-ops-policy.mjs +190 -0
- package/src/memory/lib/memory-recall-id-patch.mjs +15 -0
- package/src/memory/lib/memory-recall-read-query.mjs +7 -0
- package/src/memory/lib/memory-recall-scope-filter.mjs +63 -0
- package/src/memory/lib/memory-recall-store.mjs +621 -0
- package/src/memory/lib/memory-retrievers.mjs +112 -0
- package/src/memory/lib/memory-score.mjs +71 -0
- package/src/memory/lib/memory-text-utils.mjs +58 -0
- package/src/memory/lib/memory.mjs +412 -0
- package/src/memory/lib/model-profile.mjs +85 -0
- package/src/memory/lib/pg/adapter.mjs +308 -0
- package/src/memory/lib/pg/process.mjs +360 -0
- package/src/memory/lib/pg/supervisor.mjs +396 -0
- package/src/memory/lib/project-id-resolver.mjs +86 -0
- package/src/memory/lib/runtime-fetcher.mjs +442 -0
- package/src/memory/lib/trace-store.mjs +728 -0
- package/src/memory/tool-defs.mjs +79 -0
- package/src/search/index.mjs +1173 -0
- package/src/search/lib/backends/anthropic-oauth.mjs +98 -0
- package/src/search/lib/backends/exa.mjs +50 -0
- package/src/search/lib/backends/firecrawl.mjs +61 -0
- package/src/search/lib/backends/gemini-api.mjs +83 -0
- package/src/search/lib/backends/grok-oauth.mjs +86 -0
- package/src/search/lib/backends/index.mjs +150 -0
- package/src/search/lib/backends/openai-api.mjs +144 -0
- package/src/search/lib/backends/openai-oauth.mjs +98 -0
- package/src/search/lib/backends/openai-web-search.mjs +76 -0
- package/src/search/lib/backends/tavily.mjs +55 -0
- package/src/search/lib/backends/xai-api.mjs +113 -0
- package/src/search/lib/cache.mjs +131 -0
- package/src/search/lib/config.mjs +192 -0
- package/src/search/lib/formatter.mjs +115 -0
- package/src/search/lib/provider-usage.mjs +67 -0
- package/src/search/lib/providers.mjs +47 -0
- package/src/search/lib/search-intent.mjs +109 -0
- package/src/search/lib/setup-handler.mjs +261 -0
- package/src/search/lib/state.mjs +201 -0
- package/src/search/lib/web-tools.mjs +1207 -0
- package/src/search/tool-defs.mjs +83 -0
- package/src/setup/defender-exclusion.mjs +183 -0
- package/src/shared/abort-controller.mjs +15 -0
- package/src/shared/atomic-file.mjs +420 -0
- package/src/shared/config.mjs +350 -0
- package/src/shared/daemon-recycle.mjs +108 -0
- package/src/shared/disable-claude-builtins.mjs +88 -0
- package/src/shared/err-text.mjs +12 -0
- package/src/shared/llm/cost.mjs +66 -0
- package/src/shared/llm/http-agent.mjs +123 -0
- package/src/shared/llm/index.mjs +41 -0
- package/src/shared/llm/pid-cleanup.mjs +27 -0
- package/src/shared/llm/usage-log.mjs +47 -0
- package/src/shared/plugin-paths.mjs +58 -0
- package/src/shared/schedules-store.mjs +70 -0
- package/src/shared/seed.mjs +119 -0
- package/src/shared/user-cwd.mjs +213 -0
- package/src/shared/user-data-guard.mjs +238 -0
- package/src/status/aggregator.mjs +584 -0
- package/src/status/server.mjs +413 -0
- package/tools.json +1653 -0
|
@@ -0,0 +1,593 @@
|
|
|
1
|
+
import { readdirSync } from 'fs';
|
|
2
|
+
import { basename, relative } from 'path';
|
|
3
|
+
import {
|
|
4
|
+
extractGlobBaseDirectory,
|
|
5
|
+
hasGlobMagic,
|
|
6
|
+
normalizeInputPath,
|
|
7
|
+
normalizeOutputPath,
|
|
8
|
+
resolveAgainstCwd,
|
|
9
|
+
} from './path-utils.mjs';
|
|
10
|
+
import { normalizeErrorMessage } from './path-diagnostics.mjs';
|
|
11
|
+
import { isUncPath, isWindowsDevicePath, hasUnsafeWin32Component } from './device-paths.mjs';
|
|
12
|
+
import { capShellOutput } from './shell-output.mjs';
|
|
13
|
+
import {
|
|
14
|
+
buildListCacheKey,
|
|
15
|
+
DEFAULT_IGNORE_GLOBS,
|
|
16
|
+
} from './search-builders.mjs';
|
|
17
|
+
import { markScopedCacheIncomplete } from '../../session/cache/scoped-cache-outcome.mjs';
|
|
18
|
+
import {
|
|
19
|
+
cacheGet,
|
|
20
|
+
cacheSet,
|
|
21
|
+
getCachedReadOnlyStat,
|
|
22
|
+
statPathsForMtime,
|
|
23
|
+
lstatPathsForMtime,
|
|
24
|
+
} from './cache-layers.mjs';
|
|
25
|
+
import {
|
|
26
|
+
compileSimpleGlob,
|
|
27
|
+
NOISE_DIR_NAMES,
|
|
28
|
+
walkDir,
|
|
29
|
+
} from './glob-walk.mjs';
|
|
30
|
+
import { formatMtime } from './list-formatting.mjs';
|
|
31
|
+
import { runRg } from './rg-runner.mjs';
|
|
32
|
+
import { fuzzyRank } from './fuzzy-match.mjs';
|
|
33
|
+
import { assertPathReachable } from './fs-reachability.mjs';
|
|
34
|
+
|
|
35
|
+
const FIND_WALK_TIMEOUT_MS = 20_000;
|
|
36
|
+
const LIST_WALK_TIMEOUT_MS = 20_000;
|
|
37
|
+
const LIST_ABSOLUTE_CAP = 50_000;
|
|
38
|
+
|
|
39
|
+
/** undefined / invalid / negative → defaultCap; 0 = no page cap (absolute caps still apply). */
|
|
40
|
+
function normalizeListHeadLimit(raw, defaultCap) {
|
|
41
|
+
if (raw === undefined || raw === null || raw === '') return defaultCap;
|
|
42
|
+
const n = Number(raw);
|
|
43
|
+
if (!Number.isFinite(n) || n < 0) return defaultCap;
|
|
44
|
+
return Math.floor(n);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// UNC / Windows-device / NTFS-ADS guard for directory-walking modes
|
|
48
|
+
// (list / tree / find). Walking a UNC share auto-authenticates to the
|
|
49
|
+
// remote host (NTLM hash leak); a raw-device / reserved-name path can
|
|
50
|
+
// hang or grant raw access. Mirrors the read path's string-based checks.
|
|
51
|
+
// Returns an Error string when the path is blocked, else null.
|
|
52
|
+
function listGuardPath(p) {
|
|
53
|
+
if (typeof isUncPath === 'function' && isUncPath(p))
|
|
54
|
+
return `Error: cannot walk UNC / SMB path (network credential leak risk): ${normalizeOutputPath(p)}`;
|
|
55
|
+
if (typeof isWindowsDevicePath === 'function' && isWindowsDevicePath(p))
|
|
56
|
+
return `Error: cannot walk Windows device path (reserved name or raw-device namespace): ${normalizeOutputPath(p)}`;
|
|
57
|
+
if (typeof hasUnsafeWin32Component === 'function' && hasUnsafeWin32Component(p))
|
|
58
|
+
return `Error: cannot walk Windows path with trailing dot/space or NTFS ADS suffix (bypasses device guard): ${normalizeOutputPath(p)}`;
|
|
59
|
+
return null;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export async function executeListTool(args, workDir, options = {}) {
|
|
63
|
+
if (typeof args.fuzzy === 'string' && args.fuzzy.length > 0) {
|
|
64
|
+
return executeFuzzyFind(args, workDir);
|
|
65
|
+
}
|
|
66
|
+
if (args.mode === 'tree') return executeTreeTool(args, workDir, options);
|
|
67
|
+
if (args.mode === 'find') return executeFindFilesTool(args, workDir, options);
|
|
68
|
+
args.path = normalizeInputPath(args.path);
|
|
69
|
+
if (!args.name && hasGlobMagic(args.path)) {
|
|
70
|
+
return executeFindFilesTool({ ...args, mode: 'find' }, workDir);
|
|
71
|
+
}
|
|
72
|
+
const inputPath = args.path || '.';
|
|
73
|
+
const depth = Math.min(Math.max(parseInt(args.depth ?? 1, 10) || 1, 1), 10);
|
|
74
|
+
const hidden = Boolean(args.hidden);
|
|
75
|
+
const sort = ['name', 'mtime', 'size'].includes(args.sort) ? args.sort : 'name';
|
|
76
|
+
const typeFilter = ['any', 'file', 'dir'].includes(args.type) ? args.type : 'any';
|
|
77
|
+
const headLimit = normalizeListHeadLimit(args.head_limit, 200);
|
|
78
|
+
const offset = typeof args.offset === 'number' && args.offset > 0 ? args.offset : 0;
|
|
79
|
+
const needsGlobalStat = sort === 'mtime' || sort === 'size';
|
|
80
|
+
const includeNoise = Boolean(args.include_noise);
|
|
81
|
+
const _listGuard = listGuardPath(inputPath);
|
|
82
|
+
if (_listGuard) return _listGuard;
|
|
83
|
+
const fullPath = resolveAgainstCwd(inputPath, workDir);
|
|
84
|
+
const _listGuardFull = listGuardPath(fullPath);
|
|
85
|
+
if (_listGuardFull) return _listGuardFull;
|
|
86
|
+
const cacheKey = buildListCacheKey({
|
|
87
|
+
mode: 'list',
|
|
88
|
+
inputPath: normalizeOutputPath(fullPath),
|
|
89
|
+
depth,
|
|
90
|
+
hidden,
|
|
91
|
+
sort,
|
|
92
|
+
typeFilter,
|
|
93
|
+
headLimit,
|
|
94
|
+
offset,
|
|
95
|
+
includeNoise,
|
|
96
|
+
});
|
|
97
|
+
const cached = cacheGet(cacheKey);
|
|
98
|
+
if (cached !== null) return cached;
|
|
99
|
+
try { await assertPathReachable(fullPath); }
|
|
100
|
+
catch (err) { return `Error: ${normalizeErrorMessage(err instanceof Error ? err.message : String(err))}`; }
|
|
101
|
+
let st;
|
|
102
|
+
try { st = getCachedReadOnlyStat(fullPath); }
|
|
103
|
+
catch (err) { return `Error: ${normalizeErrorMessage(err instanceof Error ? err.message : String(err))}`; }
|
|
104
|
+
if (!st.isDirectory()) return `Error: not a directory — ${normalizeOutputPath(fullPath)}`;
|
|
105
|
+
|
|
106
|
+
const rows = [];
|
|
107
|
+
// Width guard: depth is capped above, but a single very wide directory
|
|
108
|
+
// tree could push unbounded rows before sort/slice runs and exhaust
|
|
109
|
+
// memory. Mirror the find-mode FIND_ABSOLUTE_CAP + walk deadline so the
|
|
110
|
+
// accumulator stops growing once the cap or timeout trips. Small dirs
|
|
111
|
+
// never hit either bound, so normal behavior is unchanged.
|
|
112
|
+
let truncatedByCap = false;
|
|
113
|
+
const walkDeadline = Date.now() + LIST_WALK_TIMEOUT_MS;
|
|
114
|
+
walkDir(fullPath, {
|
|
115
|
+
hidden,
|
|
116
|
+
maxDepth: depth,
|
|
117
|
+
excludeDirNames: includeNoise ? null : NOISE_DIR_NAMES,
|
|
118
|
+
visit: (ent, entPath) => {
|
|
119
|
+
if (Date.now() > walkDeadline) { truncatedByCap = true; return false; }
|
|
120
|
+
const isDir = ent.isDirectory();
|
|
121
|
+
const isFile = ent.isFile();
|
|
122
|
+
if (typeFilter === 'file' && !isFile) return;
|
|
123
|
+
if (typeFilter === 'dir' && !isDir) return;
|
|
124
|
+
const entType = isDir ? 'dir' : (isFile ? 'file' : (ent.isSymbolicLink() ? 'symlink' : 'other'));
|
|
125
|
+
rows.push({
|
|
126
|
+
path: entPath,
|
|
127
|
+
type: entType,
|
|
128
|
+
size: 0,
|
|
129
|
+
mtimeMs: 0,
|
|
130
|
+
fullPath: entPath,
|
|
131
|
+
});
|
|
132
|
+
if (rows.length >= LIST_ABSOLUTE_CAP) {
|
|
133
|
+
truncatedByCap = true;
|
|
134
|
+
return false;
|
|
135
|
+
}
|
|
136
|
+
// Pre-sort truncation removed: a global name sort needs all
|
|
137
|
+
// candidates collected before slicing, otherwise the visible
|
|
138
|
+
// window depends on traversal order rather than sort order.
|
|
139
|
+
},
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
if (needsGlobalStat && rows.length > 0) {
|
|
143
|
+
// lstat: symlinks should report own metadata, not the target's.
|
|
144
|
+
const stats = await lstatPathsForMtime(rows.map((row) => row.fullPath), workDir, 64, { deadlineMs: 5000 });
|
|
145
|
+
for (let i = 0; i < rows.length; i++) {
|
|
146
|
+
const item = stats[i];
|
|
147
|
+
if (!item?.stat) continue;
|
|
148
|
+
rows[i].size = item.size;
|
|
149
|
+
rows[i].mtimeMs = item.mtimeMs;
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
if (sort === 'mtime') rows.sort((a, b) => b.mtimeMs - a.mtimeMs);
|
|
154
|
+
else if (sort === 'size') rows.sort((a, b) => b.size - a.size);
|
|
155
|
+
else rows.sort((a, b) => a.path.localeCompare(b.path));
|
|
156
|
+
|
|
157
|
+
const windowed = offset > 0 ? rows.slice(offset) : rows;
|
|
158
|
+
const sliced = headLimit > 0 ? windowed.slice(0, headLimit) : windowed;
|
|
159
|
+
if (!needsGlobalStat && sliced.length > 0) {
|
|
160
|
+
// Use lstat so a symlink reports its own size/mtime instead of
|
|
161
|
+
// the target's. The walker already typed symlinks from Dirent;
|
|
162
|
+
// following the link here would lie about the listed entry.
|
|
163
|
+
const stats = await lstatPathsForMtime(sliced.map((row) => row.fullPath), workDir, 64, { deadlineMs: 5000 });
|
|
164
|
+
for (let i = 0; i < sliced.length; i++) {
|
|
165
|
+
const item = stats[i];
|
|
166
|
+
if (!item?.stat) continue;
|
|
167
|
+
sliced[i].size = item.size;
|
|
168
|
+
sliced[i].mtimeMs = item.mtimeMs;
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
const lines = sliced.map(r =>
|
|
172
|
+
`${normalizeOutputPath(r.path)}\t${r.type}\t${r.size}\t${formatMtime(r.mtimeMs)}`);
|
|
173
|
+
if (windowed.length > sliced.length) lines.push(`... [entries ${offset + 1}-${offset + sliced.length} of ${rows.length}; pass offset:${offset + sliced.length} to continue]`);
|
|
174
|
+
if (truncatedByCap) lines.push(`... walk truncated at ${LIST_ABSOLUTE_CAP} rows or ${LIST_WALK_TIMEOUT_MS}ms timeout; narrow the path or lower depth for a complete listing`);
|
|
175
|
+
let emptyMsg = '(empty directory)';
|
|
176
|
+
if (lines.length === 0 && (typeFilter !== 'any' || hidden === false)) {
|
|
177
|
+
const filterParts = [];
|
|
178
|
+
if (typeFilter !== 'any') filterParts.push(`type=${typeFilter}`);
|
|
179
|
+
if (hidden === false) {
|
|
180
|
+
let hasHidden = false;
|
|
181
|
+
try {
|
|
182
|
+
const entries = readdirSync(fullPath, { withFileTypes: true });
|
|
183
|
+
hasHidden = entries.some(e => e.name && e.name.startsWith('.'));
|
|
184
|
+
} catch {}
|
|
185
|
+
if (hasHidden) filterParts.push(`hidden=false (dotfiles present — pass hidden:true to include)`);
|
|
186
|
+
else filterParts.push(`hidden=false`);
|
|
187
|
+
}
|
|
188
|
+
emptyMsg = `(no entries match filter) ${filterParts.join(', ')} path=${inputPath}`;
|
|
189
|
+
}
|
|
190
|
+
const out = lines.join('\n') || emptyMsg;
|
|
191
|
+
if (options?.scopedCacheOutcome && (truncatedByCap || windowed.length > sliced.length)) {
|
|
192
|
+
markScopedCacheIncomplete(options.scopedCacheOutcome);
|
|
193
|
+
}
|
|
194
|
+
cacheSet(cacheKey, out, { scopes: [fullPath] });
|
|
195
|
+
// ② completion progress (claude "Found N" parity). Best-effort, no-op
|
|
196
|
+
// when onProgress is absent (no progressToken).
|
|
197
|
+
if (typeof options?.onProgress === 'function') {
|
|
198
|
+
try { options.onProgress(`${windowed.length} entries`); } catch { /* best-effort */ }
|
|
199
|
+
}
|
|
200
|
+
return out;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
export async function executeTreeTool(args, workDir, options = {}) {
|
|
204
|
+
args.path = normalizeInputPath(args.path);
|
|
205
|
+
const inputPath = args.path || '.';
|
|
206
|
+
const depth = Math.min(Math.max(parseInt(args.depth ?? 3, 10) || 3, 1), 6);
|
|
207
|
+
const hidden = Boolean(args.hidden);
|
|
208
|
+
const headLimit = normalizeListHeadLimit(args.head_limit, 200);
|
|
209
|
+
const offset = typeof args.offset === 'number' && args.offset > 0 ? args.offset : 0;
|
|
210
|
+
const includeNoise = Boolean(args.include_noise);
|
|
211
|
+
const _treeGuard = listGuardPath(inputPath);
|
|
212
|
+
if (_treeGuard) return _treeGuard;
|
|
213
|
+
const fullPath = resolveAgainstCwd(inputPath, workDir);
|
|
214
|
+
const _treeGuardFull = listGuardPath(fullPath);
|
|
215
|
+
if (_treeGuardFull) return _treeGuardFull;
|
|
216
|
+
const cacheKey = buildListCacheKey({
|
|
217
|
+
mode: 'tree',
|
|
218
|
+
inputPath: normalizeOutputPath(fullPath),
|
|
219
|
+
depth,
|
|
220
|
+
hidden,
|
|
221
|
+
sort: '',
|
|
222
|
+
typeFilter: '',
|
|
223
|
+
headLimit,
|
|
224
|
+
offset,
|
|
225
|
+
includeNoise,
|
|
226
|
+
});
|
|
227
|
+
const cached = cacheGet(cacheKey);
|
|
228
|
+
if (cached !== null) return cached;
|
|
229
|
+
try { await assertPathReachable(fullPath); }
|
|
230
|
+
catch (err) { return `Error: ${normalizeErrorMessage(err instanceof Error ? err.message : String(err))}`; }
|
|
231
|
+
let st;
|
|
232
|
+
try { st = getCachedReadOnlyStat(fullPath); }
|
|
233
|
+
catch (err) { return `Error: ${normalizeErrorMessage(err instanceof Error ? err.message : String(err))}`; }
|
|
234
|
+
if (!st.isDirectory()) return `Error: not a directory — ${normalizeOutputPath(fullPath)}`;
|
|
235
|
+
const lines = [`${normalizeOutputPath(fullPath)}/`];
|
|
236
|
+
const prefixStack = [''];
|
|
237
|
+
const TREE_BRANCH_LINE_CAP = 500;
|
|
238
|
+
walkDir(fullPath, {
|
|
239
|
+
hidden,
|
|
240
|
+
maxDepth: depth,
|
|
241
|
+
excludeDirNames: includeNoise ? null : NOISE_DIR_NAMES,
|
|
242
|
+
sort: (a, b) => {
|
|
243
|
+
const ad = a.isDirectory(), bd = b.isDirectory();
|
|
244
|
+
if (ad !== bd) return ad ? -1 : 1;
|
|
245
|
+
return a.name.localeCompare(b.name);
|
|
246
|
+
},
|
|
247
|
+
visit: (ent, _entPath, ctx) => {
|
|
248
|
+
const prefix = prefixStack[ctx.depth - 1] || '';
|
|
249
|
+
const branch = ctx.isLast ? '└── ' : '├── ';
|
|
250
|
+
const display = ent.isDirectory() ? `${ent.name}/` : ent.name;
|
|
251
|
+
lines.push(`${prefix}${branch}${display}`);
|
|
252
|
+
if (ent.isDirectory()) {
|
|
253
|
+
prefixStack[ctx.depth] = prefix + (ctx.isLast ? ' ' : '│ ');
|
|
254
|
+
}
|
|
255
|
+
if (headLimit !== 0) {
|
|
256
|
+
const gatherLimit = headLimit > 0
|
|
257
|
+
? offset + headLimit + 1
|
|
258
|
+
: offset + TREE_BRANCH_LINE_CAP + 1;
|
|
259
|
+
// Exclude the root line (lines[0]) from the body-row count:
|
|
260
|
+
// the windowed slice operates on lines.slice(1), so gather
|
|
261
|
+
// must measure body rows, not total. Without -1 the sentinel
|
|
262
|
+
// "+N more entries" misfires off-by-one on the boundary.
|
|
263
|
+
if (lines.length - 1 >= gatherLimit) return false;
|
|
264
|
+
}
|
|
265
|
+
},
|
|
266
|
+
});
|
|
267
|
+
const root = lines[0];
|
|
268
|
+
const body = lines.slice(1);
|
|
269
|
+
const windowed = offset > 0 ? body.slice(offset) : body;
|
|
270
|
+
// head_limit:0 means "no cap" (Infinity); negative/NaN means "use default cap".
|
|
271
|
+
const branchLimit = headLimit === 0
|
|
272
|
+
? Infinity
|
|
273
|
+
: (headLimit > 0 ? headLimit : TREE_BRANCH_LINE_CAP);
|
|
274
|
+
const sliced = branchLimit === Infinity ? windowed : windowed.slice(0, branchLimit);
|
|
275
|
+
const outLines = [root, ...sliced];
|
|
276
|
+
if (windowed.length > sliced.length) {
|
|
277
|
+
// The walk stops gathering at gatherLimit, so when body filled to the
|
|
278
|
+
// cap the true total is unknown — render `N+` so the caller keeps
|
|
279
|
+
// paging instead of reading the capped count as the real total.
|
|
280
|
+
const gatherCap = headLimit > 0 ? offset + headLimit + 1 : offset + TREE_BRANCH_LINE_CAP + 1;
|
|
281
|
+
const totalLabel = body.length >= gatherCap ? `${body.length}+` : `${body.length}`;
|
|
282
|
+
outLines.push(`... [entries ${offset + 1}-${offset + sliced.length} of ${totalLabel}; pass offset:${offset + sliced.length} to continue]`);
|
|
283
|
+
}
|
|
284
|
+
const TREE_OUTPUT_CHAR_CAP = 50_000;
|
|
285
|
+
let out = outLines.join('\n');
|
|
286
|
+
let outputCharTruncated = false;
|
|
287
|
+
if (out.length > TREE_OUTPUT_CHAR_CAP) {
|
|
288
|
+
outputCharTruncated = true;
|
|
289
|
+
out = out.slice(0, TREE_OUTPUT_CHAR_CAP) + `\n... [output truncated at ${Math.round(TREE_OUTPUT_CHAR_CAP/1024)} KB; narrow path or lower depth]`;
|
|
290
|
+
}
|
|
291
|
+
if (options?.scopedCacheOutcome && (windowed.length > sliced.length || outputCharTruncated)) {
|
|
292
|
+
markScopedCacheIncomplete(options.scopedCacheOutcome);
|
|
293
|
+
}
|
|
294
|
+
cacheSet(cacheKey, out, { scopes: [fullPath] });
|
|
295
|
+
return out;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
// Fuzzy filename search (codex file-search / nucleo style): collect the file
|
|
299
|
+
// list via `rg --files`, then rank by subsequence score. Lets a partial name
|
|
300
|
+
// like "edeng" surface "edit-engine.mjs" in one call instead of guessing an
|
|
301
|
+
// exact glob pattern. Honors path/hidden/include_noise/depth/head_limit.
|
|
302
|
+
async function executeFuzzyFind(args, workDir) {
|
|
303
|
+
const query = String(args.fuzzy);
|
|
304
|
+
const inputPath = normalizeInputPath(args.path) || '.';
|
|
305
|
+
const guard = listGuardPath(inputPath);
|
|
306
|
+
if (guard) return guard;
|
|
307
|
+
const fullPath = resolveAgainstCwd(inputPath, workDir);
|
|
308
|
+
const guardFull = listGuardPath(fullPath);
|
|
309
|
+
if (guardFull) return guardFull;
|
|
310
|
+
const hidden = Boolean(args.hidden);
|
|
311
|
+
const includeNoise = Boolean(args.include_noise);
|
|
312
|
+
// head_limit:0 means "no cap" per list semantics — keep 0 distinct from default.
|
|
313
|
+
const headLimit = normalizeListHeadLimit(args.head_limit, 40);
|
|
314
|
+
const depth = args.depth != null ? Math.max(parseInt(args.depth, 10) || 1, 1) : null;
|
|
315
|
+
const rgArgs = ['--files', '--no-ignore'];
|
|
316
|
+
if (hidden) rgArgs.push('--hidden');
|
|
317
|
+
if (depth != null) rgArgs.push('--max-depth', String(depth));
|
|
318
|
+
if (!includeNoise) {
|
|
319
|
+
for (const ex of DEFAULT_IGNORE_GLOBS) rgArgs.push('--glob', ex);
|
|
320
|
+
}
|
|
321
|
+
rgArgs.push('.');
|
|
322
|
+
let stdout;
|
|
323
|
+
try {
|
|
324
|
+
stdout = await runRg(rgArgs, { cwd: fullPath });
|
|
325
|
+
} catch (err) {
|
|
326
|
+
return `Error: ${normalizeErrorMessage(err instanceof Error ? err.message : String(err))}`;
|
|
327
|
+
}
|
|
328
|
+
const items = String(stdout)
|
|
329
|
+
.split('\n')
|
|
330
|
+
// Strip only the trailing CR from rg's line split — do NOT trim, or a
|
|
331
|
+
// filename with leading/trailing spaces would be corrupted.
|
|
332
|
+
.map((p) => (p.endsWith('\r') ? p.slice(0, -1) : p))
|
|
333
|
+
.filter((p) => p.length > 0)
|
|
334
|
+
.map((p) => ({ path: normalizeOutputPath(p.replace(/^\.[/\\]/, '')) }));
|
|
335
|
+
const ranked = fuzzyRank(query, items, headLimit);
|
|
336
|
+
if (ranked.length === 0) return `(no fuzzy match for "${query}")`;
|
|
337
|
+
const out = ranked.map((r) => r.item.path).join('\n');
|
|
338
|
+
return headLimit > 0 && ranked.length >= headLimit
|
|
339
|
+
? `${out}\n... (top ${headLimit}; raise head_limit for more)`
|
|
340
|
+
: out;
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
export async function executeFindFilesTool(args, workDir, options = {}) {
|
|
344
|
+
args.path = normalizeInputPath(args.path);
|
|
345
|
+
let inputPath = args.path || '.';
|
|
346
|
+
let namePattern = typeof args.name === 'string' ? args.name : null;
|
|
347
|
+
if (!namePattern && hasGlobMagic(inputPath)) {
|
|
348
|
+
const { baseDir, relativePattern } = extractGlobBaseDirectory(inputPath);
|
|
349
|
+
inputPath = baseDir || '.';
|
|
350
|
+
namePattern = relativePattern.replace(/^\/+/, '');
|
|
351
|
+
}
|
|
352
|
+
if (namePattern) namePattern = normalizeInputPath(namePattern).replace(/^\/+/, '');
|
|
353
|
+
const typeFilter = ['any', 'file', 'dir'].includes(args.type) ? args.type : 'any';
|
|
354
|
+
const sortMode = ['name', 'size', 'mtime'].includes(args.sort) ? args.sort : 'mtime';
|
|
355
|
+
const minSize = typeof args.min_size === 'number' && args.min_size > 0 ? args.min_size : null;
|
|
356
|
+
const maxSize = typeof args.max_size === 'number' && args.max_size >= 0 ? args.max_size : null;
|
|
357
|
+
const headLimit = normalizeListHeadLimit(args.head_limit, 100);
|
|
358
|
+
const offset = typeof args.offset === 'number' && args.offset > 0 ? args.offset : 0;
|
|
359
|
+
const includeNoise = Boolean(args.include_noise);
|
|
360
|
+
const hidden = Boolean(args.hidden);
|
|
361
|
+
// Clamp depth to >=1 when caller passes it; null means unbounded (legacy
|
|
362
|
+
// find-mode behavior). Forwarded to walkDir.maxDepth and the rg fast
|
|
363
|
+
// path's --max-depth so both code paths honor the cap consistently.
|
|
364
|
+
const depth = args.depth != null
|
|
365
|
+
? Math.max(parseInt(args.depth, 10) || 1, 1)
|
|
366
|
+
: null;
|
|
367
|
+
const _findGuard = listGuardPath(inputPath);
|
|
368
|
+
if (_findGuard) return _findGuard;
|
|
369
|
+
const fullPath = resolveAgainstCwd(inputPath, workDir);
|
|
370
|
+
const _findGuardFull = listGuardPath(fullPath);
|
|
371
|
+
if (_findGuardFull) return _findGuardFull;
|
|
372
|
+
const cacheKey = buildListCacheKey({
|
|
373
|
+
mode: 'find',
|
|
374
|
+
inputPath: normalizeOutputPath(fullPath),
|
|
375
|
+
depth: depth ?? '',
|
|
376
|
+
hidden,
|
|
377
|
+
sort: sortMode,
|
|
378
|
+
typeFilter,
|
|
379
|
+
headLimit,
|
|
380
|
+
offset,
|
|
381
|
+
namePattern,
|
|
382
|
+
minSize,
|
|
383
|
+
maxSize,
|
|
384
|
+
modifiedAfter: args.modified_after || '',
|
|
385
|
+
modifiedBefore: args.modified_before || '',
|
|
386
|
+
includeNoise,
|
|
387
|
+
});
|
|
388
|
+
const cached = cacheGet(cacheKey);
|
|
389
|
+
if (cached !== null) return cached;
|
|
390
|
+
|
|
391
|
+
const parseTime = (v) => {
|
|
392
|
+
if (typeof v !== 'string') return null;
|
|
393
|
+
const m = v.match(/^(\d+)([hdm])$/);
|
|
394
|
+
if (m) {
|
|
395
|
+
const n = parseInt(m[1], 10);
|
|
396
|
+
const unit = m[2] === 'h' ? 3600 * 1000
|
|
397
|
+
: m[2] === 'd' ? 86400 * 1000
|
|
398
|
+
: 60 * 1000;
|
|
399
|
+
return Date.now() - n * unit;
|
|
400
|
+
}
|
|
401
|
+
const t = Date.parse(v);
|
|
402
|
+
return isNaN(t) ? null : t;
|
|
403
|
+
};
|
|
404
|
+
const after = parseTime(args.modified_after);
|
|
405
|
+
const before = parseTime(args.modified_before);
|
|
406
|
+
// An unparseable date must FAIL, not silently disable the filter — a
|
|
407
|
+
// caller who passed a filter believes the listing is filtered.
|
|
408
|
+
if (args.modified_after && after === null) {
|
|
409
|
+
return `Error: invalid modified_after ${JSON.stringify(args.modified_after)}; expected an ISO date/time or a relative window like 90m / 12h / 7d`;
|
|
410
|
+
}
|
|
411
|
+
if (args.modified_before && before === null) {
|
|
412
|
+
return `Error: invalid modified_before ${JSON.stringify(args.modified_before)}; expected an ISO date/time or a relative window like 90m / 12h / 7d`;
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
// `name` is documented as a SUBSTRING filter (use glob mode for patterns).
|
|
416
|
+
// Compile it as a glob ONLY when it actually contains glob metacharacters;
|
|
417
|
+
// otherwise match by case-insensitive contains. The bug was that EVERY name
|
|
418
|
+
// was glob-compiled, so a plain fragment like ".mjs" anchored-matched nothing.
|
|
419
|
+
const nameIsGlob = Boolean(namePattern && /[*?\[\]{}]/.test(namePattern));
|
|
420
|
+
let nameRegex = null, nameRootOptionalRegex = null;
|
|
421
|
+
if (namePattern && nameIsGlob) {
|
|
422
|
+
try {
|
|
423
|
+
// compileSimpleGlob throws (R16 DoS caps: >256 brace variants /
|
|
424
|
+
// oversized pattern/regex body) — convert to a tool-error string.
|
|
425
|
+
nameRegex = compileSimpleGlob(namePattern);
|
|
426
|
+
nameRootOptionalRegex = namePattern.startsWith('**/')
|
|
427
|
+
? compileSimpleGlob(namePattern.slice(3))
|
|
428
|
+
: null;
|
|
429
|
+
} catch (err) {
|
|
430
|
+
return `Error: ${normalizeErrorMessage(err instanceof Error ? err.message : String(err))}`;
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
const nameLower = namePattern ? namePattern.toLowerCase() : null;
|
|
434
|
+
const namePatternHasPath = Boolean(namePattern && /[\\/]/.test(namePattern));
|
|
435
|
+
const matchesFindNamePattern = (entName, entPath) => {
|
|
436
|
+
if (!namePattern) return true;
|
|
437
|
+
const subject = namePatternHasPath
|
|
438
|
+
? normalizeOutputPath(relative(fullPath, entPath))
|
|
439
|
+
: entName;
|
|
440
|
+
if (nameIsGlob) return nameRegex.test(subject) || Boolean(nameRootOptionalRegex?.test(subject));
|
|
441
|
+
return subject.toLowerCase().includes(nameLower);
|
|
442
|
+
};
|
|
443
|
+
|
|
444
|
+
try { await assertPathReachable(fullPath); }
|
|
445
|
+
catch (err) { return `Error: ${normalizeErrorMessage(err instanceof Error ? err.message : String(err))}`; }
|
|
446
|
+
let rootStat;
|
|
447
|
+
try { rootStat = getCachedReadOnlyStat(fullPath); }
|
|
448
|
+
catch (err) { return `Error: ${normalizeErrorMessage(err instanceof Error ? err.message : String(err))}`; }
|
|
449
|
+
if (!rootStat.isDirectory()) return `Error: not a directory — ${normalizeOutputPath(fullPath)}`;
|
|
450
|
+
|
|
451
|
+
const matches = [];
|
|
452
|
+
const FIND_ABSOLUTE_CAP = 50_000;
|
|
453
|
+
let truncatedByCap = false;
|
|
454
|
+
let rgStdoutTruncated = false;
|
|
455
|
+
let rgStdoutPartial = false;
|
|
456
|
+
const useBatchedStat = minSize === null && maxSize === null && after === null && before === null;
|
|
457
|
+
let handledByRgFiles = false;
|
|
458
|
+
if (useBatchedStat && typeFilter === 'file') {
|
|
459
|
+
try {
|
|
460
|
+
// --no-ignore: do not consult .gitignore. The slow walk path
|
|
461
|
+
// never honours .gitignore, so the fast path must match that
|
|
462
|
+
// contract — otherwise the rg branch silently returns fewer
|
|
463
|
+
// results than the fallback. Noise dirs are still excluded
|
|
464
|
+
// via DEFAULT_IGNORE_GLOBS below (unless include_noise).
|
|
465
|
+
const rgArgs = ['--files', '--no-ignore'];
|
|
466
|
+
if (hidden) rgArgs.push('--hidden');
|
|
467
|
+
if (depth != null) rgArgs.push('--max-depth', String(depth));
|
|
468
|
+
if (!includeNoise) {
|
|
469
|
+
for (const ex of DEFAULT_IGNORE_GLOBS) rgArgs.push('--glob', ex);
|
|
470
|
+
}
|
|
471
|
+
// Substring `name` (no glob metachars) → contains-glob so rg's
|
|
472
|
+
// pre-filter matches the JS matcher; explicit globs pass through.
|
|
473
|
+
if (namePattern) rgArgs.push('--iglob', nameIsGlob ? namePattern : `*${namePattern}*`);
|
|
474
|
+
rgArgs.push('.');
|
|
475
|
+
const stdout = await runRg(rgArgs, { cwd: fullPath });
|
|
476
|
+
rgStdoutTruncated = Boolean(stdout && typeof stdout === 'object' && stdout.truncated);
|
|
477
|
+
rgStdoutPartial = Boolean(stdout && typeof stdout === 'object' && stdout.partial);
|
|
478
|
+
const candidates = [];
|
|
479
|
+
for (const line of String(stdout).split('\n')) {
|
|
480
|
+
const trimmed = line.trim();
|
|
481
|
+
if (!trimmed) continue;
|
|
482
|
+
const candidate = resolveAgainstCwd(normalizeInputPath(trimmed), fullPath);
|
|
483
|
+
if (!matchesFindNamePattern(basename(candidate), candidate)) continue;
|
|
484
|
+
candidates.push(candidate);
|
|
485
|
+
if (candidates.length >= FIND_ABSOLUTE_CAP) {
|
|
486
|
+
truncatedByCap = true;
|
|
487
|
+
break;
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
const withStat = await statPathsForMtime(candidates, workDir, 64, { deadlineMs: 5000 });
|
|
491
|
+
for (const item of withStat) {
|
|
492
|
+
if (!item?.stat) continue;
|
|
493
|
+
matches.push({ path: item.full, size: item.size, mtimeMs: item.mtimeMs });
|
|
494
|
+
}
|
|
495
|
+
handledByRgFiles = true;
|
|
496
|
+
} catch {
|
|
497
|
+
handledByRgFiles = false;
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
if (!handledByRgFiles && useBatchedStat) {
|
|
501
|
+
const candidates = [];
|
|
502
|
+
const walkDeadline1 = Date.now() + FIND_WALK_TIMEOUT_MS;
|
|
503
|
+
walkDir(fullPath, {
|
|
504
|
+
hidden,
|
|
505
|
+
maxDepth: depth ?? Infinity,
|
|
506
|
+
excludeDirNames: includeNoise ? null : NOISE_DIR_NAMES,
|
|
507
|
+
visit: (ent, entPath) => {
|
|
508
|
+
if (Date.now() > walkDeadline1) { truncatedByCap = true; return false; }
|
|
509
|
+
const isDir = ent.isDirectory();
|
|
510
|
+
const isFile = ent.isFile();
|
|
511
|
+
if (typeFilter === 'file' && !isFile) return;
|
|
512
|
+
if (typeFilter === 'dir' && !isDir) return;
|
|
513
|
+
if (!matchesFindNamePattern(ent.name, entPath)) return;
|
|
514
|
+
candidates.push(entPath);
|
|
515
|
+
if (candidates.length >= FIND_ABSOLUTE_CAP) {
|
|
516
|
+
truncatedByCap = true;
|
|
517
|
+
return false;
|
|
518
|
+
}
|
|
519
|
+
},
|
|
520
|
+
});
|
|
521
|
+
const withStat = await statPathsForMtime(candidates, workDir, 64, { deadlineMs: 5000 });
|
|
522
|
+
for (const item of withStat) {
|
|
523
|
+
if (!item?.stat) continue;
|
|
524
|
+
matches.push({ path: item.full, size: item.size, mtimeMs: item.mtimeMs });
|
|
525
|
+
}
|
|
526
|
+
} else if (!handledByRgFiles) {
|
|
527
|
+
// Size filters only have meaning for files; when the caller passed
|
|
528
|
+
// min_size/max_size without also restricting type, narrow the
|
|
529
|
+
// result set to files so directories don't slip past with their
|
|
530
|
+
// (usually 0-byte) directory size.
|
|
531
|
+
const sizeFiltered = (minSize !== null || maxSize !== null);
|
|
532
|
+
const effectiveTypeFilter = sizeFiltered && typeFilter === 'any' ? 'file' : typeFilter;
|
|
533
|
+
const candidates = [];
|
|
534
|
+
const walkDeadline2 = Date.now() + FIND_WALK_TIMEOUT_MS;
|
|
535
|
+
walkDir(fullPath, {
|
|
536
|
+
hidden,
|
|
537
|
+
maxDepth: depth ?? Infinity,
|
|
538
|
+
excludeDirNames: includeNoise ? null : NOISE_DIR_NAMES,
|
|
539
|
+
visit: (ent, entPath) => {
|
|
540
|
+
if (Date.now() > walkDeadline2) { truncatedByCap = true; return false; }
|
|
541
|
+
const isDir = ent.isDirectory();
|
|
542
|
+
const isFile = ent.isFile();
|
|
543
|
+
if (effectiveTypeFilter === 'file' && !isFile) return;
|
|
544
|
+
if (effectiveTypeFilter === 'dir' && !isDir) return;
|
|
545
|
+
if (!matchesFindNamePattern(ent.name, entPath)) return;
|
|
546
|
+
candidates.push(entPath);
|
|
547
|
+
if (candidates.length >= FIND_ABSOLUTE_CAP) {
|
|
548
|
+
truncatedByCap = true;
|
|
549
|
+
return false;
|
|
550
|
+
}
|
|
551
|
+
},
|
|
552
|
+
});
|
|
553
|
+
const withStat = await statPathsForMtime(candidates, workDir, 64, { deadlineMs: 5000 });
|
|
554
|
+
for (const item of withStat) {
|
|
555
|
+
if (!item?.stat) continue;
|
|
556
|
+
const { stat, full: entPath, mtimeMs } = item;
|
|
557
|
+
if (stat.isFile()) {
|
|
558
|
+
if (minSize !== null && stat.size < minSize) continue;
|
|
559
|
+
if (maxSize !== null && stat.size > maxSize) continue;
|
|
560
|
+
}
|
|
561
|
+
if (after !== null && mtimeMs < after) continue;
|
|
562
|
+
if (before !== null && mtimeMs > before) continue;
|
|
563
|
+
matches.push({ path: entPath, size: stat.size, mtimeMs });
|
|
564
|
+
if (matches.length >= FIND_ABSOLUTE_CAP) {
|
|
565
|
+
truncatedByCap = true;
|
|
566
|
+
break;
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
matches.sort((a, b) => {
|
|
572
|
+
if (sortMode === 'name') return normalizeOutputPath(a.path).localeCompare(normalizeOutputPath(b.path));
|
|
573
|
+
if (sortMode === 'size') return b.size - a.size;
|
|
574
|
+
return b.mtimeMs - a.mtimeMs;
|
|
575
|
+
});
|
|
576
|
+
const windowed = offset > 0 ? matches.slice(offset) : matches;
|
|
577
|
+
const sliced = headLimit > 0 ? windowed.slice(0, headLimit) : windowed;
|
|
578
|
+
const lines = sliced.map(m =>
|
|
579
|
+
`${normalizeOutputPath(m.path)}\t${m.size}\t${formatMtime(m.mtimeMs)}`);
|
|
580
|
+
if (windowed.length > sliced.length) lines.push(`... [entries ${offset + 1}-${offset + sliced.length} of ${matches.length}; pass offset:${offset + sliced.length} to continue]`);
|
|
581
|
+
if (rgStdoutTruncated) lines.push('... [warning] rg stdout truncated at 20MB cap; results incomplete');
|
|
582
|
+
if (rgStdoutPartial) lines.push('... [warning] rg exit 2 (partial results); listing may be incomplete');
|
|
583
|
+
if (truncatedByCap) lines.push(`... walk truncated at ${FIND_ABSOLUTE_CAP} matches; narrow the scope (path/name/modified_after) for accurate global sort`);
|
|
584
|
+
const out = lines.join('\n') || '(no matches)';
|
|
585
|
+
if (options?.scopedCacheOutcome && (truncatedByCap || rgStdoutTruncated || rgStdoutPartial || windowed.length > sliced.length)) {
|
|
586
|
+
markScopedCacheIncomplete(options.scopedCacheOutcome);
|
|
587
|
+
}
|
|
588
|
+
const findIncomplete = truncatedByCap || rgStdoutTruncated || rgStdoutPartial || windowed.length > sliced.length;
|
|
589
|
+
if (!findIncomplete) {
|
|
590
|
+
cacheSet(cacheKey, out, { scopes: [fullPath] });
|
|
591
|
+
}
|
|
592
|
+
return out;
|
|
593
|
+
}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import { existsSync } from 'fs';
|
|
2
|
+
import { dirname, join, resolve } from 'path';
|
|
3
|
+
import { performance } from 'perf_hooks';
|
|
4
|
+
import { fileURLToPath } from 'url';
|
|
5
|
+
import { snapshotCoversFullFile } from './snapshot-helpers.mjs';
|
|
6
|
+
import { getPluginData } from '../../config.mjs';
|
|
7
|
+
import { findCachedPatchBinary } from '../patch-binary-fetcher.mjs';
|
|
8
|
+
import { runServerEdit } from '../patch.mjs';
|
|
9
|
+
|
|
10
|
+
const PLUGIN_ROOT = process.env.CLAUDE_PLUGIN_ROOT
|
|
11
|
+
|| resolve(dirname(fileURLToPath(import.meta.url)), '../../../../..');
|
|
12
|
+
const NATIVE_EDIT_DEFAULT_BIN = join(
|
|
13
|
+
PLUGIN_ROOT,
|
|
14
|
+
'native/mixdog-patch/target/release',
|
|
15
|
+
process.platform === 'win32' ? 'mixdog-patch.exe' : 'mixdog-patch',
|
|
16
|
+
);
|
|
17
|
+
|
|
18
|
+
export function nativeEditMode() {
|
|
19
|
+
return String(process.env.MIXDOG_EDIT_NATIVE || 'auto').toLowerCase();
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function nativeEditBinPath() {
|
|
23
|
+
const override = process.env.MIXDOG_EDIT_NATIVE_BIN || process.env.MIXDOG_PATCH_NATIVE_BIN;
|
|
24
|
+
if (override) return override;
|
|
25
|
+
if (existsSync(NATIVE_EDIT_DEFAULT_BIN)) return NATIVE_EDIT_DEFAULT_BIN;
|
|
26
|
+
return findCachedPatchBinary(getPluginData()) || NATIVE_EDIT_DEFAULT_BIN;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function nativeEditShouldAttempt({ editSnapshot, oldStr, newStr, preloadedContent, preloadedRawBuf }) {
|
|
30
|
+
const mode = nativeEditMode();
|
|
31
|
+
if (/^(0|false|no|off|js|legacy)$/i.test(mode)) return false;
|
|
32
|
+
if (!existsSync(nativeEditBinPath())) return false;
|
|
33
|
+
if (!snapshotCoversFullFile(editSnapshot)) return false;
|
|
34
|
+
if (preloadedContent !== null || preloadedRawBuf !== null) return false;
|
|
35
|
+
if (typeof oldStr !== 'string' || oldStr.length === 0 || typeof newStr !== 'string') return false;
|
|
36
|
+
if (/^(1|true|yes|on|native)$/i.test(mode)) return true;
|
|
37
|
+
// auto: the persistent server removed per-call spawn cost, so route edits to
|
|
38
|
+
// native edit2 by default (B3). Same-size edits keep the JS in-place partial
|
|
39
|
+
// write, which rewrites bytes in place instead of the whole file.
|
|
40
|
+
const oldBytes = Buffer.byteLength(oldStr, 'utf-8');
|
|
41
|
+
const newBytes = Buffer.byteLength(newStr, 'utf-8');
|
|
42
|
+
if (oldBytes === newBytes) return false;
|
|
43
|
+
return true;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export async function runNativeExactEdit({ fullPath, oldStr, newStr, replaceAll, signal = null }) {
|
|
47
|
+
if (signal?.aborted) {
|
|
48
|
+
return { ok: false, fallback: false, error: signal.reason?.message || signal.reason || 'native edit aborted' };
|
|
49
|
+
}
|
|
50
|
+
const oldBuf = Buffer.from(oldStr, 'utf-8');
|
|
51
|
+
const newBuf = Buffer.from(newStr, 'utf-8');
|
|
52
|
+
const started = performance.now();
|
|
53
|
+
try {
|
|
54
|
+
// PARITY GUARD: the native engine MATCHES via the curly-quote fold
|
|
55
|
+
// tier but applies new_string verbatim, silently downgrading the
|
|
56
|
+
// file's typographic quotes (JS slow path preserves them via
|
|
57
|
+
// preserveQuoteTypography). When old_string carries quote-family
|
|
58
|
+
// chars — the only inputs that can land on the curly tier — probe
|
|
59
|
+
// with a dry run (persistent server, ~ms) and defer curly-tier
|
|
60
|
+
// matches to the JS editor.
|
|
61
|
+
if (/["'‘’“”]/.test(oldStr)) {
|
|
62
|
+
const probe = await runServerEdit({ fullPath, oldBuf, newBuf, replaceAll, dryRun: true, signal });
|
|
63
|
+
if (probe?.tier === 'curly') {
|
|
64
|
+
return { ok: false, fallback: true, error: 'curly-quote fold match — deferred to JS editor for typography preservation' };
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
const res = await runServerEdit({ fullPath, oldBuf, newBuf, replaceAll, signal });
|
|
68
|
+
return {
|
|
69
|
+
ok: true,
|
|
70
|
+
replacements: res.replacements,
|
|
71
|
+
readMs: res.readMs,
|
|
72
|
+
applyMs: res.applyMs,
|
|
73
|
+
writeMs: res.writeMs,
|
|
74
|
+
totalMs: res.totalMs,
|
|
75
|
+
roundtripMs: res.roundtripMs ?? (performance.now() - started),
|
|
76
|
+
stage: res.tier,
|
|
77
|
+
contentHash: res.contentHash,
|
|
78
|
+
};
|
|
79
|
+
} catch (err) {
|
|
80
|
+
if (err?.name === 'AbortError') {
|
|
81
|
+
return { ok: false, fallback: false, error: err.message };
|
|
82
|
+
}
|
|
83
|
+
const msg = String(err?.message || err);
|
|
84
|
+
// Tier misses and not-found map to a JS fallback; transport/spawn errors
|
|
85
|
+
// also fall back so a server hiccup never blocks an edit.
|
|
86
|
+
const fallback = /old_string (?:not found|found \d+ times)|not valid UTF-8|no exact match|not found|server/i.test(msg);
|
|
87
|
+
return { ok: false, fallback, error: msg };
|
|
88
|
+
}
|
|
89
|
+
}
|