ashlr-plugin 1.36.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.agents/plugins/marketplace.json +20 -0
- package/.claude-plugin/marketplace.json +39 -0
- package/.claude-plugin/plugin.json +36 -0
- package/.codex/agents/README.md +9 -0
- package/.codex/agents/ashlr-explorer.md +36 -0
- package/.codex/agents/ashlr-worker.md +38 -0
- package/.codex-plugin/plugin.json +45 -0
- package/.mcp.json +14 -0
- package/LICENSE +21 -0
- package/README.md +311 -0
- package/agents/ashlr-code.md +69 -0
- package/agents/ashlr-explore.md +44 -0
- package/agents/ashlr-plan.md +54 -0
- package/bench/fixtures/notebook-large.ipynb +660 -0
- package/bench/fixtures/notebook-small.ipynb +39 -0
- package/bench/fixtures/tasklist-closed.json +62 -0
- package/bench/fixtures/tasklist-large-mixed.json +27 -0
- package/bench/fixtures/tasklist-sprint-open.json +79 -0
- package/bench/fixtures/websearch-bun-runtime.json +32 -0
- package/bench/fixtures/websearch-react-hooks-patterns.json +32 -0
- package/bench/fixtures/websearch-typescript-generics.json +38 -0
- package/bench/refs/node-sdk/.refrev +1 -0
- package/bench/refs/node-sdk/README.md +17 -0
- package/bench/refs/node-sdk/src/agent/agent.ts +163 -0
- package/bench/refs/node-sdk/src/agent/create-agent-ui-stream.ts +83 -0
- package/bench/refs/node-sdk/src/agent/tool-loop-agent.ts +238 -0
- package/bench/refs/node-sdk/src/embed/embed-many.ts +409 -0
- package/bench/refs/node-sdk/src/embed/embed-result.ts +50 -0
- package/bench/refs/node-sdk/src/embed/embed.ts +257 -0
- package/bench/refs/node-sdk/src/error/invalid-argument-error.ts +34 -0
- package/bench/refs/node-sdk/src/error/invalid-tool-input-error.ts +33 -0
- package/bench/refs/node-sdk/src/error/no-object-generated-error.ts +70 -0
- package/bench/refs/node-sdk/src/generate-image/generate-image-result.ts +42 -0
- package/bench/refs/node-sdk/src/generate-image/generate-image.ts +362 -0
- package/bench/refs/node-sdk/src/generate-object/generate-object-result.ts +67 -0
- package/bench/refs/node-sdk/src/generate-object/generate-object.ts +501 -0
- package/bench/refs/node-sdk/src/generate-object/inject-json-instruction.ts +30 -0
- package/bench/refs/node-sdk/src/generate-object/output-strategy.ts +415 -0
- package/bench/refs/node-sdk/src/generate-object/repair-text.ts +12 -0
- package/bench/refs/node-sdk/src/generate-object/stream-object.ts +960 -0
- package/bench/refs/node-sdk/src/generate-speech/generate-speech.ts +191 -0
- package/bench/refs/node-sdk/src/generate-text/generate-text.ts +1484 -0
- package/bench/refs/node-sdk/src/generate-text/stream-text.ts +2708 -0
- package/bench/refs/node-sdk/src/generate-text/tool-call.ts +59 -0
- package/bench/refs/node-sdk/src/generate-text/tool-result.ts +36 -0
- package/bench/refs/node-sdk/src/telemetry/create-telemetry-dispatcher.ts +146 -0
- package/bench/refs/node-sdk/src/telemetry/telemetry-registry.ts +15 -0
- package/bench/refs/node-sdk/src/telemetry/telemetry.ts +223 -0
- package/bench/refs/node-sdk/src/util/async-iterable-stream.ts +94 -0
- package/bench/refs/node-sdk/src/util/consume-stream.ts +31 -0
- package/bench/refs/node-sdk/src/util/create-stitchable-stream.ts +112 -0
- package/bench/refs/node-sdk/src/util/download/download.ts +74 -0
- package/bench/refs/node-sdk/src/util/error-handler.ts +1 -0
- package/bench/refs/node-sdk/src/util/fix-json.ts +401 -0
- package/bench/refs/node-sdk/src/util/merge-objects.ts +79 -0
- package/bench/refs/python-lib/.refrev +1 -0
- package/bench/refs/python-lib/README.md +18 -0
- package/bench/refs/python-lib/pandas/core/_numba/executor.py +249 -0
- package/bench/refs/python-lib/pandas/core/_numba/extensions.py +586 -0
- package/bench/refs/python-lib/pandas/core/_numba/kernels/mean_.py +198 -0
- package/bench/refs/python-lib/pandas/core/_numba/kernels/min_max_.py +178 -0
- package/bench/refs/python-lib/pandas/core/_numba/kernels/sum_.py +255 -0
- package/bench/refs/python-lib/pandas/core/_numba/kernels/var_.py +251 -0
- package/bench/refs/python-lib/pandas/core/accessor.py +520 -0
- package/bench/refs/python-lib/pandas/core/api.py +138 -0
- package/bench/refs/python-lib/pandas/core/array_algos/masked_accumulations.py +97 -0
- package/bench/refs/python-lib/pandas/core/array_algos/masked_reductions.py +252 -0
- package/bench/refs/python-lib/pandas/core/array_algos/putmask.py +150 -0
- package/bench/refs/python-lib/pandas/core/array_algos/quantile.py +232 -0
- package/bench/refs/python-lib/pandas/core/array_algos/replace.py +159 -0
- package/bench/refs/python-lib/pandas/core/array_algos/take.py +609 -0
- package/bench/refs/python-lib/pandas/core/arraylike.py +534 -0
- package/bench/refs/python-lib/pandas/core/arrays/_arrow_string_mixins.py +434 -0
- package/bench/refs/python-lib/pandas/core/arrays/_mixins.py +630 -0
- package/bench/refs/python-lib/pandas/core/arrays/_ranges.py +276 -0
- package/bench/refs/python-lib/pandas/core/arrays/_utils.py +77 -0
- package/bench/refs/python-lib/pandas/core/arrays/arrow/accessors.py +521 -0
- package/bench/refs/python-lib/pandas/core/arrays/arrow/extension_types.py +174 -0
- package/bench/refs/python-lib/pandas/core/arrays/boolean.py +454 -0
- package/bench/refs/python-lib/pandas/core/arrays/floating.py +192 -0
- package/bench/refs/python-lib/pandas/core/arrays/integer.py +296 -0
- package/bench/refs/python-lib/pandas/core/arrays/masked.py +2140 -0
- package/bench/refs/python-lib/pandas/core/arrays/numeric.py +305 -0
- package/bench/refs/python-lib/pandas/core/arrays/numpy_.py +701 -0
- package/bench/refs/python-lib/pandas/core/arrays/period.py +1586 -0
- package/bench/refs/python-lib/pandas/core/arrays/sparse/accessor.py +505 -0
- package/bench/refs/python-lib/pandas/core/arrays/sparse/scipy_sparse.py +208 -0
- package/bench/refs/rust-project/.refrev +1 -0
- package/bench/refs/rust-project/README.md +17 -0
- package/bench/refs/rust-project/tokio/src/doc/os.rs +68 -0
- package/bench/refs/rust-project/tokio/src/fs/dir_builder.rs +129 -0
- package/bench/refs/rust-project/tokio/src/fs/file.rs +1113 -0
- package/bench/refs/rust-project/tokio/src/fs/mocks.rs +176 -0
- package/bench/refs/rust-project/tokio/src/fs/mod.rs +324 -0
- package/bench/refs/rust-project/tokio/src/fs/open_options.rs +856 -0
- package/bench/refs/rust-project/tokio/src/fs/read.rs +80 -0
- package/bench/refs/rust-project/tokio/src/fs/read_dir.rs +357 -0
- package/bench/refs/rust-project/tokio/src/fs/write.rs +99 -0
- package/bench/refs/rust-project/tokio/src/future/maybe_done.rs +123 -0
- package/bench/refs/rust-project/tokio/src/future/try_join.rs +82 -0
- package/bench/refs/rust-project/tokio/src/io/async_buf_read.rs +117 -0
- package/bench/refs/rust-project/tokio/src/io/async_read.rs +133 -0
- package/bench/refs/rust-project/tokio/src/io/async_seek.rs +96 -0
- package/bench/refs/rust-project/tokio/src/io/async_write.rs +402 -0
- package/bench/refs/rust-project/tokio/src/io/blocking.rs +339 -0
- package/bench/refs/rust-project/tokio/src/io/interest.rs +345 -0
- package/bench/refs/rust-project/tokio/src/io/join.rs +130 -0
- package/bench/refs/rust-project/tokio/src/io/mod.rs +308 -0
- package/bench/refs/rust-project/tokio/src/io/poll_evented.rs +319 -0
- package/bench/refs/rust-project/tokio/src/io/read_buf.rs +344 -0
- package/bench/refs/rust-project/tokio/src/io/ready.rs +322 -0
- package/bench/refs/rust-project/tokio/src/io/split.rs +166 -0
- package/bench/refs/rust-project/tokio/src/io/stderr.rs +148 -0
- package/bench/refs/rust-project/tokio/src/io/stdin.rs +98 -0
- package/bench/refs/rust-project/tokio/src/io/stdio_common.rs +222 -0
- package/bench/refs/rust-project/tokio/src/io/stdout.rs +197 -0
- package/bench/refs/rust-project/tokio/src/io/util/buf_reader.rs +311 -0
- package/bench/refs/rust-project/tokio/src/io/util/buf_stream.rs +207 -0
- package/bench/refs/rust-project/tokio/src/io/util/buf_writer.rs +310 -0
- package/bench/refs/rust-project/tokio/src/io/util/chain.rs +144 -0
- package/commands/_spawn-patterns.json +32 -0
- package/commands/ashlr-allow.md +21 -0
- package/commands/ashlr-benchmark.md +53 -0
- package/commands/ashlr-brief.md +97 -0
- package/commands/ashlr-budget.md +76 -0
- package/commands/ashlr-compact.md +54 -0
- package/commands/ashlr-dashboard.md +37 -0
- package/commands/ashlr-doctor.md +56 -0
- package/commands/ashlr-eco-mode.md +92 -0
- package/commands/ashlr-genome-init.md +24 -0
- package/commands/ashlr-genome-keygen.md +36 -0
- package/commands/ashlr-genome-loop.md +118 -0
- package/commands/ashlr-genome-push.md +36 -0
- package/commands/ashlr-genome-rewrap.md +48 -0
- package/commands/ashlr-genome-team-init.md +67 -0
- package/commands/ashlr-handoff.md +23 -0
- package/commands/ashlr-help.md +86 -0
- package/commands/ashlr-hook-timings.md +30 -0
- package/commands/ashlr-leaderboard.md +50 -0
- package/commands/ashlr-ollama-setup.md +39 -0
- package/commands/ashlr-orchestrate-status.md +72 -0
- package/commands/ashlr-orchestrate.md +83 -0
- package/commands/ashlr-parallelize.md +63 -0
- package/commands/ashlr-report-crash.md +33 -0
- package/commands/ashlr-resume.md +72 -0
- package/commands/ashlr-savings.md +24 -0
- package/commands/ashlr-settings.md +50 -0
- package/commands/ashlr-spawn.md +61 -0
- package/commands/ashlr-start.md +60 -0
- package/commands/ashlr-status.md +64 -0
- package/commands/ashlr-team-invite.md +47 -0
- package/commands/ashlr-tier.md +122 -0
- package/commands/ashlr-tour.md +60 -0
- package/commands/ashlr-update.md +155 -0
- package/commands/ashlr-upgrade.md +96 -0
- package/commands/goal.md +58 -0
- package/commands/loop.md +40 -0
- package/docs/CNAME +1 -0
- package/docs/PRO_TIER.md +104 -0
- package/docs/README.md +21 -0
- package/docs/architecture.md +415 -0
- package/docs/ashlr-pipe.md +161 -0
- package/docs/assets/logo.svg +19 -0
- package/docs/assets/og.png +0 -0
- package/docs/assets/og.svg +41 -0
- package/docs/benchmarks-v2.json +355 -0
- package/docs/benchmarks.json +102 -0
- package/docs/benchmarks.md +211 -0
- package/docs/billing.md +185 -0
- package/docs/cloud-genome.md +155 -0
- package/docs/competitive-wozcode.md +92 -0
- package/docs/deploy.md +325 -0
- package/docs/email-templates.md +113 -0
- package/docs/embed-tuning.md +103 -0
- package/docs/genome.md +120 -0
- package/docs/github-oauth-onboarding.md +127 -0
- package/docs/hero-video-script.md +129 -0
- package/docs/index.html +2262 -0
- package/docs/install-prompt.md +78 -0
- package/docs/install-windows.md +132 -0
- package/docs/install.ps1 +171 -0
- package/docs/install.sh +146 -0
- package/docs/launch/README.md +114 -0
- package/docs/launch/hn.md +49 -0
- package/docs/launch/reddit.md +45 -0
- package/docs/launch/x.md +65 -0
- package/docs/launch-post.md +112 -0
- package/docs/legal.md +78 -0
- package/docs/llm-providers.md +99 -0
- package/docs/multi-host-mcp.md +217 -0
- package/docs/multi-turn-architecture.md +108 -0
- package/docs/operations/deployment.md +108 -0
- package/docs/operations/error-monitoring.md +70 -0
- package/docs/operations/weekly-digest.md +70 -0
- package/docs/operations.md +494 -0
- package/docs/platform-support.md +133 -0
- package/docs/policy-packs.md +188 -0
- package/docs/press-kit/fact-sheet.md +78 -0
- package/docs/press-kit/founder-bio.md +3 -0
- package/docs/press-kit/logo.svg +33 -0
- package/docs/press-kit/screenshots.md +146 -0
- package/docs/pricing.md +251 -0
- package/docs/pro-backend-architecture.md +523 -0
- package/docs/producthunt.md +52 -0
- package/docs/router-migration.md +110 -0
- package/docs/session-log-schema.md +68 -0
- package/docs/social.md +95 -0
- package/docs/team-genome.md +528 -0
- package/docs/telemetry.md +194 -0
- package/docs/test-isolation.md +100 -0
- package/docs/upgrade.md +159 -0
- package/hooks/_hook-errors.ts +87 -0
- package/hooks/_hook-perf-emit.ts +113 -0
- package/hooks/_hook-timing-ledger.ts +1133 -0
- package/hooks/_nudge-throttle.ts +127 -0
- package/hooks/_recent-blocks.ts +127 -0
- package/hooks/audit-upload.ts +86 -0
- package/hooks/codex-hooks.json +171 -0
- package/hooks/commit-attribution.ts +179 -0
- package/hooks/edit-batching-nudge.ts +154 -0
- package/hooks/genome-scribe-hook.ts +219 -0
- package/hooks/hooks.json +294 -0
- package/hooks/orient-nudge-hook.ts +242 -0
- package/hooks/policy-enforce.ts +214 -0
- package/hooks/post-tool-use-embedding.ts +93 -0
- package/hooks/post-tool-use-genome.sh +37 -0
- package/hooks/post-tool-use-genome.ts +69 -0
- package/hooks/posttooluse-correlate.ts +122 -0
- package/hooks/posttooluse-dedupe-output.ts +241 -0
- package/hooks/posttooluse-genome-refresh.ts +223 -0
- package/hooks/posttooluse-native-nudge.ts +205 -0
- package/hooks/posttooluse-stale-result.ts +277 -0
- package/hooks/pretooluse-bash.ts +63 -0
- package/hooks/pretooluse-budget-guard.ts +318 -0
- package/hooks/pretooluse-common.ts +985 -0
- package/hooks/pretooluse-eco-router.ts +196 -0
- package/hooks/pretooluse-edit.sh +93 -0
- package/hooks/pretooluse-edit.ts +197 -0
- package/hooks/pretooluse-glob.ts +70 -0
- package/hooks/pretooluse-grep.sh +85 -0
- package/hooks/pretooluse-grep.ts +96 -0
- package/hooks/pretooluse-notebookedit.ts +121 -0
- package/hooks/pretooluse-read.sh +118 -0
- package/hooks/pretooluse-read.ts +118 -0
- package/hooks/pretooluse-task.ts +130 -0
- package/hooks/pretooluse-webfetch.ts +66 -0
- package/hooks/pretooluse-websearch.ts +85 -0
- package/hooks/pretooluse-write.ts +128 -0
- package/hooks/pulse-emit.ts +197 -0
- package/hooks/session-end-consolidate.sh +35 -0
- package/hooks/session-end-consolidate.ts +188 -0
- package/hooks/session-end-stats-gc.ts +80 -0
- package/hooks/session-log-append.sh +94 -0
- package/hooks/session-log-append.ts +116 -0
- package/hooks/session-start.ts +993 -0
- package/hooks/sessionend-hook-health-nudge.ts +451 -0
- package/hooks/sessionend-savings-nudge.ts +318 -0
- package/hooks/sessionend-status-line-nudge.ts +296 -0
- package/hooks/sessionstart-brief.ts +140 -0
- package/hooks/sessionstart-cost-refactor.ts +95 -0
- package/hooks/sessionstart-efficient.ts +110 -0
- package/hooks/sessionstart-genome-author.ts +93 -0
- package/hooks/sessionstart-lean-tools.ts +98 -0
- package/hooks/sessionstart-search.ts +97 -0
- package/hooks/stop-accounting.ts +152 -0
- package/hooks/subagent-stop-rollup.ts +162 -0
- package/hooks/userpromptsubmit-brief-trigger.ts +156 -0
- package/package.json +56 -0
- package/ports/README.md +108 -0
- package/ports/cursor/mcp.json +11 -0
- package/ports/goose/recipe.yaml +26 -0
- package/scripts/ashlr-mcp.ts +40 -0
- package/scripts/auto-update.ts +177 -0
- package/scripts/baseline-scan.ts +831 -0
- package/scripts/bench-orchestrate.ts +377 -0
- package/scripts/benchmark-refs.ts +292 -0
- package/scripts/bootstrap.mjs +100 -0
- package/scripts/brief-eval.ts +234 -0
- package/scripts/bump-version.ts +82 -0
- package/scripts/bun-resolve.d.mts +11 -0
- package/scripts/bun-resolve.mjs +59 -0
- package/scripts/calibrate-grep.ts +467 -0
- package/scripts/capture-grep-workload.ts +260 -0
- package/scripts/check-tier.ts +61 -0
- package/scripts/check-version-sync.ts +97 -0
- package/scripts/cli-orchestrate-status.ts +386 -0
- package/scripts/cli-orchestrate.ts +303 -0
- package/scripts/cli.ts +478 -0
- package/scripts/cloud-smoke-test.ts +313 -0
- package/scripts/coach-report.ts +373 -0
- package/scripts/context-db-migrate.ts +137 -0
- package/scripts/context-status.ts +43 -0
- package/scripts/demo-run.ts +255 -0
- package/scripts/deploy-smoke.sh +140 -0
- package/scripts/doctor.ts +992 -0
- package/scripts/embed-file-worker.ts +104 -0
- package/scripts/embed-tune.ts +308 -0
- package/scripts/errors-report.ts +428 -0
- package/scripts/find-test-leak.ts +69 -0
- package/scripts/generate-badge.ts +357 -0
- package/scripts/genome-auto-consolidate.ts +533 -0
- package/scripts/genome-auto-propose.ts +578 -0
- package/scripts/genome-cloud-pull.ts +363 -0
- package/scripts/genome-cloud-push.ts +382 -0
- package/scripts/genome-cloud-sync.ts +587 -0
- package/scripts/genome-commit-watcher.ts +295 -0
- package/scripts/genome-init.ts +986 -0
- package/scripts/genome-key.ts +233 -0
- package/scripts/genome-keygen.ts +168 -0
- package/scripts/genome-link.ts +59 -0
- package/scripts/genome-refresh-worker.ts +364 -0
- package/scripts/genome-rewrap.ts +90 -0
- package/scripts/genome-synthesizer.ts +679 -0
- package/scripts/genome-team-init.ts +312 -0
- package/scripts/handoff-pack.ts +438 -0
- package/scripts/hook-bootstrap.mjs +75 -0
- package/scripts/hook-timing-reader.ts +371 -0
- package/scripts/hook-timings-report.ts +554 -0
- package/scripts/install-genome-hooks.ts +196 -0
- package/scripts/install-onnx-model.ts +154 -0
- package/scripts/install-permissions.ts +276 -0
- package/scripts/install-status-line.ts +150 -0
- package/scripts/mcp-entrypoint.sh +78 -0
- package/scripts/mcp-entrypoint.ts +130 -0
- package/scripts/mdx-angle-bracket-lint.ts +152 -0
- package/scripts/measure-cold-start.ts +126 -0
- package/scripts/migrate-stats-to-sqlite.ts +86 -0
- package/scripts/ollama-setup.ts +498 -0
- package/scripts/onboarding-wizard.ts +1691 -0
- package/scripts/orchestrate-emit-telemetry.ts +177 -0
- package/scripts/orchestrate-expand.ts +517 -0
- package/scripts/orchestrate-run.ts +744 -0
- package/scripts/print-legend.ts +50 -0
- package/scripts/product-metadata.ts +38 -0
- package/scripts/prune-old-memories.ts +208 -0
- package/scripts/publish.sh +88 -0
- package/scripts/read-calibration.ts +122 -0
- package/scripts/report-crash.ts +226 -0
- package/scripts/run-benchmark.ts +750 -0
- package/scripts/run-root-tests.ts +28 -0
- package/scripts/savings-dashboard.ts +1258 -0
- package/scripts/savings-report-extras.ts +447 -0
- package/scripts/savings-status-line.ts +873 -0
- package/scripts/session-event-emit.ts +266 -0
- package/scripts/session-greet.ts +686 -0
- package/scripts/session-log-report.ts +551 -0
- package/scripts/session-resume.ts +614 -0
- package/scripts/set-hook-mode.ts +83 -0
- package/scripts/smoke-cross-platform.ts +117 -0
- package/scripts/smoke-realtime.ts +257 -0
- package/scripts/smoke-tools.ts +21 -0
- package/scripts/stats-cloud-pull.ts +213 -0
- package/scripts/stats-cloud-sync.ts +261 -0
- package/scripts/telemetry-flush.ts +160 -0
- package/scripts/telemetry-status.ts +266 -0
- package/scripts/timing-budget.ts +35 -0
- package/scripts/ui-animation.ts +120 -0
- package/scripts/ui.ts +431 -0
- package/scripts/upgrade-flow.ts +775 -0
- package/scripts/weekly-digest.ts +394 -0
- package/servers/_accounting.ts +78 -0
- package/servers/_ask-router.ts +84 -0
- package/servers/_ast-chunker.ts +434 -0
- package/servers/_ast-helpers.ts +183 -0
- package/servers/_ast-languages.ts +153 -0
- package/servers/_ast-refactor/_shared.ts +221 -0
- package/servers/_ast-refactor/cross-file-rename.ts +560 -0
- package/servers/_ast-refactor/extract-function.ts +357 -0
- package/servers/_ast-refactor/file-local-rename.ts +172 -0
- package/servers/_ast-refactor.ts +41 -0
- package/servers/_ast-skeleton.ts +107 -0
- package/servers/_bash-summarizers-registry.ts +1096 -0
- package/servers/_crash-dump.ts +210 -0
- package/servers/_cwd-clamp.ts +342 -0
- package/servers/_edit-log.ts +28 -0
- package/servers/_edit-match.ts +236 -0
- package/servers/_edit-validate.ts +196 -0
- package/servers/_embed-calibration.ts +123 -0
- package/servers/_embedding-cache.ts +401 -0
- package/servers/_embedding-model.ts +329 -0
- package/servers/_events.ts +113 -0
- package/servers/_genome-cache.ts +154 -0
- package/servers/_genome-commits.ts +172 -0
- package/servers/_genome-crypto-v2.ts +242 -0
- package/servers/_genome-crypto.ts +259 -0
- package/servers/_genome-discoveries.ts +140 -0
- package/servers/_genome-embed-populator.ts +216 -0
- package/servers/_genome-freshness.ts +134 -0
- package/servers/_genome-issues.ts +171 -0
- package/servers/_genome-live.ts +263 -0
- package/servers/_genome-prs.ts +188 -0
- package/servers/_genome-search.ts +479 -0
- package/servers/_genome-sync.ts +297 -0
- package/servers/_history-tracker.ts +279 -0
- package/servers/_http-helpers.ts +151 -0
- package/servers/_identity-hash.ts +151 -0
- package/servers/_llm-providers/anthropic.ts +132 -0
- package/servers/_llm-providers/cloud.ts +195 -0
- package/servers/_llm-providers/index.ts +350 -0
- package/servers/_llm-providers/local.ts +127 -0
- package/servers/_llm-providers/onnx.ts +393 -0
- package/servers/_llm-providers/types.ts +27 -0
- package/servers/_manifest-v2.ts +447 -0
- package/servers/_mcp-host.ts +93 -0
- package/servers/_nudge-events.ts +451 -0
- package/servers/_orchestrate-executor.ts +316 -0
- package/servers/_prefetch.ts +435 -0
- package/servers/_pricing.ts +256 -0
- package/servers/_pro.ts +381 -0
- package/servers/_read-cache.ts +44 -0
- package/servers/_router-handlers.ts +46 -0
- package/servers/_router.ts +55 -0
- package/servers/_run-with-timeout.ts +173 -0
- package/servers/_savings-render.ts +214 -0
- package/servers/_stats-sqlite.ts +640 -0
- package/servers/_stats.ts +1183 -0
- package/servers/_status-line-cells.ts +469 -0
- package/servers/_streaks.ts +162 -0
- package/servers/_summarize.ts +208 -0
- package/servers/_task-graph-render.ts +217 -0
- package/servers/_task-graph.ts +545 -0
- package/servers/_telemetry.ts +719 -0
- package/servers/_test-parsers.ts +530 -0
- package/servers/_test-watch.ts +626 -0
- package/servers/_text-helpers.ts +37 -0
- package/servers/_token-measure.ts +160 -0
- package/servers/_tool-base.ts +248 -0
- package/servers/_weekly-digest.ts +391 -0
- package/servers/ask-server-handlers.ts +49 -0
- package/servers/ask-server.ts +174 -0
- package/servers/bash-server-handlers.ts +138 -0
- package/servers/bash-server.ts +1136 -0
- package/servers/bench.ts +186 -0
- package/servers/diff-semantic-server-handlers.ts +39 -0
- package/servers/diff-semantic-server.ts +566 -0
- package/servers/diff-server-handlers.ts +317 -0
- package/servers/diff-server.ts +20 -0
- package/servers/edit-server-handlers.ts +31 -0
- package/servers/edit-server.ts +209 -0
- package/servers/edit-structural-server-handlers.ts +322 -0
- package/servers/efficiency-server-handlers.ts +161 -0
- package/servers/efficiency-server.ts +1135 -0
- package/servers/flush-server-handlers.ts +21 -0
- package/servers/flush-server.ts +19 -0
- package/servers/genome-server-handlers.ts +118 -0
- package/servers/genome-server.ts +397 -0
- package/servers/github-server-handlers.ts +187 -0
- package/servers/github-server.ts +856 -0
- package/servers/glob-server-handlers.ts +233 -0
- package/servers/glob-server.ts +20 -0
- package/servers/grep-server-handlers.ts +61 -0
- package/servers/grep-server.ts +564 -0
- package/servers/http-server-handlers.ts +36 -0
- package/servers/http-server.ts +149 -0
- package/servers/logs-server-handlers.ts +42 -0
- package/servers/logs-server.ts +359 -0
- package/servers/ls-server-handlers.ts +166 -0
- package/servers/ls-server.ts +20 -0
- package/servers/multi-edit-server-handlers.ts +51 -0
- package/servers/multi-edit-server.ts +422 -0
- package/servers/notebook-edit-server-handlers.ts +34 -0
- package/servers/notebook-edit-server.ts +181 -0
- package/servers/orient-server-handlers.ts +52 -0
- package/servers/orient-server.ts +436 -0
- package/servers/pipe-server-handlers.ts +68 -0
- package/servers/pipe-server.ts +300 -0
- package/servers/read-server-handlers.ts +42 -0
- package/servers/read-server.ts +206 -0
- package/servers/rename-file-server-handlers.ts +67 -0
- package/servers/rename-file-server.ts +699 -0
- package/servers/savings-server-handlers.ts +27 -0
- package/servers/savings-server.ts +54 -0
- package/servers/search-replace-regex-server-handlers.ts +96 -0
- package/servers/search-replace-regex-server.ts +751 -0
- package/servers/sql-server-handlers.ts +58 -0
- package/servers/sql-server.ts +505 -0
- package/servers/task-server-handlers.ts +358 -0
- package/servers/task-server.ts +24 -0
- package/servers/test-server-handlers.ts +351 -0
- package/servers/test-server.ts +20 -0
- package/servers/tree-server-handlers.ts +503 -0
- package/servers/tree-server.ts +20 -0
- package/servers/webfetch-server-handlers.ts +166 -0
- package/servers/webfetch-server.ts +20 -0
- package/servers/websearch-server-handlers.ts +215 -0
- package/servers/websearch-server.ts +19 -0
- package/servers/write-server-handlers.ts +27 -0
- package/servers/write-server.ts +97 -0
- package/skills/ashlr-brief/SKILL.md +80 -0
- package/skills/ashlr-codex-doctor/SKILL.md +21 -0
- package/skills/ashlr-codex-efficient-tools/SKILL.md +17 -0
- package/skills/ashlr-codex-genome-refresh/SKILL.md +20 -0
- package/skills/ashlr-codex-parallel-explore/SKILL.md +20 -0
- package/skills/ashlr-codex-savings/SKILL.md +17 -0
- package/skills/ashlr-cost-refactor/SKILL.md +50 -0
- package/skills/ashlr-efficient/SKILL.md +64 -0
- package/skills/ashlr-genome-author/SKILL.md +43 -0
- package/skills/ashlr-lean-tools/SKILL.md +43 -0
- package/skills/ashlr-search/SKILL.md +46 -0
|
@@ -0,0 +1,751 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ashlr-search-replace-regex — v1.20 multi-file regex search/replace.
|
|
3
|
+
*
|
|
4
|
+
* Companion to `ashlr__edit` (literal-only, single file) and
|
|
5
|
+
* `ashlr__rename_file` (file rename + importer update). Users frequently
|
|
6
|
+
* need multi-file regex-based search/replace — e.g. "change every
|
|
7
|
+
* `logger.info(` to `log.info(` across src/". Before this tool they had
|
|
8
|
+
* to fall back to native shell + sed / native MultiEdit spanning many
|
|
9
|
+
* files; this lands the pattern inside the ashlr router so savings are
|
|
10
|
+
* credited and safety rails (cwd-clamp, binary refusal, caps) apply.
|
|
11
|
+
*
|
|
12
|
+
* Scope rules:
|
|
13
|
+
* - `roots` default to the primary project root and are cwd-clamped.
|
|
14
|
+
* Paths that escape cwd → refusal.
|
|
15
|
+
* - Candidate discovery uses ripgrep (`rg -l`) with the compiled regex.
|
|
16
|
+
* `include` / `exclude` globs are forwarded as `--glob` / `--glob !`
|
|
17
|
+
* — same syntax as `ashlr__edit_structural`.
|
|
18
|
+
* - Candidates are capped at `maxFiles` (default 200). Overflow is
|
|
19
|
+
* reported as a non-fatal warning, not a refusal.
|
|
20
|
+
* - Per-file replacements are capped at `maxMatchesPerFile` (default
|
|
21
|
+
* 100). Extras are left untouched and flagged.
|
|
22
|
+
* - Binary files are skipped by extension AND by null-byte sniff on
|
|
23
|
+
* the first 512 bytes (defense in depth against mislabeled assets).
|
|
24
|
+
*
|
|
25
|
+
* Safety:
|
|
26
|
+
* - Zero-width / empty-match patterns (e.g. `.*` on an empty string)
|
|
27
|
+
* are refused at validate time. The tool requires that every match
|
|
28
|
+
* consume at least one character, so a runaway replaceAll can't
|
|
29
|
+
* produce an infinite-length file.
|
|
30
|
+
* - Writes are atomic per-file (temp file + rename) so a crash
|
|
31
|
+
* mid-write can't corrupt partial content — the reader either sees
|
|
32
|
+
* the old file or the new file, never a half-written one.
|
|
33
|
+
* - dryRun: true returns the planned change set (file list + counts)
|
|
34
|
+
* without touching disk.
|
|
35
|
+
*
|
|
36
|
+
* No `--force` mode for v1. The regex + include/exclude + caps are
|
|
37
|
+
* expressive enough to scope any reasonable rewrite; if the model wants
|
|
38
|
+
* a truly global rewrite it can set `roots=["."]` explicitly and own
|
|
39
|
+
* the blast radius.
|
|
40
|
+
*/
|
|
41
|
+
|
|
42
|
+
import { readFile, rename, writeFile, stat } from "fs/promises";
|
|
43
|
+
import { accessSync, realpathSync, statSync } from "fs";
|
|
44
|
+
import { spawnSync } from "child_process";
|
|
45
|
+
import { extname, normalize as pathNormalize, relative, resolve as pathResolve, sep } from "path";
|
|
46
|
+
import { minimatch } from "minimatch";
|
|
47
|
+
import { clampToCwd, primaryProjectRoot } from "./_cwd-clamp";
|
|
48
|
+
|
|
49
|
+
// ---------------------------------------------------------------------------
|
|
50
|
+
// Types
|
|
51
|
+
// ---------------------------------------------------------------------------
|
|
52
|
+
|
|
53
|
+
export interface SearchReplaceRegexArgs {
|
|
54
|
+
pattern: string;
|
|
55
|
+
replacement: string;
|
|
56
|
+
flags?: string;
|
|
57
|
+
include?: string[];
|
|
58
|
+
exclude?: string[];
|
|
59
|
+
dryRun?: boolean;
|
|
60
|
+
maxFiles?: number;
|
|
61
|
+
maxMatchesPerFile?: number;
|
|
62
|
+
roots?: string[];
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export interface FilePlan {
|
|
66
|
+
/** Absolute path of the file. */
|
|
67
|
+
path: string;
|
|
68
|
+
/** Number of matches that will be replaced in this file. */
|
|
69
|
+
matches: number;
|
|
70
|
+
/** True if matches were capped by `maxMatchesPerFile`. */
|
|
71
|
+
capped: boolean;
|
|
72
|
+
/** Byte size of the original file. */
|
|
73
|
+
originalBytes: number;
|
|
74
|
+
/** Byte size of the rewritten file (post-replacement). */
|
|
75
|
+
newBytes: number;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export interface SearchReplaceRegexPlan {
|
|
79
|
+
/** Per-file planned changes. Only files with ≥1 match are included. */
|
|
80
|
+
files: FilePlan[];
|
|
81
|
+
/** Candidate files rg surfaced but that were skipped (binary, read error, etc.). */
|
|
82
|
+
skipped: number;
|
|
83
|
+
/** Non-fatal warnings (maxFiles truncation, per-file cap hit, binary skip, …). */
|
|
84
|
+
warnings: string[];
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export type SearchReplaceRegexResult =
|
|
88
|
+
| { ok: true; plan: SearchReplaceRegexPlan }
|
|
89
|
+
| { ok: false; reason: string };
|
|
90
|
+
|
|
91
|
+
// ---------------------------------------------------------------------------
|
|
92
|
+
// Constants
|
|
93
|
+
// ---------------------------------------------------------------------------
|
|
94
|
+
|
|
95
|
+
const DEFAULT_MAX_FILES = 200;
|
|
96
|
+
const DEFAULT_MAX_MATCHES_PER_FILE = 100;
|
|
97
|
+
const MAX_FILE_BYTES = 50 * 1024 * 1024;
|
|
98
|
+
|
|
99
|
+
/** Binary extensions we flat-out skip (no regex rewrite makes sense). */
|
|
100
|
+
const BINARY_EXTS = new Set([
|
|
101
|
+
".png", ".jpg", ".jpeg", ".gif", ".webp", ".avif", ".ico", ".bmp", ".tiff",
|
|
102
|
+
".pdf", ".wasm", ".woff", ".woff2", ".ttf", ".otf", ".eot",
|
|
103
|
+
".mp3", ".mp4", ".mov", ".avi", ".webm", ".ogg", ".wav", ".flac",
|
|
104
|
+
".zip", ".tar", ".gz", ".bz2", ".7z", ".rar",
|
|
105
|
+
".so", ".dylib", ".dll", ".exe", ".class",
|
|
106
|
+
".db", ".sqlite", ".sqlite3",
|
|
107
|
+
]);
|
|
108
|
+
|
|
109
|
+
/** Cap per-file scanning at this byte count for the null-byte sniff. */
|
|
110
|
+
const SNIFF_BYTES = 512;
|
|
111
|
+
|
|
112
|
+
// ---------------------------------------------------------------------------
|
|
113
|
+
// rg resolver (duplicated from rename-file-server; see note there)
|
|
114
|
+
// ---------------------------------------------------------------------------
|
|
115
|
+
|
|
116
|
+
function resolveRg(): string {
|
|
117
|
+
const candidates = [
|
|
118
|
+
"/opt/homebrew/bin/rg",
|
|
119
|
+
"/usr/local/bin/rg",
|
|
120
|
+
"/usr/bin/rg",
|
|
121
|
+
"/opt/homebrew/lib/node_modules/@openai/codex/node_modules/@openai/codex-darwin-arm64/vendor/aarch64-apple-darwin/path/rg",
|
|
122
|
+
"/opt/homebrew/lib/node_modules/@openai/codex/bin/rg",
|
|
123
|
+
];
|
|
124
|
+
const bun = (globalThis as { Bun?: { which(bin: string): string | null } }).Bun;
|
|
125
|
+
const fromBun = bun ? bun.which("rg") : null;
|
|
126
|
+
if (fromBun) return fromBun;
|
|
127
|
+
for (const p of candidates) {
|
|
128
|
+
try {
|
|
129
|
+
accessSync(p);
|
|
130
|
+
return p;
|
|
131
|
+
} catch {
|
|
132
|
+
// keep trying
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
return "rg";
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// ---------------------------------------------------------------------------
|
|
139
|
+
// Regex validation
|
|
140
|
+
// ---------------------------------------------------------------------------
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Compile the user-supplied pattern + flags into a global RegExp and
|
|
144
|
+
* verify it can't match the empty string (which would cause a runaway
|
|
145
|
+
* replaceAll / infinite-length output). We always force the `g` flag on
|
|
146
|
+
* — cross-file replace only makes sense globally.
|
|
147
|
+
*/
|
|
148
|
+
function compilePattern(
|
|
149
|
+
pattern: string,
|
|
150
|
+
flags: string | undefined,
|
|
151
|
+
): { ok: true; re: RegExp } | { ok: false; reason: string } {
|
|
152
|
+
if (!pattern || typeof pattern !== "string") {
|
|
153
|
+
return { ok: false, reason: "'pattern' is required and must be a string" };
|
|
154
|
+
}
|
|
155
|
+
if (pattern.length === 0) {
|
|
156
|
+
return { ok: false, reason: "'pattern' must not be empty" };
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// Normalize flags. Strip any caller-supplied `g` (we re-add it) and
|
|
160
|
+
// whitelist the subset we support — `i`, `m`, `s`, `u`. `y` (sticky)
|
|
161
|
+
// is refused because it breaks String.replaceAll's global-replace
|
|
162
|
+
// contract, and `d` has no effect on replacement.
|
|
163
|
+
const rawFlags = (flags ?? "").replace(/g/g, "");
|
|
164
|
+
for (const f of rawFlags) {
|
|
165
|
+
if (!"imsu".includes(f)) {
|
|
166
|
+
return { ok: false, reason: `unsupported flag '${f}' — only i, m, s, u are supported (g is implicit)` };
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
const finalFlags = "g" + rawFlags;
|
|
170
|
+
|
|
171
|
+
let re: RegExp;
|
|
172
|
+
try {
|
|
173
|
+
re = new RegExp(pattern, finalFlags);
|
|
174
|
+
} catch (err) {
|
|
175
|
+
return { ok: false, reason: `invalid regex: ${(err as Error).message}` };
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// Reject zero-width / empty-match patterns. We probe by running the
|
|
179
|
+
// regex against a single-char string — if `.exec` produces a match of
|
|
180
|
+
// length zero, the pattern accepts the empty string and would loop
|
|
181
|
+
// infinitely against any input. Reset lastIndex after probing.
|
|
182
|
+
const probe = " ";
|
|
183
|
+
re.lastIndex = 0;
|
|
184
|
+
const m = re.exec(probe);
|
|
185
|
+
if (m && m[0].length === 0) {
|
|
186
|
+
return { ok: false, reason: "pattern matches the empty string (zero-width match would cause runaway replacement)" };
|
|
187
|
+
}
|
|
188
|
+
// Second probe: empty string directly. Some patterns (e.g. `^`) only
|
|
189
|
+
// fire against empty input.
|
|
190
|
+
re.lastIndex = 0;
|
|
191
|
+
const m2 = re.exec("");
|
|
192
|
+
if (m2 && m2[0].length === 0) {
|
|
193
|
+
return { ok: false, reason: "pattern matches the empty string (zero-width match would cause runaway replacement)" };
|
|
194
|
+
}
|
|
195
|
+
re.lastIndex = 0;
|
|
196
|
+
return { ok: true, re };
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
// ---------------------------------------------------------------------------
|
|
200
|
+
// Binary detection
|
|
201
|
+
// ---------------------------------------------------------------------------
|
|
202
|
+
|
|
203
|
+
function isBinaryByExt(path: string): boolean {
|
|
204
|
+
return BINARY_EXTS.has(extname(path).toLowerCase());
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* Read the first `SNIFF_BYTES` of `path` and check for NUL bytes. If a
|
|
209
|
+
* file contains \x00 in its header, we treat it as binary regardless of
|
|
210
|
+
* extension.
|
|
211
|
+
*/
|
|
212
|
+
function looksBinary(buf: Buffer): boolean {
|
|
213
|
+
const len = Math.min(buf.length, SNIFF_BYTES);
|
|
214
|
+
for (let i = 0; i < len; i++) {
|
|
215
|
+
if (buf[i] === 0) return true;
|
|
216
|
+
}
|
|
217
|
+
return false;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
// ---------------------------------------------------------------------------
|
|
221
|
+
// Candidate discovery via ripgrep
|
|
222
|
+
// ---------------------------------------------------------------------------
|
|
223
|
+
|
|
224
|
+
/**
|
|
225
|
+
* Enumerate files under `searchRoots` that contain at least one match
|
|
226
|
+
* for `pattern`. Honors include/exclude globs. Caps the result set at
|
|
227
|
+
* `cap` and reports truncation via the returned flag.
|
|
228
|
+
*
|
|
229
|
+
* We hand the user's regex directly to rg (Rust `regex` crate) — its
|
|
230
|
+
* syntax is a superset of the JS subset most users write, so practical
|
|
231
|
+
* patterns (`logger\.info\(`, `\bfoo\b`, etc.) behave identically. If a
|
|
232
|
+
* pattern uses JS-only syntax (lookbehind, named backrefs in some
|
|
233
|
+
* forms), rg will reject it during discovery and we'll fall through
|
|
234
|
+
* with zero candidates — a no-op, not a crash.
|
|
235
|
+
*
|
|
236
|
+
* include/exclude globs are forwarded to rg AND re-applied in JS via
|
|
237
|
+
* minimatch. The JS post-filter is the source of truth: rg's --glob
|
|
238
|
+
* behaviour with absolute search roots differs across rg versions on
|
|
239
|
+
* Windows (the glob may be matched against the full path instead of the
|
|
240
|
+
* path relative to the search root), making it unreliable as the sole
|
|
241
|
+
* filter. Passing them to rg is still worthwhile for performance — it
|
|
242
|
+
* narrows the candidate set early — but correctness is guaranteed by the
|
|
243
|
+
* minimatch pass below.
|
|
244
|
+
*/
|
|
245
|
+
|
|
246
|
+
/**
|
|
247
|
+
* Determine whether an absolute path `p` matches a set of include and/or
|
|
248
|
+
* exclude glob patterns. Patterns are matched against the POSIX-normalised
|
|
249
|
+
* path relative to `base` (forward slashes, no leading "./"). Both `include`
|
|
250
|
+
* and `exclude` use minimatch with `{dot:true, matchBase:true}` so patterns
|
|
251
|
+
* like "src/**\/*.ts" or "**\/vendor\/**" work regardless of platform.
|
|
252
|
+
*
|
|
253
|
+
* Rules:
|
|
254
|
+
* - If `include` is non-empty, the file must match at least one pattern.
|
|
255
|
+
* - If `exclude` is non-empty, the file must not match any pattern.
|
|
256
|
+
* - If both are empty, the file is always accepted.
|
|
257
|
+
*/
|
|
258
|
+
function matchesGlobs(
|
|
259
|
+
p: string,
|
|
260
|
+
base: string,
|
|
261
|
+
include: string[] | undefined,
|
|
262
|
+
exclude: string[] | undefined,
|
|
263
|
+
): boolean {
|
|
264
|
+
// Relative path with forward slashes regardless of platform.
|
|
265
|
+
const rel = relative(base, p).split(sep).join("/");
|
|
266
|
+
const opts = { dot: true, matchBase: true };
|
|
267
|
+
|
|
268
|
+
if (include && include.length > 0) {
|
|
269
|
+
const matched = include.some((g) => minimatch(rel, g, opts));
|
|
270
|
+
if (!matched) return false;
|
|
271
|
+
}
|
|
272
|
+
if (exclude && exclude.length > 0) {
|
|
273
|
+
const excluded = exclude.some((g) => minimatch(rel, g, opts));
|
|
274
|
+
if (excluded) return false;
|
|
275
|
+
}
|
|
276
|
+
return true;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
function discoverCandidates(
|
|
280
|
+
pattern: string,
|
|
281
|
+
flags: string,
|
|
282
|
+
searchRoots: string[],
|
|
283
|
+
include: string[] | undefined,
|
|
284
|
+
exclude: string[] | undefined,
|
|
285
|
+
cap: number,
|
|
286
|
+
): { files: string[]; truncated: boolean; rgFailed: boolean } {
|
|
287
|
+
const rg = resolveRg();
|
|
288
|
+
const args: string[] = ["--files-with-matches"];
|
|
289
|
+
|
|
290
|
+
if (flags.includes("i")) args.push("--ignore-case");
|
|
291
|
+
if (flags.includes("m")) args.push("--multiline");
|
|
292
|
+
if (flags.includes("s")) {
|
|
293
|
+
// `s` (dotall) in JS = "." matches \n. rg's `--multiline-dotall` plus
|
|
294
|
+
// `--multiline` gives the same effect.
|
|
295
|
+
args.push("--multiline");
|
|
296
|
+
args.push("--multiline-dotall");
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
// Pass globs to rg for a performance pre-filter (narrows the candidate set
|
|
300
|
+
// early). Correctness is re-verified below by the JS matchesGlobs() pass —
|
|
301
|
+
// see the note above discoverCandidates for why rg alone is insufficient.
|
|
302
|
+
// Cross-platform note: rg's `--glob` interpretation on Windows is brittle
|
|
303
|
+
// (case-insensitive drive letters, slash-direction semantics differ
|
|
304
|
+
// between rg builds). We still pass the globs as a perf hint, but on
|
|
305
|
+
// Windows the JS post-filter (matchesGlobs) is the load-bearing piece —
|
|
306
|
+
// skipping the pre-filter here for include/exclude on win32 would only
|
|
307
|
+
// cost extra files to scan, not correctness. We accept that small cost
|
|
308
|
+
// in exchange for predictable behavior.
|
|
309
|
+
if (process.platform !== "win32") {
|
|
310
|
+
for (const g of include ?? []) args.push("--glob", g);
|
|
311
|
+
for (const g of exclude ?? []) args.push("--glob", `!${g}`);
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
// Signal end-of-flags before the pattern + roots so a pattern starting
|
|
315
|
+
// with `-` isn't misparsed as a flag.
|
|
316
|
+
args.push("--regexp", pattern);
|
|
317
|
+
for (const r of searchRoots) args.push(r);
|
|
318
|
+
|
|
319
|
+
const res = spawnSync(rg, args, { encoding: "utf-8", timeout: 30_000 });
|
|
320
|
+
// rg exits 0 = matches found, 1 = no matches, 2 = error
|
|
321
|
+
if (res.status === 2 || (res.error && !res.stdout)) {
|
|
322
|
+
return { files: [], truncated: false, rgFailed: true };
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
// Canonical form of each search root — used by matchesGlobs to compute the
|
|
326
|
+
// path relative to whichever root contains the candidate.
|
|
327
|
+
const canonicalRoots = searchRoots.map((r) => {
|
|
328
|
+
try { return realpathSync(r); } catch { return pathResolve(r); }
|
|
329
|
+
});
|
|
330
|
+
|
|
331
|
+
const files = new Set<string>();
|
|
332
|
+
let truncated = false;
|
|
333
|
+
for (const line of (res.stdout ?? "").split("\n")) {
|
|
334
|
+
// trim() strips both '\r' (Windows CRLF from rg output) and whitespace.
|
|
335
|
+
const p = line.trim();
|
|
336
|
+
if (!p) continue;
|
|
337
|
+
// Normalize separators for the current platform so that paths emitted by
|
|
338
|
+
// rg on Windows (which may use forward slashes for --glob-matched results)
|
|
339
|
+
// are consistent with what statSync / clampToCwd expect.
|
|
340
|
+
const normalized = pathNormalize(p);
|
|
341
|
+
// Canonicalize so macOS /var → /private/var matches the clamp output.
|
|
342
|
+
let canonical: string;
|
|
343
|
+
try {
|
|
344
|
+
canonical = realpathSync(normalized);
|
|
345
|
+
} catch {
|
|
346
|
+
canonical = pathResolve(normalized);
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
// JS-side include/exclude post-filter. rg's --glob is a performance
|
|
350
|
+
// pre-filter only; this pass is the correctness gate. We match against
|
|
351
|
+
// the path relative to whichever root contains the file (longest prefix
|
|
352
|
+
// wins so nested roots are handled correctly).
|
|
353
|
+
if (include?.length || exclude?.length) {
|
|
354
|
+
let base = canonicalRoots[0] ?? process.cwd();
|
|
355
|
+
for (const r of canonicalRoots) {
|
|
356
|
+
if (canonical.startsWith(r) && r.length > base.length) base = r;
|
|
357
|
+
}
|
|
358
|
+
if (!matchesGlobs(canonical, base, include, exclude)) continue;
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
if (files.size >= cap) {
|
|
362
|
+
truncated = true;
|
|
363
|
+
break;
|
|
364
|
+
}
|
|
365
|
+
files.add(canonical);
|
|
366
|
+
}
|
|
367
|
+
return { files: Array.from(files), truncated, rgFailed: false };
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
// ---------------------------------------------------------------------------
|
|
371
|
+
// Replacement with a per-file match cap
|
|
372
|
+
// ---------------------------------------------------------------------------
|
|
373
|
+
|
|
374
|
+
/**
|
|
375
|
+
* Apply the regex against `source`, returning `{ updated, count, capped }`.
|
|
376
|
+
* Walks matches manually (rather than calling `String.replaceAll`) so we
|
|
377
|
+
* can respect the per-file cap deterministically — once we hit `cap`
|
|
378
|
+
* replacements, further matches are left in place and `capped=true` is
|
|
379
|
+
* returned.
|
|
380
|
+
*
|
|
381
|
+
* The regex is assumed to have the global `g` flag (enforced by the
|
|
382
|
+
* compile step). Capture groups (`$1`, `$&`) in `replacement` are
|
|
383
|
+
* supported via JS's native semantics by delegating each replacement to
|
|
384
|
+
* `source.substring(m.index, m.index + m[0].length).replace(singleRe, repl)`
|
|
385
|
+
* where `singleRe` is a non-global copy of the compiled pattern — that
|
|
386
|
+
* way `$n` backrefs resolve correctly without us reimplementing JS's
|
|
387
|
+
* replacement mini-DSL.
|
|
388
|
+
*/
|
|
389
|
+
export function applyRegex(
|
|
390
|
+
source: string,
|
|
391
|
+
re: RegExp,
|
|
392
|
+
replacement: string,
|
|
393
|
+
cap: number,
|
|
394
|
+
): { updated: string; count: number; capped: boolean } {
|
|
395
|
+
// Non-global sibling for single-match replacement with backref support.
|
|
396
|
+
const singleFlags = re.flags.replace(/g/g, "");
|
|
397
|
+
const singleRe = new RegExp(re.source, singleFlags);
|
|
398
|
+
|
|
399
|
+
let out = "";
|
|
400
|
+
let lastIdx = 0;
|
|
401
|
+
let count = 0;
|
|
402
|
+
re.lastIndex = 0;
|
|
403
|
+
|
|
404
|
+
let m: RegExpExecArray | null;
|
|
405
|
+
while ((m = re.exec(source))) {
|
|
406
|
+
// Zero-width match defence in depth — the compile step already
|
|
407
|
+
// rejects these but a regex could theoretically produce an
|
|
408
|
+
// empty match mid-stream (e.g. `a*`). Bail out if we see one.
|
|
409
|
+
if (m[0].length === 0) {
|
|
410
|
+
re.lastIndex = m.index + 1;
|
|
411
|
+
continue;
|
|
412
|
+
}
|
|
413
|
+
if (count >= cap) break;
|
|
414
|
+
|
|
415
|
+
out += source.slice(lastIdx, m.index);
|
|
416
|
+
// JS's `replace` on a fresh non-global regex applied to the exact
|
|
417
|
+
// match text resolves $1…$n / $& / $` / $' correctly.
|
|
418
|
+
const rewritten = m[0].replace(singleRe, replacement);
|
|
419
|
+
out += rewritten;
|
|
420
|
+
lastIdx = m.index + m[0].length;
|
|
421
|
+
count += 1;
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
const capped = count >= cap && re.exec(source) !== null;
|
|
425
|
+
re.lastIndex = 0;
|
|
426
|
+
|
|
427
|
+
out += source.slice(lastIdx);
|
|
428
|
+
return { updated: out, count, capped };
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
// ---------------------------------------------------------------------------
|
|
432
|
+
// Atomic per-file write
|
|
433
|
+
// ---------------------------------------------------------------------------
|
|
434
|
+
|
|
435
|
+
/**
|
|
436
|
+
* Write `content` to `path` atomically (temp file in same directory, then
|
|
437
|
+
* rename). Keeps reads consistent even if the process dies mid-write.
|
|
438
|
+
*/
|
|
439
|
+
async function atomicWrite(path: string, content: string): Promise<void> {
|
|
440
|
+
const tmp = `${path}.${process.pid}.${Date.now()}.tmp`;
|
|
441
|
+
await writeFile(tmp, content, "utf-8");
|
|
442
|
+
await rename(tmp, path);
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
// ---------------------------------------------------------------------------
|
|
446
|
+
// Core planner
|
|
447
|
+
// ---------------------------------------------------------------------------
|
|
448
|
+
|
|
449
|
+
export async function planSearchReplaceRegex(
|
|
450
|
+
args: SearchReplaceRegexArgs,
|
|
451
|
+
): Promise<SearchReplaceRegexResult> {
|
|
452
|
+
const {
|
|
453
|
+
pattern,
|
|
454
|
+
replacement,
|
|
455
|
+
flags,
|
|
456
|
+
include,
|
|
457
|
+
exclude,
|
|
458
|
+
maxFiles = DEFAULT_MAX_FILES,
|
|
459
|
+
maxMatchesPerFile = DEFAULT_MAX_MATCHES_PER_FILE,
|
|
460
|
+
roots,
|
|
461
|
+
} = args;
|
|
462
|
+
|
|
463
|
+
if (typeof replacement !== "string") {
|
|
464
|
+
return { ok: false, reason: "'replacement' is required and must be a string" };
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
const compiled = compilePattern(pattern, flags);
|
|
468
|
+
if (!compiled.ok) return compiled;
|
|
469
|
+
|
|
470
|
+
// Resolve + clamp search roots.
|
|
471
|
+
const searchRoots: string[] = [];
|
|
472
|
+
if (Array.isArray(roots) && roots.length > 0) {
|
|
473
|
+
for (const r of roots) {
|
|
474
|
+
const c = clampToCwd(r, "ashlr__search_replace_regex");
|
|
475
|
+
if (!c.ok) return { ok: false, reason: c.message };
|
|
476
|
+
searchRoots.push(c.abs);
|
|
477
|
+
}
|
|
478
|
+
} else {
|
|
479
|
+
const c = clampToCwd(primaryProjectRoot(), "ashlr__search_replace_regex");
|
|
480
|
+
if (!c.ok) return { ok: false, reason: c.message };
|
|
481
|
+
searchRoots.push(c.abs);
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
const warnings: string[] = [];
|
|
485
|
+
|
|
486
|
+
const rawFlags = (flags ?? "").replace(/g/g, "");
|
|
487
|
+
const finalFlags = "g" + rawFlags;
|
|
488
|
+
const { files: candidates, truncated, rgFailed } = discoverCandidates(
|
|
489
|
+
pattern,
|
|
490
|
+
finalFlags,
|
|
491
|
+
searchRoots,
|
|
492
|
+
include,
|
|
493
|
+
exclude,
|
|
494
|
+
maxFiles,
|
|
495
|
+
);
|
|
496
|
+
|
|
497
|
+
if (rgFailed) {
|
|
498
|
+
return {
|
|
499
|
+
ok: false,
|
|
500
|
+
reason: "ripgrep failed to scan — likely a regex syntax rg doesn't accept (e.g. JS lookbehind). Try a simpler pattern.",
|
|
501
|
+
};
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
if (truncated) {
|
|
505
|
+
warnings.push(
|
|
506
|
+
`ripgrep returned more than ${maxFiles} candidates — only the first ${maxFiles} were scanned. ` +
|
|
507
|
+
`Rerun with a larger maxFiles or narrower include/roots.`,
|
|
508
|
+
);
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
const filesPlan: FilePlan[] = [];
|
|
512
|
+
let skipped = 0;
|
|
513
|
+
|
|
514
|
+
for (const candidate of candidates) {
|
|
515
|
+
// Clamp each candidate too — rg shouldn't return paths outside cwd
|
|
516
|
+
// since we gave it cwd-scoped roots, but defense in depth: if a
|
|
517
|
+
// symlink in the roots points outside cwd, the clamp refuses.
|
|
518
|
+
const clamp = clampToCwd(candidate, "ashlr__search_replace_regex");
|
|
519
|
+
if (!clamp.ok) {
|
|
520
|
+
skipped++;
|
|
521
|
+
warnings.push(`${candidate}: skipped — outside cwd`);
|
|
522
|
+
continue;
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
if (isBinaryByExt(candidate)) {
|
|
526
|
+
skipped++;
|
|
527
|
+
warnings.push(`${candidate}: skipped — binary extension`);
|
|
528
|
+
continue;
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
// Stat + size guard: refuse 0-byte, non-regular, and absurdly-large
|
|
532
|
+
// files (>50 MB) so a stray ISO in the roots can't block the tool.
|
|
533
|
+
let st;
|
|
534
|
+
try {
|
|
535
|
+
st = statSync(candidate);
|
|
536
|
+
} catch {
|
|
537
|
+
skipped++;
|
|
538
|
+
continue;
|
|
539
|
+
}
|
|
540
|
+
if (!st.isFile()) {
|
|
541
|
+
skipped++;
|
|
542
|
+
continue;
|
|
543
|
+
}
|
|
544
|
+
if (st.size === 0) {
|
|
545
|
+
skipped++;
|
|
546
|
+
continue;
|
|
547
|
+
}
|
|
548
|
+
if (st.size > MAX_FILE_BYTES) {
|
|
549
|
+
skipped++;
|
|
550
|
+
warnings.push(
|
|
551
|
+
`${candidate}: skipped — file is ${st.size} bytes, over the ${MAX_FILE_BYTES} byte safety cap`,
|
|
552
|
+
);
|
|
553
|
+
continue;
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
let raw: Buffer;
|
|
557
|
+
try {
|
|
558
|
+
raw = await readFile(candidate);
|
|
559
|
+
} catch {
|
|
560
|
+
skipped++;
|
|
561
|
+
continue;
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
if (looksBinary(raw)) {
|
|
565
|
+
skipped++;
|
|
566
|
+
warnings.push(`${candidate}: skipped — binary content (NUL byte in first ${SNIFF_BYTES}B)`);
|
|
567
|
+
continue;
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
const source = raw.toString("utf-8");
|
|
571
|
+
const { updated, count, capped } = applyRegex(source, compiled.re, replacement, maxMatchesPerFile);
|
|
572
|
+
if (count === 0) {
|
|
573
|
+
// rg said there were matches but our regex saw none — possible if
|
|
574
|
+
// rg's regex engine differs at some edge (unicode classes, etc.).
|
|
575
|
+
// Skip silently.
|
|
576
|
+
skipped++;
|
|
577
|
+
continue;
|
|
578
|
+
}
|
|
579
|
+
if (capped) {
|
|
580
|
+
warnings.push(
|
|
581
|
+
`${candidate}: match cap hit — only first ${maxMatchesPerFile} replacements applied, remaining matches left in place.`,
|
|
582
|
+
);
|
|
583
|
+
}
|
|
584
|
+
filesPlan.push({
|
|
585
|
+
path: candidate,
|
|
586
|
+
matches: count,
|
|
587
|
+
capped,
|
|
588
|
+
originalBytes: Buffer.byteLength(source, "utf-8"),
|
|
589
|
+
newBytes: Buffer.byteLength(updated, "utf-8"),
|
|
590
|
+
});
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
return {
|
|
594
|
+
ok: true,
|
|
595
|
+
plan: {
|
|
596
|
+
files: filesPlan,
|
|
597
|
+
skipped,
|
|
598
|
+
warnings,
|
|
599
|
+
},
|
|
600
|
+
};
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
// ---------------------------------------------------------------------------
|
|
604
|
+
// Apply (writes with per-file rollback)
|
|
605
|
+
// ---------------------------------------------------------------------------
|
|
606
|
+
|
|
607
|
+
/**
|
|
608
|
+
* Re-read each planned file, re-apply the regex, write atomically.
|
|
609
|
+
* On any write failure, best-effort rolls back previously-written files
|
|
610
|
+
* from the cached pre-image. We read-and-replace again here (rather
|
|
611
|
+
* than caching the planner's `updated` strings) so the plan stays
|
|
612
|
+
* accurate even if the file changed between plan and apply — if a
|
|
613
|
+
* file no longer has matches, we simply skip it.
|
|
614
|
+
*/
|
|
615
|
+
export async function applySearchReplaceRegexPlan(
|
|
616
|
+
plan: SearchReplaceRegexPlan,
|
|
617
|
+
args: SearchReplaceRegexArgs,
|
|
618
|
+
): Promise<{
|
|
619
|
+
written: number;
|
|
620
|
+
totalReplacements: number;
|
|
621
|
+
totalOriginalBytes: number;
|
|
622
|
+
totalNewBytes: number;
|
|
623
|
+
rollbackWarnings: string[];
|
|
624
|
+
}> {
|
|
625
|
+
const compiled = compilePattern(args.pattern, args.flags);
|
|
626
|
+
if (!compiled.ok) {
|
|
627
|
+
// Shouldn't happen (planner already validated), but be safe.
|
|
628
|
+
throw new Error(compiled.reason);
|
|
629
|
+
}
|
|
630
|
+
const cap = args.maxMatchesPerFile ?? DEFAULT_MAX_MATCHES_PER_FILE;
|
|
631
|
+
|
|
632
|
+
const writtenPreImages: Array<{ path: string; original: string }> = [];
|
|
633
|
+
const rollbackWarnings: string[] = [];
|
|
634
|
+
let totalReplacements = 0;
|
|
635
|
+
let totalOriginalBytes = 0;
|
|
636
|
+
let totalNewBytes = 0;
|
|
637
|
+
|
|
638
|
+
for (const fp of plan.files) {
|
|
639
|
+
let source: string;
|
|
640
|
+
try {
|
|
641
|
+
source = await readFile(fp.path, "utf-8");
|
|
642
|
+
} catch (err) {
|
|
643
|
+
rollbackWarnings.push(`failed to re-read ${fp.path}: ${(err as Error).message}`);
|
|
644
|
+
continue;
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
const { updated, count } = applyRegex(source, compiled.re, args.replacement, cap);
|
|
648
|
+
if (count === 0) continue;
|
|
649
|
+
|
|
650
|
+
try {
|
|
651
|
+
await atomicWrite(fp.path, updated);
|
|
652
|
+
writtenPreImages.push({ path: fp.path, original: source });
|
|
653
|
+
totalReplacements += count;
|
|
654
|
+
totalOriginalBytes += Buffer.byteLength(source, "utf-8");
|
|
655
|
+
totalNewBytes += Buffer.byteLength(updated, "utf-8");
|
|
656
|
+
} catch (err) {
|
|
657
|
+
// Roll back everything written so far.
|
|
658
|
+
for (const prev of writtenPreImages) {
|
|
659
|
+
try {
|
|
660
|
+
await writeFile(prev.path, prev.original, "utf-8");
|
|
661
|
+
} catch (rbErr) {
|
|
662
|
+
rollbackWarnings.push(
|
|
663
|
+
`rollback failed for ${prev.path}: ${(rbErr as Error).message}`,
|
|
664
|
+
);
|
|
665
|
+
}
|
|
666
|
+
}
|
|
667
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
668
|
+
throw new Error(`failed to write ${fp.path}: ${msg}`);
|
|
669
|
+
}
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
return {
|
|
673
|
+
written: writtenPreImages.length,
|
|
674
|
+
totalReplacements,
|
|
675
|
+
totalOriginalBytes,
|
|
676
|
+
totalNewBytes,
|
|
677
|
+
rollbackWarnings,
|
|
678
|
+
};
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
// ---------------------------------------------------------------------------
|
|
682
|
+
// Top-level callable (used by the MCP handler)
|
|
683
|
+
// ---------------------------------------------------------------------------
|
|
684
|
+
|
|
685
|
+
export async function ashlrSearchReplaceRegex(
|
|
686
|
+
args: SearchReplaceRegexArgs,
|
|
687
|
+
): Promise<{ text: string; originalBytes: number; newBytes: number }> {
|
|
688
|
+
const planRes = await planSearchReplaceRegex(args);
|
|
689
|
+
if (!planRes.ok) {
|
|
690
|
+
throw new Error(planRes.reason);
|
|
691
|
+
}
|
|
692
|
+
const { plan } = planRes;
|
|
693
|
+
const dryRun = args.dryRun === true;
|
|
694
|
+
|
|
695
|
+
const fileCount = plan.files.length;
|
|
696
|
+
const totalMatches = plan.files.reduce((s, f) => s + f.matches, 0);
|
|
697
|
+
|
|
698
|
+
const lines: string[] = [];
|
|
699
|
+
lines.push(
|
|
700
|
+
`[ashlr__search_replace_regex] ${dryRun ? "(dry run) " : ""}` +
|
|
701
|
+
`${fileCount} file${fileCount === 1 ? "" : "s"}, ${totalMatches} replacement${totalMatches === 1 ? "" : "s"}` +
|
|
702
|
+
(plan.skipped > 0 ? `, ${plan.skipped} candidate${plan.skipped === 1 ? "" : "s"} skipped` : ""),
|
|
703
|
+
);
|
|
704
|
+
|
|
705
|
+
if (plan.warnings.length > 0) {
|
|
706
|
+
lines.push(" warnings:");
|
|
707
|
+
for (const w of plan.warnings) lines.push(` ${w}`);
|
|
708
|
+
}
|
|
709
|
+
|
|
710
|
+
// Compact per-file breakdown when the change set is small.
|
|
711
|
+
if (fileCount <= 10) {
|
|
712
|
+
for (const fp of plan.files) {
|
|
713
|
+
lines.push(` ${fp.path}: ${fp.matches} match${fp.matches === 1 ? "" : "es"}${fp.capped ? " (capped)" : ""}`);
|
|
714
|
+
}
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
let totalOriginalBytes = 0;
|
|
718
|
+
let totalNewBytes = 0;
|
|
719
|
+
|
|
720
|
+
if (dryRun) {
|
|
721
|
+
for (const fp of plan.files) {
|
|
722
|
+
totalOriginalBytes += fp.originalBytes;
|
|
723
|
+
totalNewBytes += fp.newBytes;
|
|
724
|
+
}
|
|
725
|
+
lines.push(" (dry run — no files written)");
|
|
726
|
+
return { text: lines.join("\n"), originalBytes: totalOriginalBytes, newBytes: totalNewBytes };
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
if (fileCount === 0) {
|
|
730
|
+
return { text: lines.join("\n"), originalBytes: 0, newBytes: 0 };
|
|
731
|
+
}
|
|
732
|
+
|
|
733
|
+
const applied = await applySearchReplaceRegexPlan(plan, args);
|
|
734
|
+
totalOriginalBytes = applied.totalOriginalBytes;
|
|
735
|
+
totalNewBytes = applied.totalNewBytes;
|
|
736
|
+
|
|
737
|
+
lines.push(
|
|
738
|
+
` ${applied.written} file${applied.written === 1 ? "" : "s"} written, ` +
|
|
739
|
+
`${applied.totalReplacements} total replacement${applied.totalReplacements === 1 ? "" : "s"}`,
|
|
740
|
+
);
|
|
741
|
+
if (applied.rollbackWarnings.length > 0) {
|
|
742
|
+
lines.push(" rollback warnings:");
|
|
743
|
+
for (const w of applied.rollbackWarnings) lines.push(` ${w}`);
|
|
744
|
+
}
|
|
745
|
+
|
|
746
|
+
return { text: lines.join("\n"), originalBytes: totalOriginalBytes, newBytes: totalNewBytes };
|
|
747
|
+
}
|
|
748
|
+
|
|
749
|
+
// Silence unused-import warnings from `stat` — kept in scope for future
|
|
750
|
+
// use (symlink detection).
|
|
751
|
+
void stat;
|