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,98 @@
|
|
|
1
|
+
import { hashText } from './hash-utils.mjs';
|
|
2
|
+
import {
|
|
3
|
+
normaliseRangeHashEntry,
|
|
4
|
+
statMatchesSnapshot,
|
|
5
|
+
} from './snapshot-helpers.mjs';
|
|
6
|
+
|
|
7
|
+
function snapshotRangeHashRows(snapshot) {
|
|
8
|
+
return Array.isArray(snapshot?.rangeHashes)
|
|
9
|
+
? snapshot.rangeHashes
|
|
10
|
+
: (snapshot?.rangeHash && Array.isArray(snapshot.ranges) && snapshot.ranges.length > 0
|
|
11
|
+
? [{ ...snapshot.ranges[0], hash: snapshot.rangeHash }]
|
|
12
|
+
: []);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function isSnapshotStale(stat, snapshot, { fullPath = '', readCache = null, readTextForSnapshotCheck } = {}) {
|
|
16
|
+
// Unified structure (mirrors edit's validatePreparedEditBase):
|
|
17
|
+
// stat-match FIRST → fail-closed-on-no-material → hash-verify.
|
|
18
|
+
// Fast-path: a full stat match (size + mtime±1 + ctime±1) ⇒ untouched
|
|
19
|
+
// → not stale, accept without reading. statMatchesSnapshot returns
|
|
20
|
+
// false on a missing/incomplete snapshot, so those fall through (no
|
|
21
|
+
// fail-open). Tradeoff (accepted policy): a size-preserving write that
|
|
22
|
+
// ALSO restores mtime AND ctime is not caught — vanishingly rare,
|
|
23
|
+
// ctime is not userland-settable on the usual platforms.
|
|
24
|
+
if (statMatchesSnapshot(stat, snapshot)) return false;
|
|
25
|
+
// Stat drifted (or incomplete snapshot) → content hash is the gate.
|
|
26
|
+
const rangeHashRows = snapshotRangeHashRows(snapshot);
|
|
27
|
+
const hasHashMaterial = !!snapshot.contentHash || rangeHashRows.length > 0;
|
|
28
|
+
// Fail-closed: no integrity material AND stat already drifted ⇒ cannot
|
|
29
|
+
// verify against current bytes, treat as stale (force re-read).
|
|
30
|
+
if (!hasHashMaterial) return true;
|
|
31
|
+
const canReadContent = fullPath && typeof readTextForSnapshotCheck === 'function';
|
|
32
|
+
if (!canReadContent) return true;
|
|
33
|
+
// Refresh the snapshot's stat fields to the live values once the hash
|
|
34
|
+
// confirms bytes are identical — silences future false positives for
|
|
35
|
+
// the same mtime-only churn.
|
|
36
|
+
const refreshSnapshotStat = () => {
|
|
37
|
+
if (Number.isFinite(stat.mtimeMs)) snapshot.mtimeMs = stat.mtimeMs;
|
|
38
|
+
if (typeof stat.size === 'number') snapshot.size = stat.size;
|
|
39
|
+
if (Number.isFinite(stat.ctimeMs)) snapshot.ctimeMs = stat.ctimeMs;
|
|
40
|
+
};
|
|
41
|
+
if (snapshot.contentHash) {
|
|
42
|
+
try {
|
|
43
|
+
const cur = readTextForSnapshotCheck(fullPath, readCache, stat);
|
|
44
|
+
if (hashText(cur) !== snapshot.contentHash) return true;
|
|
45
|
+
refreshSnapshotStat();
|
|
46
|
+
return false;
|
|
47
|
+
} catch {
|
|
48
|
+
// Unreadable / stat race — cannot verify, treat as stale.
|
|
49
|
+
return true;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
// rangeHashes-only path (paged reads).
|
|
53
|
+
try {
|
|
54
|
+
const raw = readTextForSnapshotCheck(fullPath, readCache, stat);
|
|
55
|
+
const lines = raw.split(/\r?\n/);
|
|
56
|
+
let verified = 0;
|
|
57
|
+
for (const row of rangeHashRows) {
|
|
58
|
+
const r = normaliseRangeHashEntry(row);
|
|
59
|
+
if (!r) continue;
|
|
60
|
+
const startIdx = Math.max(0, (r.startLine || 1) - 1);
|
|
61
|
+
const endIdx = r.endLine === Infinity ? lines.length : Math.min(lines.length, r.endLine);
|
|
62
|
+
const rangeText = lines.slice(startIdx, endIdx).join('\n');
|
|
63
|
+
if (hashText(rangeText) !== r.hash) return true;
|
|
64
|
+
verified++;
|
|
65
|
+
}
|
|
66
|
+
// Fail-closed: rangeHashes present but no row could be verified
|
|
67
|
+
// (all malformed) ⇒ cannot prove identity, treat as stale.
|
|
68
|
+
if (verified === 0) return true;
|
|
69
|
+
refreshSnapshotStat();
|
|
70
|
+
return false;
|
|
71
|
+
} catch {
|
|
72
|
+
return true;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export function readContentIfSnapshotHashMatches(fullPath, snapshot, { readCache = null, st = null, readTextForSnapshotCheck } = {}) {
|
|
77
|
+
if (!snapshot || typeof readTextForSnapshotCheck !== 'function') return null;
|
|
78
|
+
try {
|
|
79
|
+
const content = readTextForSnapshotCheck(fullPath, readCache, st);
|
|
80
|
+
if (snapshot.contentHash) {
|
|
81
|
+
return hashText(content) === snapshot.contentHash ? content : null;
|
|
82
|
+
}
|
|
83
|
+
const rangeHashRows = snapshotRangeHashRows(snapshot);
|
|
84
|
+
if (rangeHashRows.length === 0) return null;
|
|
85
|
+
const lines = content.split(/\r?\n/);
|
|
86
|
+
for (const row of rangeHashRows) {
|
|
87
|
+
const r = normaliseRangeHashEntry(row);
|
|
88
|
+
if (!r) return null;
|
|
89
|
+
const startIdx = Math.max(0, (r.startLine || 1) - 1);
|
|
90
|
+
const endIdx = r.endLine === Infinity ? lines.length : Math.min(lines.length, r.endLine);
|
|
91
|
+
const rangeText = lines.slice(startIdx, endIdx).join('\n');
|
|
92
|
+
if (hashText(rangeText) !== r.hash) return null;
|
|
93
|
+
}
|
|
94
|
+
return content;
|
|
95
|
+
} catch {
|
|
96
|
+
return null;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { createReadStream } from 'fs';
|
|
2
|
+
|
|
3
|
+
function isJsWhitespaceCodeUnit(code) {
|
|
4
|
+
return code === 32
|
|
5
|
+
|| (code >= 9 && code <= 13)
|
|
6
|
+
|| code === 0x00A0
|
|
7
|
+
|| code === 0x1680
|
|
8
|
+
|| (code >= 0x2000 && code <= 0x200A)
|
|
9
|
+
|| code === 0x2028
|
|
10
|
+
|| code === 0x2029
|
|
11
|
+
|| code === 0x202F
|
|
12
|
+
|| code === 0x205F
|
|
13
|
+
|| code === 0x3000
|
|
14
|
+
|| code === 0xFEFF;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
async function countTextStatsStreamingUtf8(fullPath, size) {
|
|
18
|
+
if (!size) return { lines: 0, words: 0, bytes: 0 };
|
|
19
|
+
const stream = createReadStream(fullPath, { encoding: 'utf-8', highWaterMark: 1024 * 1024 });
|
|
20
|
+
let lines = 0;
|
|
21
|
+
let words = 0;
|
|
22
|
+
let inWord = false;
|
|
23
|
+
let lastChar = '';
|
|
24
|
+
for await (const chunk of stream) {
|
|
25
|
+
if (!chunk) continue;
|
|
26
|
+
for (let i = 0; i < chunk.length; i++) {
|
|
27
|
+
const code = chunk.charCodeAt(i);
|
|
28
|
+
if (code === 10) lines++;
|
|
29
|
+
if (isJsWhitespaceCodeUnit(code)) {
|
|
30
|
+
inWord = false;
|
|
31
|
+
} else if (!inWord) {
|
|
32
|
+
words++;
|
|
33
|
+
inWord = true;
|
|
34
|
+
}
|
|
35
|
+
lastChar = code;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
if (lastChar && lastChar !== 10) lines++;
|
|
39
|
+
return { lines, words, bytes: size };
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export async function countTextStatsStreaming(fullPath, size) {
|
|
43
|
+
if (!size) return { lines: 0, words: 0, bytes: 0 };
|
|
44
|
+
const stream = createReadStream(fullPath, { highWaterMark: 1024 * 1024 });
|
|
45
|
+
let lines = 0;
|
|
46
|
+
let words = 0;
|
|
47
|
+
let inWord = false;
|
|
48
|
+
let lastByte = -1;
|
|
49
|
+
for await (const chunk of stream) {
|
|
50
|
+
if (!chunk) continue;
|
|
51
|
+
for (let i = 0; i < chunk.length; i++) {
|
|
52
|
+
const b = chunk[i];
|
|
53
|
+
if (b >= 0x80) {
|
|
54
|
+
stream.destroy();
|
|
55
|
+
return countTextStatsStreamingUtf8(fullPath, size);
|
|
56
|
+
}
|
|
57
|
+
if (b === 10) lines++;
|
|
58
|
+
if (b === 32 || (b >= 9 && b <= 13)) {
|
|
59
|
+
inWord = false;
|
|
60
|
+
} else if (!inWord) {
|
|
61
|
+
words++;
|
|
62
|
+
inWord = true;
|
|
63
|
+
}
|
|
64
|
+
lastByte = b;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
if (lastByte !== -1 && lastByte !== 10) lines++;
|
|
68
|
+
return { lines, words, bytes: size };
|
|
69
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
// Windows program-root derivation from present environment only. No guessed
|
|
2
|
+
// defaults: every candidate path must originate from a DEFINED env root, so a
|
|
3
|
+
// machine that lacks a given root simply contributes no candidates rather than
|
|
4
|
+
// inviting a hardcoded 'C:\\...' fallback.
|
|
5
|
+
|
|
6
|
+
// De-duped list of defined program-root dirs, in priority order. Undefined or
|
|
7
|
+
// empty env vars are dropped — never substituted with a literal default.
|
|
8
|
+
export function windowsProgramRoots() {
|
|
9
|
+
const roots = [
|
|
10
|
+
process.env.ProgramW6432,
|
|
11
|
+
process.env.ProgramFiles,
|
|
12
|
+
process.env['ProgramFiles(x86)'],
|
|
13
|
+
process.env.LOCALAPPDATA,
|
|
14
|
+
].filter(Boolean)
|
|
15
|
+
return [...new Set(roots)]
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
// Windows system root (e.g. the dir holding System32). Returns undefined when
|
|
19
|
+
// SystemRoot is absent — callers must treat that as "not detected", never as
|
|
20
|
+
// 'C:\\Windows'.
|
|
21
|
+
export function windowsSystemRoot() {
|
|
22
|
+
return process.env.SystemRoot
|
|
23
|
+
}
|
|
@@ -0,0 +1,401 @@
|
|
|
1
|
+
import { mkdirSync, statSync, lstatSync, openSync, readSync, closeSync } from 'fs';
|
|
2
|
+
import { dirname } from 'path';
|
|
3
|
+
import { markCodeGraphDirtyPaths } from '../code-graph.mjs';
|
|
4
|
+
import {
|
|
5
|
+
normalizeInputPath,
|
|
6
|
+
normalizeOutputPath,
|
|
7
|
+
resolveAgainstCwd,
|
|
8
|
+
} from './path-utils.mjs';
|
|
9
|
+
import { normalizeErrorMessage } from './path-diagnostics.mjs';
|
|
10
|
+
import { withPathLock, withBuiltinPathLocks, pathLockKey } from './path-locks.mjs';
|
|
11
|
+
import { withAdvisoryLocks } from './advisory-lock.mjs';
|
|
12
|
+
import { hashText } from './hash-utils.mjs';
|
|
13
|
+
import { snapshotCoversFullFile } from './snapshot-helpers.mjs';
|
|
14
|
+
import {
|
|
15
|
+
getReadSnapshot,
|
|
16
|
+
isSnapshotStale,
|
|
17
|
+
readContentIfSnapshotHashMatches,
|
|
18
|
+
recordReadSnapshot,
|
|
19
|
+
} from './read-snapshot-runtime.mjs';
|
|
20
|
+
import {
|
|
21
|
+
invalidateBuiltinResultCache,
|
|
22
|
+
seedRawContentCacheAfterWrite,
|
|
23
|
+
} from './cache-layers.mjs';
|
|
24
|
+
import { atomicWrite } from './atomic-write.mjs';
|
|
25
|
+
import {
|
|
26
|
+
hasUnsafeWin32Component,
|
|
27
|
+
isWindowsDevicePath,
|
|
28
|
+
isUncPath,
|
|
29
|
+
isSpecialFileStat,
|
|
30
|
+
} from './device-paths.mjs';
|
|
31
|
+
import { assertPathReachable, assertPathsReachable } from './fs-reachability.mjs';
|
|
32
|
+
|
|
33
|
+
const STREAMING_THRESHOLD_BYTES = 1024 * 1024;
|
|
34
|
+
|
|
35
|
+
// BOM invariant: an existing file's encoding is determined STRICTLY by its
|
|
36
|
+
// leading BOM bytes — never by content sniffing. Mirrors Claude Code
|
|
37
|
+
// detectFileEncoding (src/utils/file.ts:84-118) and the FF FE -> utf16le
|
|
38
|
+
// check in src/utils/fileRead.ts:34. Returns a tag describing how to
|
|
39
|
+
// re-emit content so an overwrite preserves the on-disk encoding:
|
|
40
|
+
// { encoding: 'utf16le', bom: <Buffer FF FE> } -> UTF-16LE w/ BOM
|
|
41
|
+
// { encoding: 'utf8', bom: <Buffer EF BB BF> } -> UTF-8 w/ BOM
|
|
42
|
+
// { encoding: 'utf8', bom: null } -> UTF-8 (new/absent/no BOM)
|
|
43
|
+
export function detectExistingEncoding(fullPath) {
|
|
44
|
+
let fd;
|
|
45
|
+
try {
|
|
46
|
+
fd = openSync(fullPath, 'r');
|
|
47
|
+
const head = Buffer.alloc(3);
|
|
48
|
+
const bytesRead = readSync(fd, head, 0, 3, 0);
|
|
49
|
+
if (bytesRead >= 2 && head[0] === 0xff && head[1] === 0xfe) {
|
|
50
|
+
return { encoding: 'utf16le', bom: Buffer.from([0xff, 0xfe]) };
|
|
51
|
+
}
|
|
52
|
+
if (bytesRead >= 3 && head[0] === 0xef && head[1] === 0xbb && head[2] === 0xbf) {
|
|
53
|
+
return { encoding: 'utf8', bom: Buffer.from([0xef, 0xbb, 0xbf]) };
|
|
54
|
+
}
|
|
55
|
+
return { encoding: 'utf8', bom: null };
|
|
56
|
+
} catch {
|
|
57
|
+
// File absent or unreadable -> new file: utf-8, no BOM.
|
|
58
|
+
return { encoding: 'utf8', bom: null };
|
|
59
|
+
} finally {
|
|
60
|
+
if (fd !== undefined) { try { closeSync(fd); } catch {} }
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export function toWriteBuffer(content, encoding = { encoding: 'utf8', bom: null }) {
|
|
65
|
+
if (typeof content !== 'string') return content;
|
|
66
|
+
const body = Buffer.from(content, encoding.encoding === 'utf16le' ? 'utf16le' : 'utf-8');
|
|
67
|
+
return encoding.bom ? Buffer.concat([encoding.bom, body]) : body;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function captureTargetSnapshot(fullPath) {
|
|
71
|
+
try {
|
|
72
|
+
const st = statSync(fullPath);
|
|
73
|
+
return {
|
|
74
|
+
exists: true,
|
|
75
|
+
size: st.size,
|
|
76
|
+
mtimeMs: Number(st.mtimeMs),
|
|
77
|
+
ctimeMs: Number(st.ctimeMs),
|
|
78
|
+
ino: Number(st.ino),
|
|
79
|
+
};
|
|
80
|
+
} catch (err) {
|
|
81
|
+
if (err && err.code === 'ENOENT') return { exists: false };
|
|
82
|
+
throw err;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/** Whole-file overwrite proof: full coverage sentinel + on-disk contentHash. */
|
|
87
|
+
function snapshotAllowsWholeFileOverwrite(snapshot) {
|
|
88
|
+
if (!snapshot) return false;
|
|
89
|
+
if (typeof snapshot.contentHash !== 'string' || !snapshot.contentHash) return false;
|
|
90
|
+
return snapshotCoversFullFile(snapshot);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function writeUncRejectMessage(displayPath, paths, { batch = false } = {}) {
|
|
94
|
+
for (const p of paths) {
|
|
95
|
+
if (typeof p !== 'string') continue;
|
|
96
|
+
if (typeof isUncPath === 'function' ? isUncPath(p) : (p.startsWith('\\\\') || p.startsWith('//'))) {
|
|
97
|
+
return batch
|
|
98
|
+
? `FAIL ${normalizeOutputPath(displayPath)}: UNC/SMB paths are not supported (R1: NTLM-leak prevention)`
|
|
99
|
+
: `Error: UNC/SMB paths are not supported (R1: NTLM-leak prevention): ${displayPath}`;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
return null;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function validateOverwriteAllowed(fullPath, displayPath, readStateScope, { batch = false, allowUnreadOverwrite = false } = {}) {
|
|
106
|
+
const uncErr = writeUncRejectMessage(displayPath, [fullPath], { batch });
|
|
107
|
+
if (uncErr) return uncErr;
|
|
108
|
+
try {
|
|
109
|
+
const existing = lstatSync(fullPath);
|
|
110
|
+
if (existing.isSymbolicLink && existing.isSymbolicLink()) {
|
|
111
|
+
return batch
|
|
112
|
+
? `FAIL ${normalizeOutputPath(displayPath)}: symlink targets are not supported for write (would replace the link)`
|
|
113
|
+
: `Error: symlink targets are not supported for write (would replace the link): ${displayPath}`;
|
|
114
|
+
}
|
|
115
|
+
if (typeof isSpecialFileStat === 'function' && isSpecialFileStat(existing)) {
|
|
116
|
+
return batch
|
|
117
|
+
? `FAIL ${normalizeOutputPath(displayPath)}: non-regular file (FIFO / device / socket) cannot be overwritten`
|
|
118
|
+
: `Error: non-regular file (FIFO / device / socket) cannot be overwritten: ${displayPath}`;
|
|
119
|
+
}
|
|
120
|
+
// Opt-in fast path: skip ONLY the read-before-write / stale-snapshot
|
|
121
|
+
// gate (codes 6/10/7). All other checks (symlink, special-file, UNC,
|
|
122
|
+
// stat-error surfacing above/below) still apply.
|
|
123
|
+
if (!allowUnreadOverwrite && existing.isFile() && !getReadSnapshot(fullPath, readStateScope)) {
|
|
124
|
+
return batch
|
|
125
|
+
? `FAIL ${normalizeOutputPath(displayPath)}: file exists but has not been read yet — read before overwriting`
|
|
126
|
+
: `Error [code 6]: file exists but has not been read yet — read before overwriting: ${displayPath}`;
|
|
127
|
+
}
|
|
128
|
+
if (!allowUnreadOverwrite && existing.isFile()) {
|
|
129
|
+
const snapshot = getReadSnapshot(fullPath, readStateScope);
|
|
130
|
+
if (snapshot && !snapshotAllowsWholeFileOverwrite(snapshot)) {
|
|
131
|
+
const detail = snapshot.grepOnly === true
|
|
132
|
+
? 'grep snapshot lacks full-file proof — read it in full before overwriting'
|
|
133
|
+
: 'partial-read snapshot — read it in full before overwriting';
|
|
134
|
+
return batch
|
|
135
|
+
? `FAIL ${normalizeOutputPath(displayPath)}: ${detail}`
|
|
136
|
+
: `Error [code 10]: ${detail}: ${displayPath}`;
|
|
137
|
+
}
|
|
138
|
+
if (snapshot && isSnapshotStale(existing, snapshot, fullPath)) {
|
|
139
|
+
const hashOk = readContentIfSnapshotHashMatches(fullPath, snapshot, null, existing);
|
|
140
|
+
if (hashOk === null) {
|
|
141
|
+
return batch
|
|
142
|
+
? `FAIL ${normalizeOutputPath(displayPath)}: file modified since read — read it again before overwriting`
|
|
143
|
+
: `Error [code 7]: file modified since read — read it again before overwriting: ${displayPath}`;
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
} catch (err) {
|
|
148
|
+
// Only ENOENT is "safe to create"; any other stat error (EACCES,
|
|
149
|
+
// EPERM, ELOOP, ENOTDIR, …) must surface so we don't silently
|
|
150
|
+
// overwrite or pretend the file is absent.
|
|
151
|
+
if (err && err.code === 'ENOENT') return null;
|
|
152
|
+
const reason = normalizeErrorMessage(err instanceof Error ? err.message : String(err));
|
|
153
|
+
return batch
|
|
154
|
+
? `FAIL ${normalizeOutputPath(displayPath)}: stat failed before overwrite: ${reason}`
|
|
155
|
+
: `Error: stat failed before overwrite: ${reason}: ${displayPath}`;
|
|
156
|
+
}
|
|
157
|
+
return null;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
async function writeOneUnlocked({ filePath, content, fullPath, readStateScope, sessionId, targetSnapshot }) {
|
|
161
|
+
mkdirSync(dirname(fullPath), { recursive: true });
|
|
162
|
+
// Preserve the existing file's encoding (BOM invariant); new files -> utf-8.
|
|
163
|
+
const writeContent = toWriteBuffer(content, detectExistingEncoding(fullPath));
|
|
164
|
+
const byteLength = Buffer.isBuffer(writeContent)
|
|
165
|
+
? writeContent.length
|
|
166
|
+
: Buffer.byteLength(String(writeContent ?? ''), 'utf-8');
|
|
167
|
+
const oversized = byteLength > STREAMING_THRESHOLD_BYTES;
|
|
168
|
+
await atomicWrite(fullPath, writeContent, {
|
|
169
|
+
sessionId,
|
|
170
|
+
expectedTargetSnapshot: targetSnapshot,
|
|
171
|
+
});
|
|
172
|
+
let writtenStat = null;
|
|
173
|
+
try { writtenStat = statSync(fullPath); } catch {}
|
|
174
|
+
recordReadSnapshot(fullPath, writtenStat || undefined, readStateScope, {
|
|
175
|
+
source: 'write',
|
|
176
|
+
contentHash: hashText(writeContent),
|
|
177
|
+
});
|
|
178
|
+
return {
|
|
179
|
+
filePath,
|
|
180
|
+
fullPath,
|
|
181
|
+
content: writeContent,
|
|
182
|
+
stat: writtenStat,
|
|
183
|
+
oversized,
|
|
184
|
+
};
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
export async function executeWriteTool(args, workDir, readStateScope, options = {}) {
|
|
188
|
+
if (typeof args.file_path === 'string' && !args.path) args.path = args.file_path;
|
|
189
|
+
const allowUnreadOverwrite = args.allow_unread_overwrite === true;
|
|
190
|
+
if (Array.isArray(args.writes) && args.writes.length > 0) {
|
|
191
|
+
const items = args.writes.map((entry) => ({
|
|
192
|
+
path: normalizeInputPath(entry?.path ?? entry?.file_path),
|
|
193
|
+
content: entry?.content,
|
|
194
|
+
}));
|
|
195
|
+
const missing = items.filter((entry) => !entry.path || entry.content === undefined);
|
|
196
|
+
if (missing.length > 0) {
|
|
197
|
+
return 'Error: each write entry requires path and content';
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// Reject duplicate targets (same lock key) and parent/child
|
|
201
|
+
// conflicts (one entry would write a file at a path another entry
|
|
202
|
+
// needs as a directory ancestor, or vice versa).
|
|
203
|
+
for (const entry of items) {
|
|
204
|
+
if (typeof isWindowsDevicePath === 'function' && isWindowsDevicePath(entry.path)) {
|
|
205
|
+
return `Error: cannot write Windows device path (reserved name or raw-device namespace): ${normalizeOutputPath(entry.path)}`;
|
|
206
|
+
}
|
|
207
|
+
if (typeof hasUnsafeWin32Component === 'function' && hasUnsafeWin32Component(entry.path)) {
|
|
208
|
+
return `Error: cannot write Windows path with trailing dot/space or NTFS ADS suffix (bypasses device guard): ${normalizeOutputPath(entry.path)}`;
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
const resolvedItems = items.map((entry) => {
|
|
212
|
+
const fp = resolveAgainstCwd(entry.path, workDir);
|
|
213
|
+
return { entry, fullPath: fp, lockKey: pathLockKey(fp) };
|
|
214
|
+
});
|
|
215
|
+
for (const r of resolvedItems) {
|
|
216
|
+
if (typeof isWindowsDevicePath === 'function' && isWindowsDevicePath(r.fullPath)) {
|
|
217
|
+
return `Error: cannot write Windows device path (reserved name or raw-device namespace): ${normalizeOutputPath(r.entry.path)}`;
|
|
218
|
+
}
|
|
219
|
+
if (typeof hasUnsafeWin32Component === 'function' && hasUnsafeWin32Component(r.fullPath)) {
|
|
220
|
+
return `Error: cannot write Windows path with trailing dot/space or NTFS ADS suffix (bypasses device guard): ${normalizeOutputPath(r.entry.path)}`;
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
const seenKeys = new Map();
|
|
224
|
+
for (const r of resolvedItems) {
|
|
225
|
+
if (seenKeys.has(r.lockKey)) {
|
|
226
|
+
return `Error: duplicate target in batch: ${normalizeOutputPath(r.entry.path)} collides with ${normalizeOutputPath(seenKeys.get(r.lockKey))}`;
|
|
227
|
+
}
|
|
228
|
+
seenKeys.set(r.lockKey, r.entry.path);
|
|
229
|
+
}
|
|
230
|
+
const sortedKeys = [...seenKeys.keys()].sort();
|
|
231
|
+
for (let i = 0; i + 1 < sortedKeys.length; i += 1) {
|
|
232
|
+
const a = sortedKeys[i];
|
|
233
|
+
const b = sortedKeys[i + 1];
|
|
234
|
+
if (b.length > a.length && b.startsWith(a) && (b[a.length] === '/' || b[a.length] === '\\')) {
|
|
235
|
+
return `Error: parent/child path conflict in batch: ${normalizeOutputPath(seenKeys.get(a))} would be a file but ${normalizeOutputPath(seenKeys.get(b))} requires it as a directory`;
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
// ATOMIC BATCH CONTRACT: UNC + preflight before any lock acquire
|
|
240
|
+
// (advisory lock mkdir must not run for rejected batches), then
|
|
241
|
+
// acquire all locks in sorted order, commit with per-file snapshots.
|
|
242
|
+
const allFullPaths = resolvedItems.map((r) => r.fullPath);
|
|
243
|
+
for (const { entry, fullPath } of resolvedItems) {
|
|
244
|
+
const uncErr = writeUncRejectMessage(entry.path, [entry.path, fullPath], { batch: true });
|
|
245
|
+
if (uncErr) {
|
|
246
|
+
return `Error: batch write rejected — UNC path blocked before lock; no files written\n${uncErr}`;
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
try {
|
|
250
|
+
await assertPathsReachable(allFullPaths);
|
|
251
|
+
} catch (err) {
|
|
252
|
+
const reason = normalizeErrorMessage(err instanceof Error ? err.message : String(err));
|
|
253
|
+
return `Error: batch write rejected — path reachability preflight failed; no files written\n${reason}`;
|
|
254
|
+
}
|
|
255
|
+
const preflightLines = [];
|
|
256
|
+
let preflightFailed = false;
|
|
257
|
+
for (const { entry, fullPath } of resolvedItems) {
|
|
258
|
+
const filePath = entry.path;
|
|
259
|
+
if (typeof entry.content === 'string') {
|
|
260
|
+
const _nulIdx = entry.content.indexOf('\u0000');
|
|
261
|
+
if (_nulIdx !== -1) {
|
|
262
|
+
preflightLines.push(`FAIL ${normalizeOutputPath(filePath)}: Error [code 11]: content contains NUL byte (U+0000) at offset ${_nulIdx} — source text must not contain NUL`);
|
|
263
|
+
preflightFailed = true;
|
|
264
|
+
continue;
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
const preflightError = validateOverwriteAllowed(fullPath, filePath, readStateScope, { batch: true, allowUnreadOverwrite });
|
|
268
|
+
if (preflightError) {
|
|
269
|
+
preflightLines.push(preflightError);
|
|
270
|
+
preflightFailed = true;
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
if (preflightFailed) {
|
|
274
|
+
return `Error: batch write rejected — preflight failed (${preflightLines.length} of ${resolvedItems.length}); no files written\n${preflightLines.join('\n')}`;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
return withBuiltinPathLocks(allFullPaths, () =>
|
|
278
|
+
withAdvisoryLocks(allFullPaths, async () => {
|
|
279
|
+
// Commit phase: preflight already passed. Per-file failure
|
|
280
|
+
// here is still possible (mid-flight disk error, TOCTOU
|
|
281
|
+
// detection), but is uncommon and reported per entry.
|
|
282
|
+
const results = [];
|
|
283
|
+
const dirtyPaths = [];
|
|
284
|
+
const successfulWrites = [];
|
|
285
|
+
let commitFailed = 0;
|
|
286
|
+
for (const { entry, fullPath } of resolvedItems) {
|
|
287
|
+
const filePath = entry.path;
|
|
288
|
+
const content = entry.content;
|
|
289
|
+
try {
|
|
290
|
+
const targetSnapshot = captureTargetSnapshot(fullPath);
|
|
291
|
+
const written = await writeOneUnlocked({
|
|
292
|
+
filePath,
|
|
293
|
+
content,
|
|
294
|
+
fullPath,
|
|
295
|
+
readStateScope,
|
|
296
|
+
sessionId: options?.sessionId,
|
|
297
|
+
targetSnapshot,
|
|
298
|
+
});
|
|
299
|
+
successfulWrites.push(written);
|
|
300
|
+
dirtyPaths.push(fullPath);
|
|
301
|
+
results.push(`OK ${normalizeOutputPath(filePath)}`);
|
|
302
|
+
} catch (err) {
|
|
303
|
+
commitFailed += 1;
|
|
304
|
+
results.push(`FAIL ${normalizeOutputPath(filePath)}: ${normalizeErrorMessage(err instanceof Error ? err.message : String(err))}`);
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
if (dirtyPaths.length > 0) {
|
|
309
|
+
invalidateBuiltinResultCache(dirtyPaths);
|
|
310
|
+
for (const seed of successfulWrites) {
|
|
311
|
+
// Skip raw-content cache seeding for oversized
|
|
312
|
+
// writes — keeping a multi-megabyte payload in
|
|
313
|
+
// memory after rename defeats the streaming win.
|
|
314
|
+
if (seed.oversized) continue;
|
|
315
|
+
seedRawContentCacheAfterWrite(seed.fullPath, seed.content, seed.stat);
|
|
316
|
+
}
|
|
317
|
+
markCodeGraphDirtyPaths(dirtyPaths);
|
|
318
|
+
}
|
|
319
|
+
if (commitFailed > 0) {
|
|
320
|
+
return `Error: batch write failed during commit (${commitFailed} of ${resolvedItems.length}); preflight passed but some renames failed\n${results.join('\n')}`;
|
|
321
|
+
}
|
|
322
|
+
// ② completion progress (claude "Found N" parity). Best-effort,
|
|
323
|
+
// no-op when onProgress is absent (no progressToken).
|
|
324
|
+
if (typeof options?.onProgress === 'function') {
|
|
325
|
+
try { options.onProgress(`wrote ${successfulWrites.length} files`); } catch { /* best-effort */ }
|
|
326
|
+
}
|
|
327
|
+
return results.join('\n');
|
|
328
|
+
})
|
|
329
|
+
);
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
args.path = normalizeInputPath(args.path);
|
|
333
|
+
const filePath = args.path;
|
|
334
|
+
const content = args.content;
|
|
335
|
+
if (!filePath) return 'Error: path is required';
|
|
336
|
+
if (content === undefined) return 'Error: content is required';
|
|
337
|
+
if (typeof content === 'string') {
|
|
338
|
+
const _nulIdx = content.indexOf('\u0000');
|
|
339
|
+
if (_nulIdx !== -1) return `Error [code 11]: content contains NUL byte (U+0000) at offset ${_nulIdx} — source text must not contain NUL: ${filePath}`;
|
|
340
|
+
}
|
|
341
|
+
// R12: Win32 component guard — reject trailing dot/space or NTFS ADS
|
|
342
|
+
// suffix (foo.txt:ads) and reserved device names (NUL, CON, …) before
|
|
343
|
+
// resolve so a relative path can't be coerced into a device alias.
|
|
344
|
+
if (typeof isWindowsDevicePath === 'function' && isWindowsDevicePath(filePath)) {
|
|
345
|
+
return `Error: cannot write Windows device path (reserved name or raw-device namespace): ${normalizeOutputPath(filePath)}`;
|
|
346
|
+
}
|
|
347
|
+
if (typeof hasUnsafeWin32Component === 'function' && hasUnsafeWin32Component(filePath)) {
|
|
348
|
+
return `Error: cannot write Windows path with trailing dot/space or NTFS ADS suffix (bypasses device guard): ${normalizeOutputPath(filePath)}`;
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
const fullPath = resolveAgainstCwd(filePath, workDir);
|
|
352
|
+
const uncErr = writeUncRejectMessage(filePath, [filePath, fullPath], { batch: false });
|
|
353
|
+
if (uncErr) return uncErr;
|
|
354
|
+
if (typeof isWindowsDevicePath === 'function' && isWindowsDevicePath(fullPath)) {
|
|
355
|
+
return `Error: cannot write Windows device path (reserved name or raw-device namespace): ${normalizeOutputPath(filePath)}`;
|
|
356
|
+
}
|
|
357
|
+
if (typeof hasUnsafeWin32Component === 'function' && hasUnsafeWin32Component(fullPath)) {
|
|
358
|
+
return `Error: cannot write Windows path with trailing dot/space or NTFS ADS suffix (bypasses device guard): ${normalizeOutputPath(filePath)}`;
|
|
359
|
+
}
|
|
360
|
+
try {
|
|
361
|
+
await assertPathReachable(fullPath);
|
|
362
|
+
} catch (err) {
|
|
363
|
+
return `Error: ${normalizeErrorMessage(err instanceof Error ? err.message : String(err))}`;
|
|
364
|
+
}
|
|
365
|
+
const preflightError = validateOverwriteAllowed(fullPath, filePath, readStateScope, { allowUnreadOverwrite });
|
|
366
|
+
if (preflightError) return preflightError;
|
|
367
|
+
return withPathLock(fullPath, () =>
|
|
368
|
+
withAdvisoryLocks([fullPath], async () => {
|
|
369
|
+
let targetSnapshot;
|
|
370
|
+
try {
|
|
371
|
+
targetSnapshot = captureTargetSnapshot(fullPath);
|
|
372
|
+
} catch (err) {
|
|
373
|
+
return `Error: snapshot capture failed: ${normalizeErrorMessage(err instanceof Error ? err.message : String(err))}: ${filePath}`;
|
|
374
|
+
}
|
|
375
|
+
try {
|
|
376
|
+
const written = await writeOneUnlocked({
|
|
377
|
+
filePath,
|
|
378
|
+
content,
|
|
379
|
+
fullPath,
|
|
380
|
+
readStateScope,
|
|
381
|
+
sessionId: options?.sessionId,
|
|
382
|
+
targetSnapshot,
|
|
383
|
+
});
|
|
384
|
+
invalidateBuiltinResultCache([fullPath]);
|
|
385
|
+
if (!written.oversized) {
|
|
386
|
+
seedRawContentCacheAfterWrite(fullPath, written.content, written.stat);
|
|
387
|
+
}
|
|
388
|
+
markCodeGraphDirtyPaths([fullPath]);
|
|
389
|
+
// ② completion progress (claude "Found N" parity). Best-effort,
|
|
390
|
+
// no-op when onProgress is absent (no progressToken).
|
|
391
|
+
if (typeof options?.onProgress === 'function') {
|
|
392
|
+
try { options.onProgress(`wrote ${normalizeOutputPath(filePath)}`); } catch { /* best-effort */ }
|
|
393
|
+
}
|
|
394
|
+
return `Written: ${normalizeOutputPath(filePath)}`;
|
|
395
|
+
}
|
|
396
|
+
catch (err) {
|
|
397
|
+
return `Error: ${normalizeErrorMessage(err instanceof Error ? err.message : String(err))}`;
|
|
398
|
+
}
|
|
399
|
+
})
|
|
400
|
+
);
|
|
401
|
+
}
|