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,869 @@
|
|
|
1
|
+
import { statSync } from 'fs';
|
|
2
|
+
import { isAbsolute, resolve } from 'path';
|
|
3
|
+
import { trueCasePath } from './path-utils.mjs';
|
|
4
|
+
import {
|
|
5
|
+
canonicalizeGlobSlashes,
|
|
6
|
+
coerceShapeFlex,
|
|
7
|
+
extractGlobBaseDirectory,
|
|
8
|
+
hasGlobMagic,
|
|
9
|
+
normalizeGlobArgs,
|
|
10
|
+
normalizeGrepArgs,
|
|
11
|
+
normalizeInputPath,
|
|
12
|
+
normalizeOutputPath,
|
|
13
|
+
normalizeSearchPattern,
|
|
14
|
+
resolveAgainstCwd,
|
|
15
|
+
} from './path-utils.mjs';
|
|
16
|
+
import {
|
|
17
|
+
buildGlobCacheKey,
|
|
18
|
+
buildGrepCacheKey,
|
|
19
|
+
buildGrepRgArgs,
|
|
20
|
+
DEFAULT_IGNORE_GLOBS,
|
|
21
|
+
} from './search-builders.mjs';
|
|
22
|
+
import { runRg, runRgWindowedLines } from './rg-runner.mjs';
|
|
23
|
+
import { markScopedCacheIncomplete } from '../../session/cache/scoped-cache-outcome.mjs';
|
|
24
|
+
import {
|
|
25
|
+
groupGrepContentByFile,
|
|
26
|
+
normalizeGrepLine,
|
|
27
|
+
splitGrepCountPrefix,
|
|
28
|
+
splitGrepLinePrefix,
|
|
29
|
+
} from './grep-formatting.mjs';
|
|
30
|
+
import {
|
|
31
|
+
cacheGet,
|
|
32
|
+
cacheSet,
|
|
33
|
+
statPathsForMtime,
|
|
34
|
+
} from './cache-layers.mjs';
|
|
35
|
+
import { recordReadSnapshot } from './read-snapshot-runtime.mjs';
|
|
36
|
+
import { applyGrepContextLeadPolicy, GREP_CONTEXT_MAX } from './arg-guard.mjs';
|
|
37
|
+
|
|
38
|
+
// Deterministic ENOENT recovery: when a grep path does not exist, surface
|
|
39
|
+
// indexed files that share the missing path's basename, turning a guessed or
|
|
40
|
+
// misplaced path (e.g. session/result-compression.mjs vs the real
|
|
41
|
+
// tools/result-compression.mjs) into the actual file in one step. Exact
|
|
42
|
+
// basename only — no stem/token fuzzing — so the hint is high-signal and
|
|
43
|
+
// noise-free. Invariant: every ENOENT runs the same basename lookup; there is
|
|
44
|
+
// no "guessed a lot" branch. Returns '' (appends nothing) when no same-named
|
|
45
|
+
// indexed file exists or the glob child is unavailable.
|
|
46
|
+
async function _suggestIndexedPaths(missingPath, executeChildBuiltinTool, workDir) {
|
|
47
|
+
if (typeof executeChildBuiltinTool !== 'function') return '';
|
|
48
|
+
const base = String(missingPath).replace(/\\/g, '/').split('/').pop();
|
|
49
|
+
// Skip when there is no usable basename or it carries glob magic (the
|
|
50
|
+
// pattern, not a literal filename, would not map to a real file).
|
|
51
|
+
if (!base || /[*?[\]{}]/.test(base)) return '';
|
|
52
|
+
try {
|
|
53
|
+
const out = await executeChildBuiltinTool('glob', { pattern: `**/${base}`, head_limit: 6 }, workDir);
|
|
54
|
+
if (typeof out !== 'string') return '';
|
|
55
|
+
// Drop ONLY the exact diagnostic forms glob emits: the empty-result line
|
|
56
|
+
// ("(no files found ...", "(no entries after offset ...") and the
|
|
57
|
+
// suffix/warning lines ("... [N more entries]", "... [warning] ...").
|
|
58
|
+
// A broad startsWith('(' / '...' / 'Error') would wrongly drop real
|
|
59
|
+
// paths like ErrorBoundary.mjs or a "(draft)/x.mjs" leading segment.
|
|
60
|
+
const hits = out.split('\n')
|
|
61
|
+
.filter((s) => s && !/^\(no (?:files found|entries\b)/.test(s) && !/^\.\.\. \[/.test(s))
|
|
62
|
+
.map((s) => s.trim())
|
|
63
|
+
.filter(Boolean)
|
|
64
|
+
.slice(0, 5);
|
|
65
|
+
return hits.length ? `\n[path not found here; same-named indexed file(s): ${hits.join(', ')}]` : '';
|
|
66
|
+
} catch {
|
|
67
|
+
return '';
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function relativePathPrefix(pathPrefix, workDir) {
|
|
72
|
+
if (!workDir) return pathPrefix;
|
|
73
|
+
const cwdFwd = workDir.replace(/\\/g, '/').replace(/\/+$/, '');
|
|
74
|
+
const absFwd = String(pathPrefix || '').replace(/\\/g, '/');
|
|
75
|
+
const haystack = process.platform === 'win32' ? absFwd.toLocaleLowerCase() : absFwd;
|
|
76
|
+
const needle = process.platform === 'win32' ? cwdFwd.toLocaleLowerCase() : cwdFwd;
|
|
77
|
+
if (haystack.startsWith(needle + '/') || haystack === needle) {
|
|
78
|
+
return absFwd.slice(cwdFwd.length + 1) || '.';
|
|
79
|
+
}
|
|
80
|
+
return pathPrefix;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function relativeGrepLine(line, workDir, pathOnly = false, outputMode = 'content', filenameOmitted = false) {
|
|
84
|
+
const normalized = normalizeGrepLine(line, pathOnly, outputMode, filenameOmitted);
|
|
85
|
+
if (!workDir) return normalized;
|
|
86
|
+
if (pathOnly) return relativePathPrefix(normalized, workDir);
|
|
87
|
+
if (filenameOmitted) return normalized;
|
|
88
|
+
const split = splitGrepLinePrefix(normalized);
|
|
89
|
+
if (split) {
|
|
90
|
+
return relativePathPrefix(normalized.slice(0, split.pathEnd), workDir) + normalized.slice(split.pathEnd);
|
|
91
|
+
}
|
|
92
|
+
if (outputMode === 'count') {
|
|
93
|
+
const countSplit = splitGrepCountPrefix(normalized);
|
|
94
|
+
if (countSplit) {
|
|
95
|
+
return relativePathPrefix(normalized.slice(0, countSplit.pathEnd), workDir) + normalized.slice(countSplit.pathEnd);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
return normalized;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function relativeSearchResultPath(path, workDir) {
|
|
102
|
+
const normalizedWorkDir = normalizeOutputPath(workDir);
|
|
103
|
+
const normalizedAbs = normalizeOutputPath(path);
|
|
104
|
+
if (normalizedAbs.startsWith(normalizedWorkDir + '/') || normalizedAbs.startsWith(normalizedWorkDir + '\\')) {
|
|
105
|
+
return normalizedAbs.slice(normalizedWorkDir.length + 1);
|
|
106
|
+
}
|
|
107
|
+
return normalizedAbs;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function uniqueStrings(values) {
|
|
111
|
+
return Array.from(new Set(values.filter((value) => typeof value === 'string' && value)));
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function coerceNonNegInt(value) {
|
|
115
|
+
if (value === undefined || value === null || value === '') return null;
|
|
116
|
+
const n = Number(value);
|
|
117
|
+
if (!Number.isFinite(n) || n < 0) return NaN;
|
|
118
|
+
return Math.floor(n);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function globMtimeTiePath(entry) {
|
|
122
|
+
const p = String(entry?.path ?? entry?.full ?? '');
|
|
123
|
+
return process.platform === 'win32' ? p.toLocaleLowerCase() : p;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// CC parity (GrepTool.ts): a single glob string may pack multiple filters
|
|
127
|
+
// separated by whitespace or commas, e.g. "*.ts,*.tsx" or "*.ts *.tsx". Split
|
|
128
|
+
// each into its own --glob. Brace patterns ("*.{ts,tsx}") are left intact so
|
|
129
|
+
// their internal commas are not torn apart.
|
|
130
|
+
function splitGlobString(value) {
|
|
131
|
+
const out = [];
|
|
132
|
+
const str = String(value);
|
|
133
|
+
let depth = 0;
|
|
134
|
+
let token = '';
|
|
135
|
+
const flush = () => {
|
|
136
|
+
const trimmed = token.trim();
|
|
137
|
+
if (trimmed) out.push(trimmed);
|
|
138
|
+
token = '';
|
|
139
|
+
};
|
|
140
|
+
for (const ch of str) {
|
|
141
|
+
if (ch === '{') {
|
|
142
|
+
depth++;
|
|
143
|
+
token += ch;
|
|
144
|
+
} else if (ch === '}') {
|
|
145
|
+
if (depth > 0) depth--;
|
|
146
|
+
token += ch;
|
|
147
|
+
} else if (depth === 0 && (ch === ',' || /\s/.test(ch))) {
|
|
148
|
+
flush();
|
|
149
|
+
} else {
|
|
150
|
+
token += ch;
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
flush();
|
|
154
|
+
return out;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function resolveSearchScope(root, workDir) {
|
|
158
|
+
return isAbsolute(root) ? resolve(root) : resolveAgainstCwd(root, workDir);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function isUncOrSmbPath(path) {
|
|
162
|
+
if (typeof path !== 'string' || !path) return false;
|
|
163
|
+
return path.startsWith('\\\\') || path.startsWith('//');
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function uncRefusalMessage(toolName, original, resolved) {
|
|
167
|
+
const shown = normalizeOutputPath(resolved || original || '');
|
|
168
|
+
return `Error: ${toolName} refuses UNC/SMB path ${JSON.stringify(shown)}; remote share access is blocked to prevent NTLM credential leaks`;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function basePathDiagnostic(basePaths, workDir) {
|
|
172
|
+
return basePaths.map((basePath) => {
|
|
173
|
+
const resolved = resolveSearchScope(basePath, workDir);
|
|
174
|
+
try {
|
|
175
|
+
const st = statSync(resolved);
|
|
176
|
+
return `${normalizeOutputPath(basePath)}: ${st.isDirectory() ? 'path exists (dir)' : 'path exists (file)'}`;
|
|
177
|
+
} catch (err) {
|
|
178
|
+
return `${normalizeOutputPath(basePath)}: path does not exist (${err?.code || 'ENOENT'})`;
|
|
179
|
+
}
|
|
180
|
+
}).join('; ');
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
function grepMissingPatternMessage() {
|
|
184
|
+
return 'Error: grep requires pattern.';
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
function globMissingPatternMessage() {
|
|
188
|
+
return 'Error: glob requires pattern.';
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
function parseGrepCountLine(line) {
|
|
192
|
+
const text = String(line || '');
|
|
193
|
+
const searchFrom = /^[A-Za-z]:/.test(text) ? 2 : 0;
|
|
194
|
+
const idx = text.lastIndexOf(':');
|
|
195
|
+
if (idx <= searchFrom) return null;
|
|
196
|
+
const count = Number(text.slice(idx + 1));
|
|
197
|
+
if (!Number.isFinite(count) || count <= 0) return null;
|
|
198
|
+
const path = text.slice(0, idx);
|
|
199
|
+
if (!path) return null;
|
|
200
|
+
return { path, count };
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// Per-pattern file-count probe for array grby: runs ONE rg --files-with-matches
|
|
204
|
+
// per pattern (reusing the same scope/glob/type/flags as the merged search) so
|
|
205
|
+
// the summary line can surface patterns that matched zero files — otherwise the
|
|
206
|
+
// merged result hides which member of the array contributed nothing.
|
|
207
|
+
async function _perPatternFileCounts({ patterns, searchPath, globPatterns, caseInsensitive, multilineMode, fileType, workDir }) {
|
|
208
|
+
const results = await Promise.all(patterns.map(async (pattern) => {
|
|
209
|
+
try {
|
|
210
|
+
const rgArgs = buildGrepRgArgs({
|
|
211
|
+
patterns: [pattern],
|
|
212
|
+
searchPath,
|
|
213
|
+
globPatterns,
|
|
214
|
+
outputMode: 'files_with_matches',
|
|
215
|
+
caseInsensitive,
|
|
216
|
+
showLineNumbers: false,
|
|
217
|
+
beforeN: null,
|
|
218
|
+
afterN: null,
|
|
219
|
+
contextN: null,
|
|
220
|
+
multilineMode,
|
|
221
|
+
fileType,
|
|
222
|
+
onlyMatching: false,
|
|
223
|
+
});
|
|
224
|
+
const stdout = await runRg(rgArgs, { cwd: workDir });
|
|
225
|
+
// A boxed partial/truncated stdout means the count is not the true
|
|
226
|
+
// total — render '?' rather than a misleadingly-low or zero number.
|
|
227
|
+
if (stdout && typeof stdout === 'object' && (stdout.partial || stdout.truncated)) return null;
|
|
228
|
+
return String(stdout).split('\n').filter(Boolean).length;
|
|
229
|
+
} catch {
|
|
230
|
+
// Probe threw → count unknown, NOT zero.
|
|
231
|
+
return null;
|
|
232
|
+
}
|
|
233
|
+
}));
|
|
234
|
+
// Quote first (so newline/control chars can't inject footer lines), then
|
|
235
|
+
// truncate the quoted string to 40 chars — matches the no-match path.
|
|
236
|
+
const trunc = (p) => {
|
|
237
|
+
const q = JSON.stringify(p);
|
|
238
|
+
return q.length > 40 ? `${q.slice(0, 40)}...` : q;
|
|
239
|
+
};
|
|
240
|
+
return `\n# per-pattern: ${patterns.map((p, i) => `${trunc(p)}=${results[i] === null ? '?' : `${results[i]} files`}`).join(', ')}`;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
function formatGrepOutput({ windowed, totalWindowed, totalKnown, headLimit, offset, outputMode, patterns, beforeN, afterN, contextN, searchPath, grepResolvedPath, workDir, globPatterns, fileType, filenameOmitted = false, prefix = '', broadAdvisory = true }) {
|
|
244
|
+
const lines = headLimit === Infinity ? windowed : windowed.slice(0, headLimit);
|
|
245
|
+
const normalized = lines.map((line) => relativeGrepLine(line, workDir, outputMode === 'files_with_matches', outputMode, filenameOmitted));
|
|
246
|
+
const remaining = Math.max(0, totalWindowed - lines.length);
|
|
247
|
+
const shown = lines.length;
|
|
248
|
+
const total = totalWindowed;
|
|
249
|
+
const scopePath = JSON.stringify(normalizeOutputPath(searchPath));
|
|
250
|
+
const truncated = (remaining > 0 || !totalKnown)
|
|
251
|
+
? (totalKnown
|
|
252
|
+
? `\n[Showing ${shown} of ${total} results; pass offset:${offset + shown} for more]`
|
|
253
|
+
: `\n[Showing ${shown} (more matches exist — use output_mode:'count' for the exact total on ${scopePath}); pass offset:${offset + shown} for more]`)
|
|
254
|
+
: '';
|
|
255
|
+
|
|
256
|
+
let countSummary = '';
|
|
257
|
+
if (outputMode === 'count') {
|
|
258
|
+
let totalMatches = 0;
|
|
259
|
+
let fileCount = 0;
|
|
260
|
+
for (const line of normalized) {
|
|
261
|
+
const m = line.match(/(?:^|:)(\d+)$/);
|
|
262
|
+
if (m) { totalMatches += Number(m[1]); fileCount++; }
|
|
263
|
+
}
|
|
264
|
+
countSummary = `\n[total ${totalMatches} match${totalMatches === 1 ? '' : 'es'} across ${fileCount} file${fileCount === 1 ? '' : 's'}]`;
|
|
265
|
+
}
|
|
266
|
+
const hasContext = (beforeN > 0 || afterN > 0 || contextN > 0);
|
|
267
|
+
const groupedBody = (outputMode === 'content' && !hasContext && !filenameOmitted)
|
|
268
|
+
? groupGrepContentByFile(normalized)
|
|
269
|
+
: normalized.join('\n');
|
|
270
|
+
const body = groupedBody + truncated + countSummary;
|
|
271
|
+
return `${prefix}${body}`;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
export async function executeGrepTool(args, workDir, executeChildBuiltinTool, readStateScope = null, options = {}) {
|
|
275
|
+
args = normalizeGrepArgs(args);
|
|
276
|
+
// Shape context immediately before deriving rg flags. This keeps the
|
|
277
|
+
// Lead-direct MCP path and direct executeGrepTool callers on the same
|
|
278
|
+
// policy even if they bypass or race the outer builtin arg guard.
|
|
279
|
+
applyGrepContextLeadPolicy(args);
|
|
280
|
+
args.path = normalizeInputPath(args.path);
|
|
281
|
+
args.pattern = coerceShapeFlex(args.pattern);
|
|
282
|
+
args.glob = coerceShapeFlex(args.glob);
|
|
283
|
+
const rawPattern = args.pattern;
|
|
284
|
+
const patterns = uniqueStrings((Array.isArray(rawPattern)
|
|
285
|
+
? rawPattern.filter(p => typeof p === 'string' && p)
|
|
286
|
+
: (rawPattern ? [String(rawPattern)] : [])).map(normalizeSearchPattern));
|
|
287
|
+
if (patterns.length === 0) {
|
|
288
|
+
if (args.glob || hasGlobMagic(args.path)) {
|
|
289
|
+
const globArgs = {
|
|
290
|
+
pattern: hasGlobMagic(args.path) ? args.path : args.glob,
|
|
291
|
+
path: hasGlobMagic(args.path) ? undefined : (args.path || '.'),
|
|
292
|
+
};
|
|
293
|
+
if (args.head_limit !== undefined) globArgs.head_limit = args.head_limit;
|
|
294
|
+
if (args.offset !== undefined) globArgs.offset = args.offset;
|
|
295
|
+
return executeChildBuiltinTool('glob', globArgs, workDir);
|
|
296
|
+
}
|
|
297
|
+
return grepMissingPatternMessage();
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
const GREP_MULTILINE_PATTERN_CAP = 5;
|
|
301
|
+
const GREP_ARRAY_PATTERN_CAP = 20;
|
|
302
|
+
const multilineMode = args.multiline === true;
|
|
303
|
+
if (multilineMode && patterns.length > GREP_MULTILINE_PATTERN_CAP) {
|
|
304
|
+
return `Error: multiline:true with more than ${GREP_MULTILINE_PATTERN_CAP} patterns is not allowed (got ${patterns.length}); split into separate grep calls`;
|
|
305
|
+
}
|
|
306
|
+
if (patterns.length > GREP_ARRAY_PATTERN_CAP) {
|
|
307
|
+
return `Error: pattern array exceeds the ${GREP_ARRAY_PATTERN_CAP}-pattern cap (got ${patterns.length}); split into separate grep calls`;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
let searchPath = args.path || '.';
|
|
311
|
+
const rawGlob = args.glob;
|
|
312
|
+
const rawGlobs = uniqueStrings((Array.isArray(rawGlob)
|
|
313
|
+
? rawGlob.filter(g => typeof g === 'string' && g)
|
|
314
|
+
: (rawGlob ? [String(rawGlob)] : []))
|
|
315
|
+
.flatMap(splitGlobString)
|
|
316
|
+
.map(normalizeInputPath));
|
|
317
|
+
if (hasGlobMagic(searchPath)) {
|
|
318
|
+
const { baseDir, relativePattern } = extractGlobBaseDirectory(searchPath);
|
|
319
|
+
searchPath = baseDir || '.';
|
|
320
|
+
rawGlobs.unshift(relativePattern.replace(/^\//, ''));
|
|
321
|
+
}
|
|
322
|
+
const grepResolvedPath = resolveSearchScope(searchPath, workDir);
|
|
323
|
+
if (isUncOrSmbPath(searchPath) || isUncOrSmbPath(grepResolvedPath)) {
|
|
324
|
+
return uncRefusalMessage('grep', searchPath, grepResolvedPath);
|
|
325
|
+
}
|
|
326
|
+
const globPatterns = [];
|
|
327
|
+
const rootFwd = normalizeOutputPath(grepResolvedPath).replace(/\/+$/, '');
|
|
328
|
+
for (const g of rawGlobs) {
|
|
329
|
+
if (isAbsolute(g)) {
|
|
330
|
+
const { baseDir, relativePattern } = extractGlobBaseDirectory(g);
|
|
331
|
+
const baseFwd = baseDir ? normalizeOutputPath(baseDir).replace(/\/+$/, '') : '';
|
|
332
|
+
const rel = relativePattern.replace(/^\//, '');
|
|
333
|
+
// Windows is case-insensitive: compare path casing accordingly so a
|
|
334
|
+
// valid in-root absolute glob is not rejected when its drive/dir
|
|
335
|
+
// casing differs from the resolved root.
|
|
336
|
+
const ci = process.platform === 'win32';
|
|
337
|
+
const baseCmp = ci ? baseFwd.toLowerCase() : baseFwd;
|
|
338
|
+
const rootCmp = ci ? rootFwd.toLowerCase() : rootFwd;
|
|
339
|
+
if (!baseFwd || baseCmp === rootCmp) {
|
|
340
|
+
globPatterns.push(rel);
|
|
341
|
+
} else if (baseCmp.startsWith(rootCmp + '/')) {
|
|
342
|
+
const prefix = baseFwd.slice(rootFwd.length + 1);
|
|
343
|
+
globPatterns.push(prefix ? `${prefix}/${rel}` : rel);
|
|
344
|
+
} else {
|
|
345
|
+
return `Error: absolute glob ${JSON.stringify(g)} resolves outside search root ${JSON.stringify(rootFwd)}; pass a relative glob or move the search path`;
|
|
346
|
+
}
|
|
347
|
+
} else {
|
|
348
|
+
globPatterns.push(g);
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
// ripgrep `--glob` uses forward slashes on all platforms; canonicalize
|
|
352
|
+
// `\`→`/` (win32 only) so a `**\*.ts` filter matches instead of being
|
|
353
|
+
// parsed as an escape sequence.
|
|
354
|
+
const normalizedGlobPatterns = uniqueStrings(globPatterns.map(canonicalizeGlobSlashes));
|
|
355
|
+
|
|
356
|
+
const ALLOWED_OUTPUT_MODES = new Set(['files_with_matches', 'content', 'count']);
|
|
357
|
+
const rawOutputMode = typeof args.output_mode === 'string' ? args.output_mode.trim() : '';
|
|
358
|
+
if (rawOutputMode && !ALLOWED_OUTPUT_MODES.has(rawOutputMode)) {
|
|
359
|
+
return `Error: invalid output_mode ${JSON.stringify(args.output_mode)}; expected one of ${[...ALLOWED_OUTPUT_MODES].join(', ')}`;
|
|
360
|
+
}
|
|
361
|
+
// Default to `content` when output_mode is omitted. A pattern is always
|
|
362
|
+
// present here (the no-pattern case returned above), so this is a content
|
|
363
|
+
// search — it should return the matching lines WITH line numbers, not just
|
|
364
|
+
// filenames. Filename-only was forcing callers to re-grep for the actual
|
|
365
|
+
// coordinates (the explorer over-iteration root cause). `files_with_matches`
|
|
366
|
+
// is now opt-in; pure filename discovery belongs to `glob`.
|
|
367
|
+
const outputMode = rawOutputMode || 'content';
|
|
368
|
+
const headLimitRaw = args.head_limit;
|
|
369
|
+
const headLimitCoerced = coerceNonNegInt(headLimitRaw);
|
|
370
|
+
if (Number.isNaN(headLimitCoerced)) {
|
|
371
|
+
return `Error: invalid head_limit ${JSON.stringify(headLimitRaw)}; expected a non-negative integer (0 = unlimited)`;
|
|
372
|
+
}
|
|
373
|
+
const headLimit = headLimitCoerced === null
|
|
374
|
+
? 80
|
|
375
|
+
: (headLimitCoerced === 0 ? Infinity : headLimitCoerced);
|
|
376
|
+
const offsetCoerced = coerceNonNegInt(args.offset);
|
|
377
|
+
if (Number.isNaN(offsetCoerced)) {
|
|
378
|
+
return `Error: invalid offset ${JSON.stringify(args.offset)}; expected a non-negative integer`;
|
|
379
|
+
}
|
|
380
|
+
const offset = offsetCoerced === null || offsetCoerced === 0 ? 0 : offsetCoerced;
|
|
381
|
+
const caseInsensitive = args['-i'] === true;
|
|
382
|
+
const showLineNumbers = args['-n'] !== false;
|
|
383
|
+
const coerceContext = (value) => {
|
|
384
|
+
if (value === undefined || value === null || value === '') return null;
|
|
385
|
+
const n = Number(value);
|
|
386
|
+
if (!Number.isFinite(n) || n < 0) return NaN;
|
|
387
|
+
return Math.min(Math.floor(n), GREP_CONTEXT_MAX);
|
|
388
|
+
};
|
|
389
|
+
let afterN = coerceContext(args['-A']);
|
|
390
|
+
let beforeN = coerceContext(args['-B']);
|
|
391
|
+
let contextN = args['-C'] !== undefined && args['-C'] !== null && args['-C'] !== ''
|
|
392
|
+
? coerceContext(args['-C'])
|
|
393
|
+
: coerceContext(args.context);
|
|
394
|
+
if (contextN !== null && contextN > 0) {
|
|
395
|
+
if (afterN === 0) afterN = null;
|
|
396
|
+
if (beforeN === 0) beforeN = null;
|
|
397
|
+
}
|
|
398
|
+
for (const [name, value] of [['-A', afterN], ['-B', beforeN], ['-C', contextN]]) {
|
|
399
|
+
if (Number.isNaN(value)) {
|
|
400
|
+
return `Error: invalid context option ${name}; expected a non-negative finite integer`;
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
const rawType = args.type;
|
|
404
|
+
let fileType = '';
|
|
405
|
+
let fileTypes = [];
|
|
406
|
+
if (Array.isArray(rawType)) {
|
|
407
|
+
for (const entry of rawType) {
|
|
408
|
+
if (typeof entry !== 'string') {
|
|
409
|
+
return `Error: invalid type entry ${JSON.stringify(entry)}; expected string`;
|
|
410
|
+
}
|
|
411
|
+
const t = entry.trim();
|
|
412
|
+
if (t) fileTypes.push(t);
|
|
413
|
+
}
|
|
414
|
+
} else if (typeof rawType === 'string') {
|
|
415
|
+
const t = rawType.trim();
|
|
416
|
+
if (t) {
|
|
417
|
+
fileTypes = [t];
|
|
418
|
+
fileType = t;
|
|
419
|
+
}
|
|
420
|
+
} else if (rawType !== undefined && rawType !== null) {
|
|
421
|
+
return `Error: invalid type ${JSON.stringify(rawType)}; expected string or string[]`;
|
|
422
|
+
}
|
|
423
|
+
if (fileTypes.length > 1) fileType = fileTypes;
|
|
424
|
+
else if (fileTypes.length === 1) fileType = fileTypes[0];
|
|
425
|
+
const cacheKey = buildGrepCacheKey({
|
|
426
|
+
patterns,
|
|
427
|
+
searchPath: normalizeOutputPath(grepResolvedPath),
|
|
428
|
+
globPatterns: normalizedGlobPatterns,
|
|
429
|
+
outputMode,
|
|
430
|
+
headLimit,
|
|
431
|
+
offset,
|
|
432
|
+
caseInsensitive,
|
|
433
|
+
showLineNumbers,
|
|
434
|
+
beforeN,
|
|
435
|
+
afterN,
|
|
436
|
+
contextN,
|
|
437
|
+
multilineMode,
|
|
438
|
+
onlyMatching: args['-o'] === true,
|
|
439
|
+
fileType,
|
|
440
|
+
});
|
|
441
|
+
// Single-file grep registers a whole-file read snapshot (parity with
|
|
442
|
+
// apply_patch), satisfying the read-before-edit guard while keeping drift
|
|
443
|
+
// detection intact via the auto-computed contentHash. Directory/glob greps
|
|
444
|
+
// do NOT record.
|
|
445
|
+
const recordGrepReadSnapshot = (st) => {
|
|
446
|
+
try {
|
|
447
|
+
if (st && st.isFile()) {
|
|
448
|
+
recordReadSnapshot(grepResolvedPath, st, readStateScope, { source: 'grep' });
|
|
449
|
+
}
|
|
450
|
+
} catch {}
|
|
451
|
+
};
|
|
452
|
+
|
|
453
|
+
const cached = cacheGet(cacheKey);
|
|
454
|
+
// Cache-hit returns a PRIOR grep's output; the file may have changed since
|
|
455
|
+
// that result was cached. Recording a fresh whole-file snapshot here would
|
|
456
|
+
// mismatch what the caller actually saw (stale cached lines) and defeat
|
|
457
|
+
// drift detection. So only the fresh-compute path (below) records a read.
|
|
458
|
+
if (cached !== null) return cached;
|
|
459
|
+
|
|
460
|
+
let grepStat;
|
|
461
|
+
try { grepStat = statSync(grepResolvedPath); }
|
|
462
|
+
catch (err) {
|
|
463
|
+
const msg = `Error: path does not exist: ${normalizeOutputPath(grepResolvedPath)} (${err?.code || 'ENOENT'})`;
|
|
464
|
+
return msg + await _suggestIndexedPaths(grepResolvedPath, executeChildBuiltinTool, workDir);
|
|
465
|
+
}
|
|
466
|
+
const filenameOmitted = grepStat.isFile();
|
|
467
|
+
|
|
468
|
+
// rg builds --glob overrides rooted at its process cwd and relativizes each
|
|
469
|
+
// candidate against it with a CASE-SENSITIVE prefix strip; workDir is
|
|
470
|
+
// case-normalized (lowercased) while callers pass real-cased absolute paths,
|
|
471
|
+
// so the strip fails and slash-anchored globs (src/**/*.mjs) silently match
|
|
472
|
+
// nothing. Spawn rg at the TRUE-CASED search root so relativization — and
|
|
473
|
+
// therefore glob anchoring — always engages. Relative searchPath keeps the
|
|
474
|
+
// workDir cwd (both sides already share workDir's casing).
|
|
475
|
+
let rgSpawnCwd = workDir;
|
|
476
|
+
if (isAbsolute(searchPath)) {
|
|
477
|
+
searchPath = trueCasePath(searchPath);
|
|
478
|
+
if (grepStat.isDirectory()) rgSpawnCwd = searchPath;
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
try {
|
|
482
|
+
const GREP_CONTENT_HARD_CAP = 300;
|
|
483
|
+
const callerExplicitUnlimited = headLimitCoerced === 0;
|
|
484
|
+
const effectiveHeadLimit = headLimit === Infinity
|
|
485
|
+
? (callerExplicitUnlimited ? Infinity : (outputMode === 'content' ? GREP_CONTENT_HARD_CAP : Infinity))
|
|
486
|
+
: headLimit;
|
|
487
|
+
const rgArgs = buildGrepRgArgs({
|
|
488
|
+
patterns,
|
|
489
|
+
searchPath,
|
|
490
|
+
globPatterns: normalizedGlobPatterns,
|
|
491
|
+
outputMode,
|
|
492
|
+
caseInsensitive,
|
|
493
|
+
showLineNumbers,
|
|
494
|
+
beforeN,
|
|
495
|
+
afterN,
|
|
496
|
+
contextN,
|
|
497
|
+
multilineMode,
|
|
498
|
+
fileType,
|
|
499
|
+
onlyMatching: args['-o'] === true,
|
|
500
|
+
});
|
|
501
|
+
let windowed;
|
|
502
|
+
let totalWindowed = 0;
|
|
503
|
+
let totalKnown = true;
|
|
504
|
+
let rgPartialSuffix = '';
|
|
505
|
+
if (effectiveHeadLimit !== Infinity) {
|
|
506
|
+
const summaryLimit = outputMode === 'content' ? 120 : 0;
|
|
507
|
+
const streamed = await runRgWindowedLines(rgArgs, { cwd: rgSpawnCwd }, {
|
|
508
|
+
offset,
|
|
509
|
+
limit: effectiveHeadLimit,
|
|
510
|
+
summaryLimit,
|
|
511
|
+
});
|
|
512
|
+
windowed = streamed.lines;
|
|
513
|
+
totalWindowed = streamed.totalSeen;
|
|
514
|
+
totalKnown = streamed.complete;
|
|
515
|
+
if (streamed.partial) {
|
|
516
|
+
totalKnown = false;
|
|
517
|
+
rgPartialSuffix = streamed.rgStderr
|
|
518
|
+
? `\n[warning] rg exit 2 (partial results): ${String(streamed.rgStderr).trim().slice(0, 300)}`
|
|
519
|
+
: '\n[warning] rg exit 2 (partial results)';
|
|
520
|
+
}
|
|
521
|
+
} else {
|
|
522
|
+
const stdout = await runRg(rgArgs, { cwd: rgSpawnCwd });
|
|
523
|
+
const allLines = String(stdout).split('\n').filter(Boolean);
|
|
524
|
+
windowed = offset > 0 ? allLines.slice(offset) : allLines;
|
|
525
|
+
totalWindowed = windowed.length;
|
|
526
|
+
// runRg boxes stdout + sets .truncated when the 20MB stdout cap
|
|
527
|
+
// tripped (rg-runner). Mark the result incomplete so formatGrepOutput
|
|
528
|
+
// emits the truncation notice instead of presenting it as complete.
|
|
529
|
+
if (typeof stdout === 'object' && stdout.truncated) totalKnown = false;
|
|
530
|
+
if (typeof stdout === 'object' && stdout.partial) {
|
|
531
|
+
totalKnown = false;
|
|
532
|
+
rgPartialSuffix = stdout.rgStderr
|
|
533
|
+
? `\n[warning] rg exit 2 (partial results): ${String(stdout.rgStderr).trim().slice(0, 300)}`
|
|
534
|
+
: '\n[warning] rg exit 2 (partial results)';
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
let body = formatGrepOutput({
|
|
538
|
+
windowed,
|
|
539
|
+
totalWindowed,
|
|
540
|
+
totalKnown,
|
|
541
|
+
headLimit,
|
|
542
|
+
offset,
|
|
543
|
+
outputMode,
|
|
544
|
+
patterns,
|
|
545
|
+
beforeN,
|
|
546
|
+
afterN,
|
|
547
|
+
contextN,
|
|
548
|
+
searchPath,
|
|
549
|
+
grepResolvedPath,
|
|
550
|
+
workDir,
|
|
551
|
+
globPatterns: normalizedGlobPatterns,
|
|
552
|
+
fileType,
|
|
553
|
+
filenameOmitted,
|
|
554
|
+
});
|
|
555
|
+
if (!body) {
|
|
556
|
+
const pathInfo = grepStat.isDirectory() ? 'path exists (dir)' : 'path exists (file)';
|
|
557
|
+
const patternStr = patterns.length === 1 ? JSON.stringify(patterns[0]) : JSON.stringify(patterns);
|
|
558
|
+
const globStr = normalizedGlobPatterns.length > 0 ? ` glob=${JSON.stringify(normalizedGlobPatterns)}` : '';
|
|
559
|
+
body = `(no matches) pattern=${patternStr} path=${searchPath}${globStr}; ${pathInfo}`;
|
|
560
|
+
// Cased-letter hint: a no-match single-pattern search whose pattern
|
|
561
|
+
// carries cased letters may have failed only on case. Run ONE
|
|
562
|
+
// case-insensitive probe; if it would match, nudge toward `-i`.
|
|
563
|
+
// Skipped for arrays (single-pattern support is enough) and when
|
|
564
|
+
// `-i` is already set or the pattern has no cased letters. Also
|
|
565
|
+
// require a true zero-match search: an empty body with offset>0 (or
|
|
566
|
+
// pre-offset matches) just means the window skipped past real
|
|
567
|
+
// case-sensitive hits, so the hint would be misleading.
|
|
568
|
+
const trueZeroMatch = offset === 0 && totalWindowed === 0;
|
|
569
|
+
if (trueZeroMatch && !caseInsensitive && patterns.length === 1 && /[A-Za-z]/.test(patterns[0])) {
|
|
570
|
+
try {
|
|
571
|
+
const probeArgs = buildGrepRgArgs({
|
|
572
|
+
patterns,
|
|
573
|
+
searchPath,
|
|
574
|
+
globPatterns: normalizedGlobPatterns,
|
|
575
|
+
outputMode: 'files_with_matches',
|
|
576
|
+
caseInsensitive: true,
|
|
577
|
+
showLineNumbers: false,
|
|
578
|
+
beforeN: null,
|
|
579
|
+
afterN: null,
|
|
580
|
+
contextN: null,
|
|
581
|
+
multilineMode,
|
|
582
|
+
fileType,
|
|
583
|
+
onlyMatching: false,
|
|
584
|
+
});
|
|
585
|
+
const probeOut = await runRg(probeArgs, { cwd: rgSpawnCwd });
|
|
586
|
+
if (String(probeOut).split('\n').some(Boolean)) {
|
|
587
|
+
body += ' (case-insensitive would match — try -i)';
|
|
588
|
+
}
|
|
589
|
+
} catch { /* best-effort hint */ }
|
|
590
|
+
}
|
|
591
|
+
}
|
|
592
|
+
// Array-pattern visibility: append a per-pattern file-count summary so
|
|
593
|
+
// zero-hit members of the merged result are not silently hidden.
|
|
594
|
+
let perPatternSummary = '';
|
|
595
|
+
if (patterns.length > 1) {
|
|
596
|
+
perPatternSummary = await _perPatternFileCounts({
|
|
597
|
+
patterns,
|
|
598
|
+
searchPath,
|
|
599
|
+
globPatterns: normalizedGlobPatterns,
|
|
600
|
+
caseInsensitive,
|
|
601
|
+
multilineMode,
|
|
602
|
+
fileType,
|
|
603
|
+
workDir: rgSpawnCwd,
|
|
604
|
+
});
|
|
605
|
+
}
|
|
606
|
+
const out = body + rgPartialSuffix + perPatternSummary;
|
|
607
|
+
const shownLines = headLimit === Infinity ? windowed : windowed.slice(0, headLimit);
|
|
608
|
+
const remaining = Math.max(0, totalWindowed - shownLines.length);
|
|
609
|
+
// Mirrors formatGrepOutput truncation / totalKnown semantics.
|
|
610
|
+
if (options?.scopedCacheOutcome && (!totalKnown || remaining > 0)) {
|
|
611
|
+
markScopedCacheIncomplete(options.scopedCacheOutcome);
|
|
612
|
+
}
|
|
613
|
+
recordGrepReadSnapshot(grepStat);
|
|
614
|
+
if (totalKnown && remaining === 0) {
|
|
615
|
+
cacheSet(cacheKey, out, { scopes: [grepResolvedPath] });
|
|
616
|
+
}
|
|
617
|
+
// ② completion progress (claude "Found N" parity). Best-effort,
|
|
618
|
+
// no-op when onProgress is absent (no progressToken).
|
|
619
|
+
if (typeof options?.onProgress === 'function') {
|
|
620
|
+
try {
|
|
621
|
+
let _n = totalWindowed;
|
|
622
|
+
let _label = 'matches';
|
|
623
|
+
if (outputMode === 'files_with_matches') {
|
|
624
|
+
_label = 'files';
|
|
625
|
+
} else if (outputMode === 'count') {
|
|
626
|
+
_n = 0;
|
|
627
|
+
for (const _line of windowed) { const _c = parseGrepCountLine(_line); if (_c) _n += _c.count; }
|
|
628
|
+
}
|
|
629
|
+
options.onProgress(`found ${_n} ${_label}`);
|
|
630
|
+
} catch { /* best-effort */ }
|
|
631
|
+
}
|
|
632
|
+
return out;
|
|
633
|
+
}
|
|
634
|
+
catch (err) {
|
|
635
|
+
const stderr = err?.stderr ? String(err.stderr).trim() : '';
|
|
636
|
+
const msg = stderr || err?.message || String(err);
|
|
637
|
+
return `Error: ${msg.slice(0, 500)}`;
|
|
638
|
+
}
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
export async function executeGlobTool(args, workDir, options = {}) {
|
|
642
|
+
args = normalizeGlobArgs(args);
|
|
643
|
+
args.path = Array.isArray(args.path)
|
|
644
|
+
? args.path.map((p) => normalizeInputPath(p)).filter((p) => typeof p === 'string' && p)
|
|
645
|
+
: normalizeInputPath(args.path);
|
|
646
|
+
if (Array.isArray(args.path) && args.path.length === 0) {
|
|
647
|
+
return 'Error: path array must contain at least one base directory';
|
|
648
|
+
}
|
|
649
|
+
args.pattern = coerceShapeFlex(args.pattern);
|
|
650
|
+
const rawPattern = args.pattern;
|
|
651
|
+
// ripgrep `--glob` matchers use forward slashes on all platforms;
|
|
652
|
+
// canonicalize `\`→`/` (win32 only) so a `**\*.ts` pattern matches
|
|
653
|
+
// instead of being parsed as an escape sequence.
|
|
654
|
+
let patterns = uniqueStrings((Array.isArray(rawPattern)
|
|
655
|
+
? rawPattern.filter(p => typeof p === 'string' && p)
|
|
656
|
+
: (rawPattern ? [String(rawPattern)] : [])).map(normalizeInputPath).map(canonicalizeGlobSlashes));
|
|
657
|
+
if (patterns.length === 0) {
|
|
658
|
+
if (Array.isArray(args.path)) {
|
|
659
|
+
const pathGlobs = args.path.filter((p) => hasGlobMagic(p));
|
|
660
|
+
if (pathGlobs.length > 0 && pathGlobs.length === args.path.length) {
|
|
661
|
+
patterns = uniqueStrings(pathGlobs.map(normalizeInputPath).map(canonicalizeGlobSlashes));
|
|
662
|
+
args.path = undefined;
|
|
663
|
+
}
|
|
664
|
+
} else if (hasGlobMagic(args.path)) {
|
|
665
|
+
patterns = [canonicalizeGlobSlashes(normalizeInputPath(args.path))];
|
|
666
|
+
args.path = undefined;
|
|
667
|
+
}
|
|
668
|
+
}
|
|
669
|
+
if (patterns.length === 0) {
|
|
670
|
+
return globMissingPatternMessage();
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
const basePaths = (Array.isArray(args.path) && args.path.length > 0)
|
|
674
|
+
? args.path
|
|
675
|
+
: [args.path || '.'];
|
|
676
|
+
// A base path carrying glob magic (path:'src/**/cache/*') names a SET of
|
|
677
|
+
// directories, not a literal one — resolving it literally ENOENTs. Split
|
|
678
|
+
// it the way grep's path handling does: walk from the static baseDir and
|
|
679
|
+
// fold the magic suffix into each pattern under that root.
|
|
680
|
+
const baseEntries = basePaths.map((basePath) => {
|
|
681
|
+
if (typeof basePath !== 'string' || !hasGlobMagic(basePath)) return { root: basePath, prefix: '' };
|
|
682
|
+
const { baseDir, relativePattern } = extractGlobBaseDirectory(canonicalizeGlobSlashes(basePath));
|
|
683
|
+
// A trailing pure-`*` segment ("cache/*") means "the children" — the
|
|
684
|
+
// pattern itself supplies the leaf match, so nesting it one level
|
|
685
|
+
// deeper ("*/<pat>") would skip files directly under the dir. Drop
|
|
686
|
+
// that segment; `**` and mid-path magic still nest.
|
|
687
|
+
const segs = relativePattern.replace(/^\//, '').split('/').filter(Boolean);
|
|
688
|
+
if (segs[segs.length - 1] === '*') segs.pop();
|
|
689
|
+
return { root: baseDir || '.', prefix: segs.join('/') };
|
|
690
|
+
});
|
|
691
|
+
const resolvedSearchRoots = new Map();
|
|
692
|
+
function resolvedForSearchRoot(root) {
|
|
693
|
+
if (!resolvedSearchRoots.has(root)) {
|
|
694
|
+
resolvedSearchRoots.set(root, resolveSearchScope(root, workDir));
|
|
695
|
+
}
|
|
696
|
+
return resolvedSearchRoots.get(root);
|
|
697
|
+
}
|
|
698
|
+
for (const e of baseEntries) {
|
|
699
|
+
if (isUncOrSmbPath(e.root)) {
|
|
700
|
+
return uncRefusalMessage('glob', e.root, e.root);
|
|
701
|
+
}
|
|
702
|
+
const resolvedBase = resolvedForSearchRoot(e.root);
|
|
703
|
+
if (isUncOrSmbPath(resolvedBase)) {
|
|
704
|
+
return uncRefusalMessage('glob', e.root, resolvedBase);
|
|
705
|
+
}
|
|
706
|
+
}
|
|
707
|
+
for (const p of patterns) {
|
|
708
|
+
if (isAbsolute(p) && isUncOrSmbPath(p)) {
|
|
709
|
+
return uncRefusalMessage('glob', p, p);
|
|
710
|
+
}
|
|
711
|
+
}
|
|
712
|
+
const headLimitRaw = args.head_limit;
|
|
713
|
+
const headLimitCoerced = coerceNonNegInt(headLimitRaw);
|
|
714
|
+
if (Number.isNaN(headLimitCoerced)) {
|
|
715
|
+
return `Error: invalid head_limit ${JSON.stringify(headLimitRaw)}; expected a non-negative integer (0 = unlimited)`;
|
|
716
|
+
}
|
|
717
|
+
const headLimit = headLimitCoerced === null
|
|
718
|
+
? 100
|
|
719
|
+
: (headLimitCoerced === 0 ? Infinity : headLimitCoerced);
|
|
720
|
+
const offsetCoerced = coerceNonNegInt(args.offset);
|
|
721
|
+
if (Number.isNaN(offsetCoerced)) {
|
|
722
|
+
return `Error: invalid offset ${JSON.stringify(args.offset)}; expected a non-negative integer`;
|
|
723
|
+
}
|
|
724
|
+
const offset = offsetCoerced === null || offsetCoerced === 0 ? 0 : offsetCoerced;
|
|
725
|
+
// Internal-only ignore extension (see normalizeGlobArgs). Caller (e.g.
|
|
726
|
+
// ai-wrapped-dispatch broad-cwd preflight) appends basename ignore globs
|
|
727
|
+
// so head_limit bounds SOURCE entries rather than artifact noise.
|
|
728
|
+
const extraIgnoreGlobs = Array.isArray(args._extraIgnoreDirs)
|
|
729
|
+
? args._extraIgnoreDirs.map((name) => `!**/${name}/**`)
|
|
730
|
+
: [];
|
|
731
|
+
const groups = new Map();
|
|
732
|
+
function addToGroup(root, rel) {
|
|
733
|
+
if (!groups.has(root)) groups.set(root, []);
|
|
734
|
+
const rels = groups.get(root);
|
|
735
|
+
if (!rels.includes(rel)) rels.push(rel);
|
|
736
|
+
}
|
|
737
|
+
for (const p of patterns) {
|
|
738
|
+
if (isAbsolute(p)) {
|
|
739
|
+
const { baseDir, relativePattern } = extractGlobBaseDirectory(p);
|
|
740
|
+
addToGroup(baseDir || baseEntries[0]?.root || '.', relativePattern);
|
|
741
|
+
} else {
|
|
742
|
+
for (const e of baseEntries) addToGroup(e.root, e.prefix ? `${e.prefix}/${p}` : p);
|
|
743
|
+
}
|
|
744
|
+
}
|
|
745
|
+
|
|
746
|
+
const cacheBasePath = [...groups.keys()]
|
|
747
|
+
.map((root) => normalizeOutputPath(resolvedForSearchRoot(root)))
|
|
748
|
+
.sort()
|
|
749
|
+
.join('\x01');
|
|
750
|
+
const cacheKey = buildGlobCacheKey({ patterns, basePath: cacheBasePath, headLimit, offset, extraIgnore: extraIgnoreGlobs });
|
|
751
|
+
const cached = cacheGet(cacheKey);
|
|
752
|
+
if (cached !== null) return cached;
|
|
753
|
+
|
|
754
|
+
const globGroups = [...groups.entries()];
|
|
755
|
+
|
|
756
|
+
const allFiles = [];
|
|
757
|
+
const rgErrors = [];
|
|
758
|
+
let accumTruncated = false;
|
|
759
|
+
let rgStdoutTruncated = false;
|
|
760
|
+
let rgStdoutPartial = false;
|
|
761
|
+
const accumCap = 50000;
|
|
762
|
+
const groupRuns = await Promise.all(globGroups.map(async ([root, rels]) => {
|
|
763
|
+
const rgArgs = ['--files', '--hidden'];
|
|
764
|
+
for (const ex of DEFAULT_IGNORE_GLOBS) rgArgs.push('--glob', ex);
|
|
765
|
+
for (const ex of extraIgnoreGlobs) rgArgs.push('--glob', ex);
|
|
766
|
+
for (const rel of rels) rgArgs.push('--glob', rel);
|
|
767
|
+
const rgCwd = resolvedForSearchRoot(root);
|
|
768
|
+
rgArgs.push('.');
|
|
769
|
+
try { statSync(rgCwd); }
|
|
770
|
+
catch (err) {
|
|
771
|
+
return {
|
|
772
|
+
error: `path does not exist: ${normalizeOutputPath(rgCwd)} (${err?.code || 'ENOENT'})`,
|
|
773
|
+
paths: [],
|
|
774
|
+
stdoutTruncated: false,
|
|
775
|
+
};
|
|
776
|
+
}
|
|
777
|
+
try {
|
|
778
|
+
const stdout = await runRg(rgArgs, { cwd: rgCwd, timeout: 10000 });
|
|
779
|
+
const stdoutTruncated = Boolean(stdout && typeof stdout === 'object' && stdout.truncated);
|
|
780
|
+
const stdoutPartial = Boolean(stdout && typeof stdout === 'object' && stdout.partial);
|
|
781
|
+
const paths = [];
|
|
782
|
+
for (const line of String(stdout).split('\n')) {
|
|
783
|
+
const trimmed = line.trim();
|
|
784
|
+
if (!trimmed) continue;
|
|
785
|
+
paths.push(isAbsolute(trimmed) ? trimmed : resolveAgainstCwd(trimmed, rgCwd));
|
|
786
|
+
}
|
|
787
|
+
return { error: null, paths, stdoutTruncated, stdoutPartial };
|
|
788
|
+
} catch (err) {
|
|
789
|
+
const stderr = String(err?.stderr || err?.message || err).trim().split('\n').slice(0, 3).join('; ');
|
|
790
|
+
return {
|
|
791
|
+
error: `rg failed for ${normalizeOutputPath(root)}: ${stderr || 'unknown error'}`,
|
|
792
|
+
paths: [],
|
|
793
|
+
stdoutTruncated: false,
|
|
794
|
+
stdoutPartial: false,
|
|
795
|
+
};
|
|
796
|
+
}
|
|
797
|
+
}));
|
|
798
|
+
|
|
799
|
+
outer: for (const run of groupRuns) {
|
|
800
|
+
if (run.error) {
|
|
801
|
+
rgErrors.push(run.error);
|
|
802
|
+
continue;
|
|
803
|
+
}
|
|
804
|
+
if (run.stdoutTruncated) rgStdoutTruncated = true;
|
|
805
|
+
if (run.stdoutPartial) rgStdoutPartial = true;
|
|
806
|
+
for (const p of run.paths) {
|
|
807
|
+
allFiles.push(p);
|
|
808
|
+
if (allFiles.length >= accumCap) {
|
|
809
|
+
accumTruncated = true;
|
|
810
|
+
break outer;
|
|
811
|
+
}
|
|
812
|
+
}
|
|
813
|
+
}
|
|
814
|
+
if (rgErrors.length > 0 && allFiles.length === 0) {
|
|
815
|
+
return `Error: ${rgErrors.join(' | ').slice(0, 500)}`;
|
|
816
|
+
}
|
|
817
|
+
|
|
818
|
+
const unique = Array.from(new Set(allFiles));
|
|
819
|
+
// Bound the post-rg stat phase: a single hung stat (dead mount /
|
|
820
|
+
// unresponsive network path) must not pin glob until the 600s bridge stall
|
|
821
|
+
// watchdog. Per-stat 5s deadline → a hung entry is treated as stat-failed
|
|
822
|
+
// and dropped, while normal local stats (sub-ms) are unaffected.
|
|
823
|
+
const withStatAll = await statPathsForMtime(unique, workDir, 64, { deadlineMs: 5000 });
|
|
824
|
+
const withStat = withStatAll.filter((entry) => entry?.stat != null);
|
|
825
|
+
withStat.sort((a, b) => {
|
|
826
|
+
const dm = b.mtime - a.mtime;
|
|
827
|
+
if (dm !== 0) return dm;
|
|
828
|
+
return globMtimeTiePath(a).localeCompare(globMtimeTiePath(b));
|
|
829
|
+
});
|
|
830
|
+
const totalBeforeOffset = withStat.length;
|
|
831
|
+
const windowed = offset > 0 ? withStat.slice(offset) : withStat;
|
|
832
|
+
const capped = (headLimit === Infinity ? windowed : windowed.slice(0, headLimit)).map((entry) => {
|
|
833
|
+
const abs = entry.full || resolveAgainstCwd(entry.path, workDir);
|
|
834
|
+
return relativeSearchResultPath(abs, workDir);
|
|
835
|
+
});
|
|
836
|
+
const remaining = windowed.length - capped.length;
|
|
837
|
+
const truncSuffix = accumTruncated
|
|
838
|
+
? '\n... [truncated at accumulation cap (50000)]'
|
|
839
|
+
: (rgStdoutTruncated ? '\n... [truncated at rg stdout cap (20MB); results incomplete]' : '')
|
|
840
|
+
+ (rgStdoutPartial ? '\n... [warning] rg exit 2 (partial results); listing may be incomplete' : '');
|
|
841
|
+
const errSuffix = (rgErrors.length > 0 ? `\n... [warning] ${rgErrors.join(' | ')}` : '') + truncSuffix;
|
|
842
|
+
let emptyDiag = '';
|
|
843
|
+
if (capped.length === 0 && rgErrors.length === 0) {
|
|
844
|
+
const patternStr = patterns.length === 1 ? JSON.stringify(patterns[0]) : JSON.stringify(patterns);
|
|
845
|
+
const baseLabel = basePaths.length === 1 ? normalizeOutputPath(basePaths[0]) : `[${basePaths.map(normalizeOutputPath).join(', ')}]`;
|
|
846
|
+
if (totalBeforeOffset > 0 && offset >= totalBeforeOffset) {
|
|
847
|
+
emptyDiag = `(no entries after offset=${offset}; total=${totalBeforeOffset}) pattern=${patternStr} path=${baseLabel}`;
|
|
848
|
+
} else {
|
|
849
|
+
emptyDiag = `(no files found) pattern=${patternStr} path=${baseLabel}; ${basePathDiagnostic(baseEntries.map((e) => e.root), workDir)}`;
|
|
850
|
+
}
|
|
851
|
+
}
|
|
852
|
+
const body = capped.length > 0
|
|
853
|
+
? `${capped.join('\n')}${remaining > 0 ? `\n... [${remaining} more entries of ${totalBeforeOffset} total — pass offset:${offset + capped.length} to continue]` : ''}${errSuffix}`
|
|
854
|
+
: '';
|
|
855
|
+
const out = body || emptyDiag || '(no files found)';
|
|
856
|
+
if (options?.scopedCacheOutcome && (accumTruncated || rgStdoutTruncated || rgStdoutPartial || remaining > 0)) {
|
|
857
|
+
markScopedCacheIncomplete(options.scopedCacheOutcome);
|
|
858
|
+
}
|
|
859
|
+
const globIncomplete = accumTruncated || rgStdoutTruncated || rgStdoutPartial || remaining > 0;
|
|
860
|
+
if (!globIncomplete) {
|
|
861
|
+
cacheSet(cacheKey, out, { scopes: [...groups.keys()].map((root) => resolvedForSearchRoot(root)) });
|
|
862
|
+
}
|
|
863
|
+
// ② completion progress (claude "Found N" parity). Best-effort, no-op
|
|
864
|
+
// when onProgress is absent (no progressToken).
|
|
865
|
+
if (typeof options?.onProgress === 'function') {
|
|
866
|
+
try { options.onProgress(`found ${totalBeforeOffset} files`); } catch { /* best-effort */ }
|
|
867
|
+
}
|
|
868
|
+
return out;
|
|
869
|
+
}
|