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,626 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* _test-watch — background test-watch session manager for `ashlr__test`.
|
|
3
|
+
*
|
|
4
|
+
* Companion to the `ashlr__bash_start`/`bash_tail`/`bash_stop`/`bash_list`
|
|
5
|
+
* primitives. A test-watch session:
|
|
6
|
+
*
|
|
7
|
+
* 1. Spawns the detected runner (bun/vitest/jest/pytest/go) once on entry.
|
|
8
|
+
* 2. Watches the session cwd for .ts/.tsx/.js/.jsx/.mjs/.cjs/.py/.go file
|
|
9
|
+
* changes (via `fs.watch` recursive, with a poll fallback when the
|
|
10
|
+
* platform rejects `{ recursive: true }`).
|
|
11
|
+
* 3. Debounces rapid change bursts (200ms) so a multi-file save doesn't
|
|
12
|
+
* thrash the runner.
|
|
13
|
+
* 4. Reruns the same command+args on each settled change.
|
|
14
|
+
* 5. Lives until explicitly stopped (via `stopTestWatch` / `ashlr__bash_stop`)
|
|
15
|
+
* or until the parent MCP server exits (`process.on('exit')`).
|
|
16
|
+
*
|
|
17
|
+
* Sessions are registered with the bash-server via `registerExternalSessions`,
|
|
18
|
+
* so `ashlr__bash_list` enumerates both "bash" and "test-watch" sessions from
|
|
19
|
+
* one place. Per-session metadata is persisted to
|
|
20
|
+
* `~/.ashlr/test-watch-sessions/<id>.json` so a doctor can prune stale entries
|
|
21
|
+
* after a crash.
|
|
22
|
+
*
|
|
23
|
+
* Safety caps:
|
|
24
|
+
* - max 8 concurrent watch sessions per user
|
|
25
|
+
* - 50 MB stdout cap per session (mirrors bash-server)
|
|
26
|
+
* - child is spawned with its own process group on POSIX so SIGKILL reaches
|
|
27
|
+
* grandchildren (vitest workers, bun subprocesses)
|
|
28
|
+
*/
|
|
29
|
+
|
|
30
|
+
import { spawn, type ChildProcess } from "child_process";
|
|
31
|
+
import { existsSync, mkdirSync, readdirSync, statSync, unlinkSync, watch, writeFileSync, type FSWatcher } from "fs";
|
|
32
|
+
import { writeFile, readFile, mkdir, readdir, unlink } from "fs/promises";
|
|
33
|
+
import { homedir } from "os";
|
|
34
|
+
import { join, sep } from "path";
|
|
35
|
+
import { randomBytes } from "crypto";
|
|
36
|
+
import { registerExternalSessions, type ExternalSession } from "./bash-server";
|
|
37
|
+
|
|
38
|
+
// ---------------------------------------------------------------------------
|
|
39
|
+
// Constants
|
|
40
|
+
// ---------------------------------------------------------------------------
|
|
41
|
+
|
|
42
|
+
const MAX_WATCH_SESSIONS = 8;
|
|
43
|
+
const DEBOUNCE_MS = 200;
|
|
44
|
+
const MAX_CUMULATIVE_BYTES = 50 * 1024 * 1024;
|
|
45
|
+
const POLL_INTERVAL_MS = 1000;
|
|
46
|
+
const WATCH_EXTENSIONS = new Set([
|
|
47
|
+
".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs", ".py", ".go",
|
|
48
|
+
]);
|
|
49
|
+
const IGNORE_DIRS = new Set([
|
|
50
|
+
"node_modules", ".git", "dist", "build", "coverage",
|
|
51
|
+
".next", ".nuxt", ".svelte-kit", "target", "__pycache__",
|
|
52
|
+
".pytest_cache", ".venv", "venv",
|
|
53
|
+
]);
|
|
54
|
+
|
|
55
|
+
// Prefer $HOME when explicitly set (matches the rest of the ashlr codebase).
|
|
56
|
+
const ASHLR_HOME = process.env.HOME ?? homedir();
|
|
57
|
+
const WATCH_DIR = join(ASHLR_HOME, ".ashlr", "test-watch-sessions");
|
|
58
|
+
|
|
59
|
+
// ---------------------------------------------------------------------------
|
|
60
|
+
// Types
|
|
61
|
+
// ---------------------------------------------------------------------------
|
|
62
|
+
|
|
63
|
+
export interface TestWatchArgs {
|
|
64
|
+
/** Working directory to watch and to spawn tests in. Must be clamped. */
|
|
65
|
+
cwd: string;
|
|
66
|
+
/** Command + args array to run on each change (already resolved). */
|
|
67
|
+
command: string[];
|
|
68
|
+
/** Human-readable label for bash_list ("bun test src/foo.test.ts"). */
|
|
69
|
+
label: string;
|
|
70
|
+
/** Optional grep filter appended to args for re-runs (informational). */
|
|
71
|
+
grep?: string;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
interface WatchSession {
|
|
75
|
+
id: string;
|
|
76
|
+
pid: number; // pid of the CURRENT running child (-1 if idle)
|
|
77
|
+
command: string[];
|
|
78
|
+
label: string;
|
|
79
|
+
cwd: string;
|
|
80
|
+
startedAt: number;
|
|
81
|
+
child: ChildProcess | null;
|
|
82
|
+
stdout: string;
|
|
83
|
+
stderr: string;
|
|
84
|
+
offset: number;
|
|
85
|
+
stderrOffset: number;
|
|
86
|
+
cumulativeBytes: number;
|
|
87
|
+
exitCode: number | null;
|
|
88
|
+
signal: NodeJS.Signals | null;
|
|
89
|
+
lastActivity: number;
|
|
90
|
+
lastRunAt: number;
|
|
91
|
+
runCount: number;
|
|
92
|
+
watcher: FSWatcher | null;
|
|
93
|
+
pollTimer: ReturnType<typeof setInterval> | null;
|
|
94
|
+
pollSnapshot: Map<string, number>; // path -> mtimeMs
|
|
95
|
+
debounceTimer: ReturnType<typeof setTimeout> | null;
|
|
96
|
+
stopped: boolean;
|
|
97
|
+
dataEmitter: Set<() => void>;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
interface PersistedWatchSession {
|
|
101
|
+
id: string;
|
|
102
|
+
pid: number;
|
|
103
|
+
command: string[];
|
|
104
|
+
label: string;
|
|
105
|
+
cwd: string;
|
|
106
|
+
startedAt: number;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// ---------------------------------------------------------------------------
|
|
110
|
+
// In-memory registry
|
|
111
|
+
// ---------------------------------------------------------------------------
|
|
112
|
+
|
|
113
|
+
const SESSIONS = new Map<string, WatchSession>();
|
|
114
|
+
|
|
115
|
+
function newId(): string {
|
|
116
|
+
return randomBytes(4).toString("hex");
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// ---------------------------------------------------------------------------
|
|
120
|
+
// Persistence
|
|
121
|
+
// ---------------------------------------------------------------------------
|
|
122
|
+
|
|
123
|
+
function sessionFile(id: string): string {
|
|
124
|
+
return join(WATCH_DIR, `${id}.json`);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
async function persistSession(s: WatchSession): Promise<void> {
|
|
128
|
+
try {
|
|
129
|
+
await mkdir(WATCH_DIR, { recursive: true });
|
|
130
|
+
const payload: PersistedWatchSession = {
|
|
131
|
+
id: s.id,
|
|
132
|
+
pid: s.pid,
|
|
133
|
+
command: s.command,
|
|
134
|
+
label: s.label,
|
|
135
|
+
cwd: s.cwd,
|
|
136
|
+
startedAt: s.startedAt,
|
|
137
|
+
};
|
|
138
|
+
await writeFile(sessionFile(s.id), JSON.stringify(payload, null, 2));
|
|
139
|
+
} catch {
|
|
140
|
+
/* best-effort — disk full / read-only home shouldn't wedge the runner */
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
async function unpersistSession(id: string): Promise<void> {
|
|
145
|
+
try {
|
|
146
|
+
await unlink(sessionFile(id));
|
|
147
|
+
} catch {
|
|
148
|
+
/* already gone */
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Prune stale JSON files whose pids are no longer alive. Called once at
|
|
154
|
+
* module load; dead files don't count against MAX_WATCH_SESSIONS.
|
|
155
|
+
*/
|
|
156
|
+
export async function reloadWatchSessions(): Promise<void> {
|
|
157
|
+
if (!existsSync(WATCH_DIR)) return;
|
|
158
|
+
let entries: string[];
|
|
159
|
+
try {
|
|
160
|
+
entries = await readdir(WATCH_DIR);
|
|
161
|
+
} catch {
|
|
162
|
+
return;
|
|
163
|
+
}
|
|
164
|
+
for (const name of entries) {
|
|
165
|
+
if (!name.endsWith(".json")) continue;
|
|
166
|
+
const path = join(WATCH_DIR, name);
|
|
167
|
+
try {
|
|
168
|
+
const raw = await readFile(path, "utf-8");
|
|
169
|
+
const p = JSON.parse(raw) as PersistedWatchSession;
|
|
170
|
+
if (!pidAlive(p.pid)) {
|
|
171
|
+
try { await unlink(path); } catch { /* ignore */ }
|
|
172
|
+
}
|
|
173
|
+
// We can't reattach to a process we didn't spawn, so even live pids
|
|
174
|
+
// from a prior run are orphan zombies; drop their files too. They
|
|
175
|
+
// were cleaned up by process.on('exit') in the previous run when the
|
|
176
|
+
// parent died.
|
|
177
|
+
} catch {
|
|
178
|
+
try { await unlink(path); } catch { /* ignore */ }
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
function pidAlive(pid: number): boolean {
|
|
184
|
+
if (!pid || pid <= 0) return false;
|
|
185
|
+
try {
|
|
186
|
+
process.kill(pid, 0);
|
|
187
|
+
return true;
|
|
188
|
+
} catch {
|
|
189
|
+
return false;
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// ---------------------------------------------------------------------------
|
|
194
|
+
// File watching
|
|
195
|
+
// ---------------------------------------------------------------------------
|
|
196
|
+
|
|
197
|
+
function isWatchedFile(path: string): boolean {
|
|
198
|
+
const parts = path.split(sep);
|
|
199
|
+
for (const p of parts) {
|
|
200
|
+
if (IGNORE_DIRS.has(p)) return false;
|
|
201
|
+
}
|
|
202
|
+
const dot = path.lastIndexOf(".");
|
|
203
|
+
if (dot < 0) return false;
|
|
204
|
+
const ext = path.slice(dot);
|
|
205
|
+
return WATCH_EXTENSIONS.has(ext);
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
function snapshotDir(root: string): Map<string, number> {
|
|
209
|
+
// Walk the tree breadth-first and stat each watched file. Used by the
|
|
210
|
+
// poll-fallback path when fs.watch(recursive) is unavailable.
|
|
211
|
+
const out = new Map<string, number>();
|
|
212
|
+
const queue: string[] = [root];
|
|
213
|
+
while (queue.length > 0) {
|
|
214
|
+
const dir = queue.shift()!;
|
|
215
|
+
let names: string[];
|
|
216
|
+
try { names = readdirSync(dir); } catch { continue; }
|
|
217
|
+
for (const name of names) {
|
|
218
|
+
if (IGNORE_DIRS.has(name)) continue;
|
|
219
|
+
const full = join(dir, name);
|
|
220
|
+
let st: ReturnType<typeof statSync>;
|
|
221
|
+
try { st = statSync(full); } catch { continue; }
|
|
222
|
+
if (st.isDirectory()) {
|
|
223
|
+
queue.push(full);
|
|
224
|
+
} else if (st.isFile() && isWatchedFile(full)) {
|
|
225
|
+
out.set(full, st.mtimeMs);
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
return out;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
function diffSnapshots(prev: Map<string, number>, next: Map<string, number>): boolean {
|
|
233
|
+
if (prev.size !== next.size) return true;
|
|
234
|
+
for (const [k, v] of next) {
|
|
235
|
+
const pv = prev.get(k);
|
|
236
|
+
if (pv === undefined || pv !== v) return true;
|
|
237
|
+
}
|
|
238
|
+
return false;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
/**
|
|
242
|
+
* Install a watcher on `cwd`. Prefers `fs.watch(cwd, { recursive: true })`
|
|
243
|
+
* (supported on macOS/Windows; Linux requires kernel >= 5.1 for inotify
|
|
244
|
+
* recursive, otherwise throws ERR_FEATURE_UNAVAILABLE_ON_PLATFORM). On
|
|
245
|
+
* failure, falls back to a 1-second mtime poll.
|
|
246
|
+
*
|
|
247
|
+
* Returns { watcher, pollTimer }; exactly one is non-null.
|
|
248
|
+
*/
|
|
249
|
+
function installWatcher(
|
|
250
|
+
session: WatchSession,
|
|
251
|
+
onChange: (path: string) => void,
|
|
252
|
+
): { watcher: FSWatcher | null; pollTimer: ReturnType<typeof setInterval> | null; pollSnapshot: Map<string, number> } {
|
|
253
|
+
// Try native recursive watch first.
|
|
254
|
+
try {
|
|
255
|
+
const w = watch(session.cwd, { recursive: true }, (_event, filename) => {
|
|
256
|
+
if (!filename) return;
|
|
257
|
+
const fname = typeof filename === "string" ? filename : String(filename);
|
|
258
|
+
const full = join(session.cwd, fname);
|
|
259
|
+
if (!isWatchedFile(full)) return;
|
|
260
|
+
onChange(full);
|
|
261
|
+
});
|
|
262
|
+
// fs.watch emits 'error' on some platforms instead of throwing.
|
|
263
|
+
w.on("error", () => { /* swallow — poll will take over if we respawn */ });
|
|
264
|
+
return { watcher: w, pollTimer: null, pollSnapshot: new Map() };
|
|
265
|
+
} catch {
|
|
266
|
+
// Fallback: poll at 1Hz comparing mtime snapshots.
|
|
267
|
+
const snap = snapshotDir(session.cwd);
|
|
268
|
+
const timer = setInterval(() => {
|
|
269
|
+
const next = snapshotDir(session.cwd);
|
|
270
|
+
if (diffSnapshots(session.pollSnapshot, next)) {
|
|
271
|
+
session.pollSnapshot = next;
|
|
272
|
+
// We don't have a single "changed" path; pass the cwd as a sentinel.
|
|
273
|
+
onChange(session.cwd);
|
|
274
|
+
}
|
|
275
|
+
}, POLL_INTERVAL_MS);
|
|
276
|
+
return { watcher: null, pollTimer: timer, pollSnapshot: snap };
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
// ---------------------------------------------------------------------------
|
|
281
|
+
// Child process lifecycle
|
|
282
|
+
// ---------------------------------------------------------------------------
|
|
283
|
+
|
|
284
|
+
function spawnTestRun(session: WatchSession): void {
|
|
285
|
+
if (session.stopped) return;
|
|
286
|
+
// If a prior run is still going, kill it so we don't stack runners.
|
|
287
|
+
if (session.child && session.exitCode === null) {
|
|
288
|
+
killChild(session.child);
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
const isWin = process.platform === "win32";
|
|
292
|
+
const [bin, ...args] = session.command;
|
|
293
|
+
const child = spawn(bin, args, {
|
|
294
|
+
cwd: session.cwd,
|
|
295
|
+
env: { ...process.env },
|
|
296
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
297
|
+
detached: !isWin,
|
|
298
|
+
shell: isWin,
|
|
299
|
+
});
|
|
300
|
+
|
|
301
|
+
session.child = child;
|
|
302
|
+
session.pid = child.pid ?? -1;
|
|
303
|
+
session.exitCode = null;
|
|
304
|
+
session.signal = null;
|
|
305
|
+
session.lastRunAt = Date.now();
|
|
306
|
+
session.runCount += 1;
|
|
307
|
+
session.lastActivity = Date.now();
|
|
308
|
+
|
|
309
|
+
const runHeader = `\n[run #${session.runCount} at ${new Date().toISOString()}] $ ${session.label}\n`;
|
|
310
|
+
session.stdout += runHeader;
|
|
311
|
+
session.cumulativeBytes += runHeader.length;
|
|
312
|
+
|
|
313
|
+
child.stdout?.on("data", (b: Buffer) => {
|
|
314
|
+
const s = b.toString("utf-8");
|
|
315
|
+
session.stdout += s;
|
|
316
|
+
session.cumulativeBytes += b.length;
|
|
317
|
+
session.lastActivity = Date.now();
|
|
318
|
+
if (session.cumulativeBytes > MAX_CUMULATIVE_BYTES) {
|
|
319
|
+
session.stderr += `\n[ashlr: test-watch session exceeded ${MAX_CUMULATIVE_BYTES} bytes — killed]\n`;
|
|
320
|
+
stopTestWatchInternal(session);
|
|
321
|
+
}
|
|
322
|
+
for (const fn of [...session.dataEmitter]) fn();
|
|
323
|
+
});
|
|
324
|
+
child.stderr?.on("data", (b: Buffer) => {
|
|
325
|
+
session.stderr += b.toString("utf-8");
|
|
326
|
+
session.lastActivity = Date.now();
|
|
327
|
+
for (const fn of [...session.dataEmitter]) fn();
|
|
328
|
+
});
|
|
329
|
+
child.on("close", (code, signal) => {
|
|
330
|
+
if (session.child === child) {
|
|
331
|
+
session.exitCode = code;
|
|
332
|
+
session.signal = signal;
|
|
333
|
+
session.pid = -1;
|
|
334
|
+
session.child = null;
|
|
335
|
+
session.lastActivity = Date.now();
|
|
336
|
+
const footer = `[run #${session.runCount} → exit ${code ?? "?"}${signal ? ` · ${signal}` : ""}]\n`;
|
|
337
|
+
session.stdout += footer;
|
|
338
|
+
session.cumulativeBytes += footer.length;
|
|
339
|
+
for (const fn of [...session.dataEmitter]) fn();
|
|
340
|
+
}
|
|
341
|
+
});
|
|
342
|
+
child.on("error", (err) => {
|
|
343
|
+
session.stderr += `\n[spawn error: ${err.message}]\n`;
|
|
344
|
+
for (const fn of [...session.dataEmitter]) fn();
|
|
345
|
+
});
|
|
346
|
+
|
|
347
|
+
// Update persisted pid so a doctor can reap cleanly.
|
|
348
|
+
void persistSession(session);
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
function killChild(child: ChildProcess): void {
|
|
352
|
+
const isWin = process.platform === "win32";
|
|
353
|
+
if (isWin) {
|
|
354
|
+
try { child.kill("SIGKILL"); } catch { /* already dead */ }
|
|
355
|
+
} else if (child.pid != null) {
|
|
356
|
+
try { process.kill(-child.pid, "SIGKILL"); } catch {
|
|
357
|
+
try { child.kill("SIGKILL"); } catch { /* already dead */ }
|
|
358
|
+
}
|
|
359
|
+
} else {
|
|
360
|
+
try { child.kill("SIGKILL"); } catch { /* already dead */ }
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
// ---------------------------------------------------------------------------
|
|
365
|
+
// Public API
|
|
366
|
+
// ---------------------------------------------------------------------------
|
|
367
|
+
|
|
368
|
+
export interface StartResult {
|
|
369
|
+
ok: true;
|
|
370
|
+
id: string;
|
|
371
|
+
pid: number;
|
|
372
|
+
message: string;
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
export interface StartError {
|
|
376
|
+
ok: false;
|
|
377
|
+
message: string;
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
export function startTestWatch(args: TestWatchArgs): StartResult | StartError {
|
|
381
|
+
// Count only still-alive sessions.
|
|
382
|
+
for (const s of [...SESSIONS.values()]) {
|
|
383
|
+
if (s.stopped) SESSIONS.delete(s.id);
|
|
384
|
+
}
|
|
385
|
+
if (SESSIONS.size >= MAX_WATCH_SESSIONS) {
|
|
386
|
+
return {
|
|
387
|
+
ok: false,
|
|
388
|
+
message:
|
|
389
|
+
`ashlr__test watch: max ${MAX_WATCH_SESSIONS} concurrent watch sessions reached — ` +
|
|
390
|
+
`stop one via ashlr__bash_stop first.`,
|
|
391
|
+
};
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
// Ensure the persistence dir exists synchronously so the caller sees
|
|
395
|
+
// the file immediately (tests rely on this).
|
|
396
|
+
try { mkdirSync(WATCH_DIR, { recursive: true }); } catch { /* ignore */ }
|
|
397
|
+
|
|
398
|
+
const id = newId();
|
|
399
|
+
const session: WatchSession = {
|
|
400
|
+
id,
|
|
401
|
+
pid: -1,
|
|
402
|
+
command: args.command,
|
|
403
|
+
label: args.label,
|
|
404
|
+
cwd: args.cwd,
|
|
405
|
+
startedAt: Date.now(),
|
|
406
|
+
child: null,
|
|
407
|
+
stdout: "",
|
|
408
|
+
stderr: "",
|
|
409
|
+
offset: 0,
|
|
410
|
+
stderrOffset: 0,
|
|
411
|
+
cumulativeBytes: 0,
|
|
412
|
+
exitCode: null,
|
|
413
|
+
signal: null,
|
|
414
|
+
lastActivity: Date.now(),
|
|
415
|
+
lastRunAt: 0,
|
|
416
|
+
runCount: 0,
|
|
417
|
+
watcher: null,
|
|
418
|
+
pollTimer: null,
|
|
419
|
+
pollSnapshot: new Map(),
|
|
420
|
+
debounceTimer: null,
|
|
421
|
+
stopped: false,
|
|
422
|
+
dataEmitter: new Set(),
|
|
423
|
+
};
|
|
424
|
+
|
|
425
|
+
// Install watcher with debounce.
|
|
426
|
+
const onChange = (_path: string) => {
|
|
427
|
+
if (session.stopped) return;
|
|
428
|
+
if (session.debounceTimer) clearTimeout(session.debounceTimer);
|
|
429
|
+
session.debounceTimer = setTimeout(() => {
|
|
430
|
+
session.debounceTimer = null;
|
|
431
|
+
spawnTestRun(session);
|
|
432
|
+
}, DEBOUNCE_MS);
|
|
433
|
+
};
|
|
434
|
+
const { watcher, pollTimer, pollSnapshot } = installWatcher(session, onChange);
|
|
435
|
+
session.watcher = watcher;
|
|
436
|
+
session.pollTimer = pollTimer;
|
|
437
|
+
session.pollSnapshot = pollSnapshot;
|
|
438
|
+
|
|
439
|
+
SESSIONS.set(id, session);
|
|
440
|
+
|
|
441
|
+
// Persist synchronously-ish (fire-and-forget; file exists shortly after).
|
|
442
|
+
void persistSession(session);
|
|
443
|
+
|
|
444
|
+
// Kick off the initial run.
|
|
445
|
+
spawnTestRun(session);
|
|
446
|
+
|
|
447
|
+
return {
|
|
448
|
+
ok: true,
|
|
449
|
+
id,
|
|
450
|
+
pid: session.pid,
|
|
451
|
+
message:
|
|
452
|
+
`[ashlr__test watch] started · id=${id} · watching ${args.cwd} · ` +
|
|
453
|
+
`use ashlr__bash_tail id=${id} to stream output, ashlr__bash_stop id=${id} to stop.`,
|
|
454
|
+
};
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
function stopTestWatchInternal(session: WatchSession): void {
|
|
458
|
+
if (session.stopped) return;
|
|
459
|
+
session.stopped = true;
|
|
460
|
+
if (session.debounceTimer) {
|
|
461
|
+
clearTimeout(session.debounceTimer);
|
|
462
|
+
session.debounceTimer = null;
|
|
463
|
+
}
|
|
464
|
+
if (session.watcher) {
|
|
465
|
+
try { session.watcher.close(); } catch { /* ignore */ }
|
|
466
|
+
session.watcher = null;
|
|
467
|
+
}
|
|
468
|
+
if (session.pollTimer) {
|
|
469
|
+
clearInterval(session.pollTimer);
|
|
470
|
+
session.pollTimer = null;
|
|
471
|
+
}
|
|
472
|
+
if (session.child && session.exitCode === null) {
|
|
473
|
+
killChild(session.child);
|
|
474
|
+
}
|
|
475
|
+
SESSIONS.delete(session.id);
|
|
476
|
+
void unpersistSession(session.id);
|
|
477
|
+
for (const fn of [...session.dataEmitter]) fn();
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
export function stopTestWatch(id: string, _signal?: NodeJS.Signals): string | null {
|
|
481
|
+
const s = SESSIONS.get(id);
|
|
482
|
+
if (!s) return null;
|
|
483
|
+
const label = s.label;
|
|
484
|
+
stopTestWatchInternal(s);
|
|
485
|
+
return `[${id}] test-watch stopped · ${label}`;
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
export function getTestWatchSession(id: string): WatchSession | undefined {
|
|
489
|
+
return SESSIONS.get(id);
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
// ---------------------------------------------------------------------------
|
|
493
|
+
// Tail adapter — mirrors ashlr__bash_tail's semantics so the same tool works
|
|
494
|
+
// ---------------------------------------------------------------------------
|
|
495
|
+
|
|
496
|
+
export async function tailTestWatch(
|
|
497
|
+
id: string,
|
|
498
|
+
waitMs: number,
|
|
499
|
+
_maxBytes: number,
|
|
500
|
+
): Promise<string | null> {
|
|
501
|
+
const s = SESSIONS.get(id);
|
|
502
|
+
if (!s) return null;
|
|
503
|
+
|
|
504
|
+
const hasNew = (): boolean =>
|
|
505
|
+
s.stdout.length > s.offset ||
|
|
506
|
+
s.stderr.length > s.stderrOffset ||
|
|
507
|
+
s.stopped;
|
|
508
|
+
|
|
509
|
+
if (!hasNew() && waitMs > 0) {
|
|
510
|
+
await new Promise<void>((resolve) => {
|
|
511
|
+
let done = false;
|
|
512
|
+
const finish = () => {
|
|
513
|
+
if (done) return;
|
|
514
|
+
done = true;
|
|
515
|
+
s.dataEmitter.delete(notify);
|
|
516
|
+
clearTimeout(t);
|
|
517
|
+
resolve();
|
|
518
|
+
};
|
|
519
|
+
const notify = () => finish();
|
|
520
|
+
const t = setTimeout(finish, waitMs);
|
|
521
|
+
s.dataEmitter.add(notify);
|
|
522
|
+
if (hasNew()) finish();
|
|
523
|
+
});
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
const newStdout = s.stdout.slice(s.offset);
|
|
527
|
+
const newStderr = s.stderr.slice(s.stderrOffset);
|
|
528
|
+
const newBytes = newStdout.length;
|
|
529
|
+
s.offset = s.stdout.length;
|
|
530
|
+
s.stderrOffset = s.stderr.length;
|
|
531
|
+
s.lastActivity = Date.now();
|
|
532
|
+
|
|
533
|
+
const statusLabel = s.stopped
|
|
534
|
+
? "stopped"
|
|
535
|
+
: s.child
|
|
536
|
+
? `running (run #${s.runCount})`
|
|
537
|
+
: `idle (last run #${s.runCount} exit ${s.exitCode ?? "?"})`;
|
|
538
|
+
|
|
539
|
+
const header = `[${id} · ${statusLabel}] +${newBytes} bytes since last poll`;
|
|
540
|
+
const stderrBlock = newStderr.length > 0 ? `\n--- stderr ---\n${newStderr}` : "";
|
|
541
|
+
const body = newStdout;
|
|
542
|
+
|
|
543
|
+
return `${header}\n${body}${body.endsWith("\n") || body.length === 0 ? "" : "\n"}${stderrBlock}`;
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
// ---------------------------------------------------------------------------
|
|
547
|
+
// bash-server integration — expose test-watch sessions to ashlr__bash_list
|
|
548
|
+
// and to ashlr__bash_stop / ashlr__bash_tail (so one tool surface manages
|
|
549
|
+
// both kinds of background work).
|
|
550
|
+
// ---------------------------------------------------------------------------
|
|
551
|
+
|
|
552
|
+
function toExternalSession(s: WatchSession): ExternalSession {
|
|
553
|
+
return {
|
|
554
|
+
id: s.id,
|
|
555
|
+
pid: s.pid > 0 ? s.pid : -1,
|
|
556
|
+
command: `[test-watch] ${s.label}`,
|
|
557
|
+
cwd: s.cwd,
|
|
558
|
+
startedAt: s.startedAt,
|
|
559
|
+
cumulativeBytes: s.cumulativeBytes,
|
|
560
|
+
kind: "test-watch",
|
|
561
|
+
};
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
registerExternalSessions({
|
|
565
|
+
kind: "test-watch",
|
|
566
|
+
list(): ExternalSession[] {
|
|
567
|
+
return [...SESSIONS.values()].map(toExternalSession);
|
|
568
|
+
},
|
|
569
|
+
tail(id, waitMs, maxBytes): Promise<string | null> {
|
|
570
|
+
return tailTestWatch(id, waitMs, maxBytes);
|
|
571
|
+
},
|
|
572
|
+
stop(id, signal): string | null {
|
|
573
|
+
return stopTestWatch(id, signal);
|
|
574
|
+
},
|
|
575
|
+
has(id): boolean {
|
|
576
|
+
return SESSIONS.has(id);
|
|
577
|
+
},
|
|
578
|
+
});
|
|
579
|
+
|
|
580
|
+
// ---------------------------------------------------------------------------
|
|
581
|
+
// Parent-exit cleanup — kill every watcher child and clean up JSON files so
|
|
582
|
+
// a future run sees a clean slate.
|
|
583
|
+
// ---------------------------------------------------------------------------
|
|
584
|
+
|
|
585
|
+
let exitHandlerInstalled = false;
|
|
586
|
+
function installExitHandler(): void {
|
|
587
|
+
if (exitHandlerInstalled) return;
|
|
588
|
+
exitHandlerInstalled = true;
|
|
589
|
+
const cleanup = () => {
|
|
590
|
+
for (const s of [...SESSIONS.values()]) {
|
|
591
|
+
try { stopTestWatchInternal(s); } catch { /* ignore */ }
|
|
592
|
+
try {
|
|
593
|
+
// Synchronous unlink on exit — async won't complete.
|
|
594
|
+
unlinkSync(sessionFile(s.id));
|
|
595
|
+
} catch { /* already gone */ }
|
|
596
|
+
// Also synchronously write a best-effort tombstone so cwd-clamp
|
|
597
|
+
// tests can observe final state.
|
|
598
|
+
try {
|
|
599
|
+
writeFileSync(
|
|
600
|
+
sessionFile(s.id) + ".tombstone",
|
|
601
|
+
JSON.stringify({ id: s.id, exitedAt: Date.now() }),
|
|
602
|
+
);
|
|
603
|
+
unlinkSync(sessionFile(s.id) + ".tombstone");
|
|
604
|
+
} catch { /* ignore */ }
|
|
605
|
+
}
|
|
606
|
+
};
|
|
607
|
+
process.on("exit", cleanup);
|
|
608
|
+
process.on("SIGINT", () => { cleanup(); process.exit(130); });
|
|
609
|
+
process.on("SIGTERM", () => { cleanup(); process.exit(143); });
|
|
610
|
+
}
|
|
611
|
+
installExitHandler();
|
|
612
|
+
|
|
613
|
+
// Opportunistic prune at module load (best-effort).
|
|
614
|
+
void reloadWatchSessions();
|
|
615
|
+
|
|
616
|
+
// Exposed for tests.
|
|
617
|
+
export function __activeWatchSessionsForTests(): number {
|
|
618
|
+
return SESSIONS.size;
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
export function __clearWatchSessionsForTests(): void {
|
|
622
|
+
for (const s of [...SESSIONS.values()]) {
|
|
623
|
+
stopTestWatchInternal(s);
|
|
624
|
+
}
|
|
625
|
+
SESSIONS.clear();
|
|
626
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* _text-helpers — pure string utilities shared across servers.
|
|
3
|
+
*
|
|
4
|
+
* Kept dependency-free so other `_*`-prefixed modules (especially the
|
|
5
|
+
* router-bound `_ask-router.ts`) can import them without pulling a whole
|
|
6
|
+
* tool-server module.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
// Minimal English stopword list — enough to filter "how does auth work" → ["auth"].
|
|
10
|
+
export const STOPWORDS: ReadonlySet<string> = new Set([
|
|
11
|
+
"the","and","for","are","but","not","you","all","can","her","was","one",
|
|
12
|
+
"our","out","his","who","its","how","what","where","when","why","does",
|
|
13
|
+
"did","done","this","that","those","these","here","there","with","from",
|
|
14
|
+
"into","onto","your","yours","have","has","had","been","being","work",
|
|
15
|
+
"works","working","about","show","tell","explain","please","over","some",
|
|
16
|
+
"such","than","then","them","they","thing","things","stuff","use","used",
|
|
17
|
+
"using","code","file","files","find","look","see","want","need","get",
|
|
18
|
+
]);
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Tokenize a natural-language query into content keywords.
|
|
22
|
+
*
|
|
23
|
+
* - Lowercases, splits on non-alnum/underscore boundaries.
|
|
24
|
+
* - Drops tokens ≤3 chars (avoids pronouns + tool noise).
|
|
25
|
+
* - Drops {@link STOPWORDS}.
|
|
26
|
+
* - Deduplicates, preserving first-occurrence order.
|
|
27
|
+
*/
|
|
28
|
+
export function extractKeywords(query: string): string[] {
|
|
29
|
+
const tokens = query
|
|
30
|
+
.toLowerCase()
|
|
31
|
+
.split(/[^a-z0-9_]+/)
|
|
32
|
+
.filter((t) => t.length > 3 && !STOPWORDS.has(t));
|
|
33
|
+
const seen = new Set<string>();
|
|
34
|
+
const out: string[] = [];
|
|
35
|
+
for (const t of tokens) if (!seen.has(t)) { seen.add(t); out.push(t); }
|
|
36
|
+
return out;
|
|
37
|
+
}
|