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,992 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
/**
|
|
3
|
+
* ashlr doctor — single-shot diagnostics for an ashlr-plugin install.
|
|
4
|
+
*
|
|
5
|
+
* Designed to run in under 10 seconds and produce an output block that's
|
|
6
|
+
* safe to paste into a GitHub issue. Every ⚠ / ✗ line carries a one-line
|
|
7
|
+
* copy-pastable fix.
|
|
8
|
+
*
|
|
9
|
+
* Exported helpers are consumed by __tests__/doctor.test.ts — keep their
|
|
10
|
+
* signatures stable.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { spawn } from "bun";
|
|
14
|
+
import { existsSync, statSync } from "fs";
|
|
15
|
+
import { readFile } from "fs/promises";
|
|
16
|
+
import { devNull, homedir } from "os";
|
|
17
|
+
import { dirname, join, resolve } from "path";
|
|
18
|
+
import { fileURLToPath } from "url";
|
|
19
|
+
import { c, sym, box, isColorEnabled } from "./ui.ts";
|
|
20
|
+
import { bunBinaryOnDisk } from "./bun-resolve.mjs";
|
|
21
|
+
import { isModelPresent, modelDir, modelSizeBytes } from "../servers/_llm-providers/onnx.ts";
|
|
22
|
+
import { selectProvider } from "../servers/_llm-providers/index.ts";
|
|
23
|
+
import { proTokenCachePath, isProAssumeEnabled } from "../servers/_pro.ts";
|
|
24
|
+
import { getDoctorHookHealth } from "./hook-timings-report.ts";
|
|
25
|
+
|
|
26
|
+
export type Status = "ok" | "warn" | "fail";
|
|
27
|
+
export interface Line {
|
|
28
|
+
status: Status;
|
|
29
|
+
label: string;
|
|
30
|
+
detail: string;
|
|
31
|
+
fix?: string;
|
|
32
|
+
}
|
|
33
|
+
export interface Section {
|
|
34
|
+
title: string;
|
|
35
|
+
lines: Line[];
|
|
36
|
+
}
|
|
37
|
+
export interface Report {
|
|
38
|
+
header: string;
|
|
39
|
+
sections: Section[];
|
|
40
|
+
warnings: number;
|
|
41
|
+
failures: number;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const GLYPH: Record<Status, string> = { ok: sym.check, warn: sym.warn, fail: sym.cross };
|
|
45
|
+
|
|
46
|
+
/** Colored glyph for TTY rendering; falls back to the plain glyph otherwise. */
|
|
47
|
+
function coloredGlyph(status: Status): string {
|
|
48
|
+
const g = GLYPH[status];
|
|
49
|
+
if (!isColorEnabled()) return g;
|
|
50
|
+
if (status === "ok") return c.green(g);
|
|
51
|
+
if (status === "warn") return c.yellow(g);
|
|
52
|
+
return c.red(g);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// ---------- plugin root resolution ----------
|
|
56
|
+
|
|
57
|
+
export function resolvePluginRoot(startDir: string, env = process.env): string | null {
|
|
58
|
+
if (env.CLAUDE_PLUGIN_ROOT && existsSync(join(env.CLAUDE_PLUGIN_ROOT, ".claude-plugin/plugin.json"))) {
|
|
59
|
+
return env.CLAUDE_PLUGIN_ROOT;
|
|
60
|
+
}
|
|
61
|
+
let dir = resolve(startDir);
|
|
62
|
+
for (let i = 0; i < 10; i++) {
|
|
63
|
+
if (existsSync(join(dir, ".claude-plugin/plugin.json"))) return dir;
|
|
64
|
+
const parent = dirname(dir);
|
|
65
|
+
if (parent === dir) break;
|
|
66
|
+
dir = parent;
|
|
67
|
+
}
|
|
68
|
+
return null;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// ---------- plugin.json ----------
|
|
72
|
+
|
|
73
|
+
async function readPluginJson(root: string): Promise<{ name?: string; version?: string } | null> {
|
|
74
|
+
try {
|
|
75
|
+
return JSON.parse(await readFile(join(root, ".claude-plugin/plugin.json"), "utf8"));
|
|
76
|
+
} catch {
|
|
77
|
+
return null;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// ---------- latest release ----------
|
|
82
|
+
|
|
83
|
+
export async function fetchLatestRelease(
|
|
84
|
+
url = "https://api.github.com/repos/ashlrai/ashlr-plugin/releases/latest",
|
|
85
|
+
timeoutMs = 3000,
|
|
86
|
+
fetchImpl: typeof fetch = fetch,
|
|
87
|
+
): Promise<string | null> {
|
|
88
|
+
const ctrl = new AbortController();
|
|
89
|
+
const t = setTimeout(() => ctrl.abort(), timeoutMs);
|
|
90
|
+
try {
|
|
91
|
+
const res = await fetchImpl(url, { signal: ctrl.signal, headers: { "user-agent": "ashlr-doctor" } });
|
|
92
|
+
if (!res.ok) return null;
|
|
93
|
+
const j = (await res.json()) as { tag_name?: string; name?: string };
|
|
94
|
+
return (j.tag_name || j.name || "").replace(/^v/, "") || null;
|
|
95
|
+
} catch {
|
|
96
|
+
return null;
|
|
97
|
+
} finally {
|
|
98
|
+
clearTimeout(t);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// ---------- MCP probe ----------
|
|
103
|
+
|
|
104
|
+
export interface ProbeResult {
|
|
105
|
+
server: string;
|
|
106
|
+
ok: boolean;
|
|
107
|
+
tools: string[];
|
|
108
|
+
error?: string;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
export async function probeServer(
|
|
112
|
+
name: string,
|
|
113
|
+
scriptPath: string,
|
|
114
|
+
perServerTimeoutMs = 2000,
|
|
115
|
+
): Promise<ProbeResult> {
|
|
116
|
+
if (!existsSync(scriptPath)) {
|
|
117
|
+
return { server: name, ok: false, tools: [], error: "server script missing" };
|
|
118
|
+
}
|
|
119
|
+
const initReq = {
|
|
120
|
+
jsonrpc: "2.0",
|
|
121
|
+
id: 1,
|
|
122
|
+
method: "initialize",
|
|
123
|
+
params: { protocolVersion: "2024-11-05", capabilities: {}, clientInfo: { name: "ashlr-doctor", version: "1" } },
|
|
124
|
+
};
|
|
125
|
+
const listReq = { jsonrpc: "2.0", id: 2, method: "tools/list", params: {} };
|
|
126
|
+
const payload = JSON.stringify(initReq) + "\n" + JSON.stringify(listReq) + "\n";
|
|
127
|
+
|
|
128
|
+
const proc = spawn({
|
|
129
|
+
cmd: ["bun", "run", scriptPath],
|
|
130
|
+
stdin: "pipe",
|
|
131
|
+
stdout: "pipe",
|
|
132
|
+
stderr: "pipe",
|
|
133
|
+
env: { ...process.env },
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
const timer = setTimeout(() => {
|
|
137
|
+
try { proc.kill(); } catch {}
|
|
138
|
+
}, perServerTimeoutMs);
|
|
139
|
+
|
|
140
|
+
try {
|
|
141
|
+
proc.stdin.write(payload);
|
|
142
|
+
await proc.stdin.end();
|
|
143
|
+
} catch {
|
|
144
|
+
// proc may already be dead
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
let out = "";
|
|
148
|
+
try {
|
|
149
|
+
out = await new Response(proc.stdout).text();
|
|
150
|
+
} catch {
|
|
151
|
+
// stream errored
|
|
152
|
+
}
|
|
153
|
+
// best-effort: we've already captured stdout and only need the process to settle; exit-code and signal reasons are not used in the probe.
|
|
154
|
+
await proc.exited.catch(() => {});
|
|
155
|
+
clearTimeout(timer);
|
|
156
|
+
|
|
157
|
+
const tools: string[] = [];
|
|
158
|
+
let sawListResult = false;
|
|
159
|
+
for (const line of out.split("\n")) {
|
|
160
|
+
const trimmed = line.trim();
|
|
161
|
+
if (!trimmed) continue;
|
|
162
|
+
try {
|
|
163
|
+
const msg = JSON.parse(trimmed);
|
|
164
|
+
if (msg.id === 2 && msg.result && Array.isArray(msg.result.tools)) {
|
|
165
|
+
sawListResult = true;
|
|
166
|
+
for (const t of msg.result.tools) if (t && t.name) tools.push(String(t.name));
|
|
167
|
+
}
|
|
168
|
+
} catch {
|
|
169
|
+
// non-JSON log line
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
if (!sawListResult) return { server: name, ok: false, tools: [], error: "no tools/list response" };
|
|
174
|
+
return { server: name, ok: true, tools };
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
export async function probeAll(
|
|
178
|
+
servers: Array<{ name: string; script: string }>,
|
|
179
|
+
totalCapMs = 5000,
|
|
180
|
+
perServerTimeoutMs = 2000,
|
|
181
|
+
): Promise<ProbeResult[]> {
|
|
182
|
+
const work = Promise.all(servers.map((s) => probeServer(s.name, s.script, perServerTimeoutMs)));
|
|
183
|
+
const cap = new Promise<ProbeResult[]>((resolveP) =>
|
|
184
|
+
setTimeout(
|
|
185
|
+
() =>
|
|
186
|
+
resolveP(
|
|
187
|
+
servers.map((s) => ({ server: s.name, ok: false, tools: [], error: "total probe cap exceeded" })),
|
|
188
|
+
),
|
|
189
|
+
totalCapMs,
|
|
190
|
+
),
|
|
191
|
+
);
|
|
192
|
+
return await Promise.race([work, cap]);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// ---------- project hint check ----------
|
|
196
|
+
|
|
197
|
+
const PROJECT_HINT_STALE_MS = 24 * 60 * 60 * 1000;
|
|
198
|
+
|
|
199
|
+
export interface ProjectHintResult {
|
|
200
|
+
state: "ok" | "stale" | "missing" | "invalid";
|
|
201
|
+
projectDir?: string;
|
|
202
|
+
updatedAt?: string;
|
|
203
|
+
/** ms since updatedAt; undefined when not parseable */
|
|
204
|
+
ageMs?: number;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* Inspect `~/.ashlr/last-project.json` and return a structured result.
|
|
209
|
+
* Never throws — any parse / stat failure maps to "missing" or "invalid".
|
|
210
|
+
*
|
|
211
|
+
* Mirrors the logic in servers/_cwd-clamp.ts:_readLastProjectHintForTest but
|
|
212
|
+
* is intentionally self-contained here so scripts/ doesn't import from servers/.
|
|
213
|
+
*/
|
|
214
|
+
export function checkProjectHint(home: string): ProjectHintResult {
|
|
215
|
+
const hintPath = join(home, ".ashlr", "last-project.json");
|
|
216
|
+
let raw: string;
|
|
217
|
+
try {
|
|
218
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
219
|
+
raw = (require("fs") as typeof import("fs")).readFileSync(hintPath, "utf-8");
|
|
220
|
+
} catch {
|
|
221
|
+
return { state: "missing" };
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
let parsed: { projectDir?: unknown; updatedAt?: unknown };
|
|
225
|
+
try {
|
|
226
|
+
parsed = JSON.parse(raw) as { projectDir?: unknown; updatedAt?: unknown };
|
|
227
|
+
} catch {
|
|
228
|
+
return { state: "missing" };
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
const projectDir = typeof parsed.projectDir === "string" && parsed.projectDir ? parsed.projectDir : undefined;
|
|
232
|
+
const updatedAtRaw = typeof parsed.updatedAt === "string" ? parsed.updatedAt : undefined;
|
|
233
|
+
|
|
234
|
+
if (!projectDir) return { state: "missing" };
|
|
235
|
+
|
|
236
|
+
// Check whether the directory currently exists.
|
|
237
|
+
let dirExists = false;
|
|
238
|
+
try {
|
|
239
|
+
const st = (require("fs") as typeof import("fs")).statSync(projectDir);
|
|
240
|
+
dirExists = st.isDirectory();
|
|
241
|
+
} catch {
|
|
242
|
+
dirExists = false;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
if (!dirExists) return { state: "invalid", projectDir, updatedAt: updatedAtRaw };
|
|
246
|
+
|
|
247
|
+
const parsedAge = updatedAtRaw ? Date.now() - Date.parse(updatedAtRaw) : NaN;
|
|
248
|
+
const ageMs = Number.isFinite(parsedAge) ? parsedAge : undefined;
|
|
249
|
+
|
|
250
|
+
if (ageMs === undefined || ageMs > PROJECT_HINT_STALE_MS) {
|
|
251
|
+
return { state: "stale", projectDir, updatedAt: updatedAtRaw, ageMs };
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
return { state: "ok", projectDir, updatedAt: updatedAtRaw, ageMs };
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
// ---------- helpers ----------
|
|
258
|
+
|
|
259
|
+
async function readJson<T = any>(p: string): Promise<T | null> {
|
|
260
|
+
try {
|
|
261
|
+
return JSON.parse(await readFile(p, "utf8"));
|
|
262
|
+
} catch {
|
|
263
|
+
return null;
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
function versionCompare(a: string, b: string): number {
|
|
268
|
+
const pa = a.split(".").map((n) => parseInt(n, 10) || 0);
|
|
269
|
+
const pb = b.split(".").map((n) => parseInt(n, 10) || 0);
|
|
270
|
+
for (let i = 0; i < Math.max(pa.length, pb.length); i++) {
|
|
271
|
+
const d = (pa[i] ?? 0) - (pb[i] ?? 0);
|
|
272
|
+
if (d !== 0) return d > 0 ? 1 : -1;
|
|
273
|
+
}
|
|
274
|
+
return 0;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
function isExecutable(path: string): boolean {
|
|
278
|
+
// NTFS has no POSIX execute bit — statSync().mode & 0o100 always returns 0
|
|
279
|
+
// on Windows, which would make doctor report every hook as "not executable"
|
|
280
|
+
// and print meaningless `chmod +x` advice. Skip the check on win32 and
|
|
281
|
+
// assume executable if the file exists.
|
|
282
|
+
if (process.platform === "win32") {
|
|
283
|
+
return existsSync(path);
|
|
284
|
+
}
|
|
285
|
+
try {
|
|
286
|
+
const st = statSync(path);
|
|
287
|
+
// owner-execute bit
|
|
288
|
+
return (st.mode & 0o100) !== 0;
|
|
289
|
+
} catch {
|
|
290
|
+
return false;
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
// ---------- build report ----------
|
|
295
|
+
|
|
296
|
+
export interface CloudPingResult {
|
|
297
|
+
ok: boolean;
|
|
298
|
+
ms: number;
|
|
299
|
+
error?: string;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
export interface BuildOpts {
|
|
303
|
+
root: string;
|
|
304
|
+
home?: string;
|
|
305
|
+
cwd?: string;
|
|
306
|
+
claudeSettingsPath?: string;
|
|
307
|
+
statsPath?: string;
|
|
308
|
+
fetchLatest?: () => Promise<string | null>;
|
|
309
|
+
probe?: (servers: Array<{ name: string; script: string }>) => Promise<ProbeResult[]>;
|
|
310
|
+
bunVersion?: () => Promise<string | null>;
|
|
311
|
+
bunOffPath?: () => Promise<string | null>;
|
|
312
|
+
/** Override for testing cloud ping without real network calls */
|
|
313
|
+
pingCloud?: (url: string, timeoutMs: number) => Promise<CloudPingResult>;
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
/** Returns true if the allow array contains at least one ashlr MCP wildcard. */
|
|
317
|
+
export function hasAshlrAllowEntry(allow: unknown): boolean {
|
|
318
|
+
if (!Array.isArray(allow)) return false;
|
|
319
|
+
return (allow as string[]).some((e) => /^mcp__ashlr(-|__)/.test(e) || e === "mcp__ashlr-*");
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
export async function pingCloud(
|
|
323
|
+
apiUrl: string,
|
|
324
|
+
timeoutMs = 3000,
|
|
325
|
+
fetchImpl: typeof fetch = fetch,
|
|
326
|
+
): Promise<CloudPingResult> {
|
|
327
|
+
const start = Date.now();
|
|
328
|
+
const ctrl = new AbortController();
|
|
329
|
+
const t = setTimeout(() => ctrl.abort(), timeoutMs);
|
|
330
|
+
try {
|
|
331
|
+
const res = await fetchImpl(`${apiUrl}/healthz`, {
|
|
332
|
+
signal: ctrl.signal,
|
|
333
|
+
headers: { "user-agent": "ashlr-doctor" },
|
|
334
|
+
});
|
|
335
|
+
const ms = Date.now() - start;
|
|
336
|
+
if (!res.ok) return { ok: false, ms, error: `HTTP ${res.status}` };
|
|
337
|
+
return { ok: true, ms };
|
|
338
|
+
} catch (err: unknown) {
|
|
339
|
+
const ms = Date.now() - start;
|
|
340
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
341
|
+
const isTimeout = msg.includes("abort") || msg.includes("timeout") || msg.includes("signal");
|
|
342
|
+
return { ok: false, ms, error: isTimeout ? "timeout" : msg };
|
|
343
|
+
} finally {
|
|
344
|
+
clearTimeout(t);
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
async function getBunVersion(): Promise<string | null> {
|
|
349
|
+
try {
|
|
350
|
+
const proc = spawn({ cmd: ["bun", "--version"], stdout: "pipe", stderr: "pipe" });
|
|
351
|
+
const out = await new Response(proc.stdout).text();
|
|
352
|
+
await proc.exited;
|
|
353
|
+
const v = out.trim();
|
|
354
|
+
return v || null;
|
|
355
|
+
} catch {
|
|
356
|
+
return null;
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
/**
|
|
361
|
+
* Returns the absolute path to ~/.bun/bin/bun(.exe) if the binary is on disk,
|
|
362
|
+
* else null. Used to distinguish the "installed but the parent Claude Code
|
|
363
|
+
* session's PATH predates the install" case from "genuinely missing".
|
|
364
|
+
*/
|
|
365
|
+
async function getBunOffPath(): Promise<string | null> {
|
|
366
|
+
return bunBinaryOnDisk();
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
export async function buildReport(opts: BuildOpts): Promise<Report> {
|
|
370
|
+
const root = opts.root;
|
|
371
|
+
const home = opts.home ?? homedir();
|
|
372
|
+
const cwd = opts.cwd ?? process.cwd();
|
|
373
|
+
const settingsPath = opts.claudeSettingsPath ?? join(home, ".claude/settings.json");
|
|
374
|
+
const statsPath = opts.statsPath ?? join(home, ".ashlr/stats.json");
|
|
375
|
+
const fetchLatest = opts.fetchLatest ?? (() => fetchLatestRelease());
|
|
376
|
+
const probe = opts.probe ?? ((servers) => probeAll(servers));
|
|
377
|
+
const bunVersionFn = opts.bunVersion ?? getBunVersion;
|
|
378
|
+
const bunOffPathFn = opts.bunOffPath ?? getBunOffPath;
|
|
379
|
+
|
|
380
|
+
const plugin = await readPluginJson(root);
|
|
381
|
+
const currentVersion = plugin?.version ?? "unknown";
|
|
382
|
+
const [latestVersion, bunVersion, bunOffPath] = await Promise.all([
|
|
383
|
+
fetchLatest(),
|
|
384
|
+
bunVersionFn(),
|
|
385
|
+
bunOffPathFn(),
|
|
386
|
+
]);
|
|
387
|
+
|
|
388
|
+
let versionTag: string;
|
|
389
|
+
if (!latestVersion) versionTag = "latest: unknown";
|
|
390
|
+
else {
|
|
391
|
+
const cmp = versionCompare(currentVersion, latestVersion);
|
|
392
|
+
if (cmp === 0) versionTag = `latest: v${latestVersion} · up to date`;
|
|
393
|
+
else if (cmp < 0) versionTag = `latest: v${latestVersion} · update available`;
|
|
394
|
+
else versionTag = `latest: v${latestVersion} · ahead of release`;
|
|
395
|
+
}
|
|
396
|
+
const header = `ashlr doctor · plugin v${currentVersion} (${versionTag})`;
|
|
397
|
+
|
|
398
|
+
const sections: Section[] = [];
|
|
399
|
+
|
|
400
|
+
// ----- install -----
|
|
401
|
+
const install: Line[] = [];
|
|
402
|
+
install.push({ status: "ok", label: "plugin root", detail: root });
|
|
403
|
+
|
|
404
|
+
if (bunVersion) {
|
|
405
|
+
install.push({ status: "ok", label: "bun", detail: bunVersion });
|
|
406
|
+
} else if (bunOffPath) {
|
|
407
|
+
// Bun exists on disk but the parent Claude Code session's PATH predates
|
|
408
|
+
// the install. Ashlr's MCP server + hooks still work via their trampolines,
|
|
409
|
+
// which prepend ~/.bun/bin to the child process PATH at spawn time.
|
|
410
|
+
install.push({
|
|
411
|
+
status: "warn",
|
|
412
|
+
label: "bun",
|
|
413
|
+
detail: `installed at ${bunOffPath} but not on this session's PATH`,
|
|
414
|
+
fix: "ashlr hooks + MCP work via trampoline; restart Claude Code so other tools that call `bun` directly will see it too",
|
|
415
|
+
});
|
|
416
|
+
} else {
|
|
417
|
+
install.push({
|
|
418
|
+
status: "fail",
|
|
419
|
+
label: "bun",
|
|
420
|
+
detail: "not found on PATH",
|
|
421
|
+
fix: "install: curl -fsSL https://bun.sh/install | bash",
|
|
422
|
+
});
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
const mcpSdkPkg = await readJson<{ version?: string }>(
|
|
426
|
+
join(root, "node_modules/@modelcontextprotocol/sdk/package.json"),
|
|
427
|
+
);
|
|
428
|
+
const corePkg = await readJson<{ version?: string }>(
|
|
429
|
+
join(root, "node_modules/@ashlr/core-efficiency/package.json"),
|
|
430
|
+
);
|
|
431
|
+
if (!mcpSdkPkg || !corePkg) {
|
|
432
|
+
const missing: string[] = [];
|
|
433
|
+
if (!mcpSdkPkg) missing.push("@modelcontextprotocol/sdk");
|
|
434
|
+
if (!corePkg) missing.push("@ashlr/core-efficiency");
|
|
435
|
+
install.push({
|
|
436
|
+
status: "fail",
|
|
437
|
+
label: "node_modules",
|
|
438
|
+
detail: `missing: ${missing.join(", ")}`,
|
|
439
|
+
fix: `cd ${root} && bun install`,
|
|
440
|
+
});
|
|
441
|
+
} else {
|
|
442
|
+
install.push({
|
|
443
|
+
status: "ok",
|
|
444
|
+
label: "node_modules",
|
|
445
|
+
detail: `@modelcontextprotocol/sdk@${mcpSdkPkg.version ?? "?"} · @ashlr/core-efficiency@${corePkg.version ?? "?"}`,
|
|
446
|
+
});
|
|
447
|
+
}
|
|
448
|
+
sections.push({ title: "install", lines: install });
|
|
449
|
+
|
|
450
|
+
// ----- mcp servers -----
|
|
451
|
+
const servers = [
|
|
452
|
+
{ name: "efficiency", script: join(root, "servers/efficiency-server.ts") },
|
|
453
|
+
{ name: "sql", script: join(root, "servers/sql-server.ts") },
|
|
454
|
+
{ name: "bash", script: join(root, "servers/bash-server.ts") },
|
|
455
|
+
{ name: "tree", script: join(root, "servers/tree-server.ts") },
|
|
456
|
+
{ name: "http", script: join(root, "servers/http-server.ts") },
|
|
457
|
+
{ name: "diff", script: join(root, "servers/diff-server.ts") },
|
|
458
|
+
{ name: "logs", script: join(root, "servers/logs-server.ts") },
|
|
459
|
+
{ name: "genome", script: join(root, "servers/genome-server.ts") },
|
|
460
|
+
];
|
|
461
|
+
const mcpLines: Line[] = [];
|
|
462
|
+
const results = await probe(servers);
|
|
463
|
+
for (const r of results) {
|
|
464
|
+
if (r.ok) {
|
|
465
|
+
const count = r.tools.length;
|
|
466
|
+
mcpLines.push({
|
|
467
|
+
status: "ok",
|
|
468
|
+
label: r.server,
|
|
469
|
+
detail: `${count} tool${count === 1 ? "" : "s"}: ${r.tools.join(", ") || "(none)"}`,
|
|
470
|
+
});
|
|
471
|
+
} else {
|
|
472
|
+
mcpLines.push({
|
|
473
|
+
status: "fail",
|
|
474
|
+
label: r.server,
|
|
475
|
+
detail: `unreachable (${r.error ?? "unknown error"})`,
|
|
476
|
+
fix: `bun run ${join(root, `servers/${r.server === "efficiency" ? "efficiency-server" : r.server + "-server"}.ts`)} < ${devNull} # check startup errors`,
|
|
477
|
+
});
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
sections.push({ title: "mcp servers", lines: mcpLines });
|
|
481
|
+
|
|
482
|
+
// ----- runtime state -----
|
|
483
|
+
const runtime: Line[] = [];
|
|
484
|
+
|
|
485
|
+
// stats
|
|
486
|
+
let statsLine: Line;
|
|
487
|
+
if (!existsSync(statsPath)) {
|
|
488
|
+
statsLine = {
|
|
489
|
+
status: "warn",
|
|
490
|
+
label: "stats.json",
|
|
491
|
+
detail: "not found (will be created on first tool call)",
|
|
492
|
+
fix: `mkdir -p ${dirname(statsPath)} # harmless; created automatically`,
|
|
493
|
+
};
|
|
494
|
+
} else {
|
|
495
|
+
const stats = await readJson<any>(statsPath);
|
|
496
|
+
if (!stats) {
|
|
497
|
+
statsLine = {
|
|
498
|
+
status: "warn",
|
|
499
|
+
label: "stats.json",
|
|
500
|
+
detail: "unreadable — file exists but won't parse",
|
|
501
|
+
fix: `mv ${statsPath} ${statsPath}.corrupt # preserves the file; a fresh one is created on next call`,
|
|
502
|
+
};
|
|
503
|
+
} else {
|
|
504
|
+
const life = stats.lifetime ?? {};
|
|
505
|
+
const sess = stats.session ?? {};
|
|
506
|
+
const calls = life.calls ?? sess.calls ?? 0;
|
|
507
|
+
const saved = life.tokensSaved ?? sess.tokensSaved ?? 0;
|
|
508
|
+
statsLine = {
|
|
509
|
+
status: "ok",
|
|
510
|
+
label: "stats.json",
|
|
511
|
+
detail: `${calls.toLocaleString()} lifetime calls · ${saved.toLocaleString()} tokens saved`,
|
|
512
|
+
};
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
runtime.push(statsLine);
|
|
516
|
+
|
|
517
|
+
// genome
|
|
518
|
+
const genomeDir = join(cwd, ".ashlrcode/genome");
|
|
519
|
+
if (existsSync(genomeDir)) {
|
|
520
|
+
runtime.push({ status: "ok", label: "genome", detail: `found at ${genomeDir}` });
|
|
521
|
+
|
|
522
|
+
// genome manifest freshness — warn if >48h stale so retrieval quality
|
|
523
|
+
// doesn't silently degrade. Missing manifest is a soft skip (the genome
|
|
524
|
+
// dir exists but may be mid-init or partial).
|
|
525
|
+
const manifestPath = join(genomeDir, "manifest.json");
|
|
526
|
+
if (existsSync(manifestPath)) {
|
|
527
|
+
try {
|
|
528
|
+
const mtimeMs = statSync(manifestPath).mtimeMs;
|
|
529
|
+
const ageMs = Date.now() - mtimeMs;
|
|
530
|
+
const STALE_MS = 48 * 60 * 60 * 1000;
|
|
531
|
+
const ageHours = Math.round(ageMs / (60 * 60 * 1000));
|
|
532
|
+
if (ageMs > STALE_MS) {
|
|
533
|
+
runtime.push({
|
|
534
|
+
status: "warn",
|
|
535
|
+
label: "genome manifest",
|
|
536
|
+
detail: `stale — last refreshed ${ageHours}h ago (>48h); retrieval quality may degrade`,
|
|
537
|
+
fix: "run: bun run scripts/genome-refresh-worker.ts --full # or /ashlr-genome-init --force",
|
|
538
|
+
});
|
|
539
|
+
} else {
|
|
540
|
+
runtime.push({
|
|
541
|
+
status: "ok",
|
|
542
|
+
label: "genome manifest",
|
|
543
|
+
detail: `fresh — last refreshed ${ageHours}h ago`,
|
|
544
|
+
});
|
|
545
|
+
}
|
|
546
|
+
} catch {
|
|
547
|
+
// unreadable manifest — silent skip (existing genome check covers presence)
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
} else {
|
|
551
|
+
runtime.push({
|
|
552
|
+
status: "warn",
|
|
553
|
+
label: "genome",
|
|
554
|
+
detail: "no .ashlrcode/genome in cwd",
|
|
555
|
+
fix: "run: /ashlr-genome-init",
|
|
556
|
+
});
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
// settings
|
|
560
|
+
// Note: `toolRedirect` was retired along with hooks/tool-redirect.ts —
|
|
561
|
+
// the Read/Grep/Edit routing is now handled by pretooluse-{read,grep,edit}.ts
|
|
562
|
+
// and gated by `ASHLR_HOOK_MODE` / `~/.ashlr/settings.json` instead of the
|
|
563
|
+
// old `~/.claude/settings.json { ashlr: { toolRedirect } }` toggle.
|
|
564
|
+
const settings = existsSync(settingsPath) ? await readJson<any>(settingsPath) : null;
|
|
565
|
+
const ashlr = settings?.ashlr ?? {};
|
|
566
|
+
const attribution = ashlr.attribution ?? true;
|
|
567
|
+
const editBatchingNudge = ashlr.editBatchingNudge ?? true;
|
|
568
|
+
const toggles = `attribution:${attribution ? "on" : "off"} editBatchingNudge:${editBatchingNudge ? "on" : "off"}`;
|
|
569
|
+
if (!settings) {
|
|
570
|
+
runtime.push({
|
|
571
|
+
status: "warn",
|
|
572
|
+
label: "settings",
|
|
573
|
+
detail: `no ~/.claude/settings.json — using defaults (${toggles})`,
|
|
574
|
+
fix: "run: /ashlr-settings # creates and edits the file",
|
|
575
|
+
});
|
|
576
|
+
} else {
|
|
577
|
+
runtime.push({ status: "ok", label: "settings", detail: toggles });
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
// allowlist check
|
|
581
|
+
const allowList = settings?.permissions?.allow;
|
|
582
|
+
if (hasAshlrAllowEntry(allowList)) {
|
|
583
|
+
runtime.push({
|
|
584
|
+
status: "ok",
|
|
585
|
+
label: "allowlist",
|
|
586
|
+
detail: "ashlr MCP tools pre-approved in ~/.claude/settings.json",
|
|
587
|
+
});
|
|
588
|
+
} else {
|
|
589
|
+
runtime.push({
|
|
590
|
+
status: "fail",
|
|
591
|
+
label: "allowlist",
|
|
592
|
+
detail: "ashlr MCP tools not in allowlist — Claude Code will prompt on every ashlr__ call",
|
|
593
|
+
fix: "run: /ashlr-allow",
|
|
594
|
+
});
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
// status line
|
|
598
|
+
const statusLineCmd: string | undefined = settings?.statusLine?.command;
|
|
599
|
+
if (statusLineCmd && statusLineCmd.includes("savings-status-line.ts")) {
|
|
600
|
+
runtime.push({ status: "ok", label: "status line", detail: "savings-status-line.ts wired" });
|
|
601
|
+
} else {
|
|
602
|
+
runtime.push({
|
|
603
|
+
status: "warn",
|
|
604
|
+
label: "status line",
|
|
605
|
+
detail: "not installed",
|
|
606
|
+
fix: `bun run ${join(root, "scripts/install-status-line.ts")}`,
|
|
607
|
+
});
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
// pro status
|
|
611
|
+
{
|
|
612
|
+
const cachePath = proTokenCachePath(home);
|
|
613
|
+
const tokenPath = join(home, ".ashlr", "pro-token");
|
|
614
|
+
const tokenExists = existsSync(tokenPath);
|
|
615
|
+
|
|
616
|
+
// Surface the ASHLR_PRO_ASSUME offline / air-gapped override before the
|
|
617
|
+
// normal token+cache reporting so users can see why Pro features are
|
|
618
|
+
// active even without a valid token / fresh cache.
|
|
619
|
+
if (isProAssumeEnabled()) {
|
|
620
|
+
runtime.push({
|
|
621
|
+
status: "ok",
|
|
622
|
+
label: "pro status",
|
|
623
|
+
detail: "assumed (ASHLR_PRO_ASSUME=true) · offline / air-gapped override",
|
|
624
|
+
});
|
|
625
|
+
} else if (!tokenExists) {
|
|
626
|
+
runtime.push({
|
|
627
|
+
status: "warn",
|
|
628
|
+
label: "pro status",
|
|
629
|
+
detail: "not signed in",
|
|
630
|
+
fix: "run: /ashlr-upgrade",
|
|
631
|
+
});
|
|
632
|
+
} else {
|
|
633
|
+
let proLine: Line;
|
|
634
|
+
try {
|
|
635
|
+
const cacheRaw = await readFile(cachePath, "utf-8");
|
|
636
|
+
const cache = JSON.parse(cacheRaw) as {
|
|
637
|
+
valid?: boolean;
|
|
638
|
+
plan?: string | null;
|
|
639
|
+
trialEndsAt?: string | null;
|
|
640
|
+
validatedAt?: string;
|
|
641
|
+
};
|
|
642
|
+
const validatedAt = cache.validatedAt
|
|
643
|
+
? new Date(cache.validatedAt).toLocaleString()
|
|
644
|
+
: "unknown";
|
|
645
|
+
if (cache.valid) {
|
|
646
|
+
const planStr = cache.plan ?? "pro";
|
|
647
|
+
const trialStr = cache.trialEndsAt
|
|
648
|
+
? ` · trial ends ${new Date(cache.trialEndsAt).toLocaleDateString()}`
|
|
649
|
+
: "";
|
|
650
|
+
proLine = {
|
|
651
|
+
status: "ok",
|
|
652
|
+
label: "pro status",
|
|
653
|
+
detail: `plan: ${planStr}${trialStr} · last validated: ${validatedAt}`,
|
|
654
|
+
};
|
|
655
|
+
} else {
|
|
656
|
+
proLine = {
|
|
657
|
+
status: "warn",
|
|
658
|
+
label: "pro status",
|
|
659
|
+
detail: `token present but plan is free · last validated: ${validatedAt}`,
|
|
660
|
+
fix: "run: /ashlr-upgrade to subscribe, or re-run to refresh validation",
|
|
661
|
+
};
|
|
662
|
+
}
|
|
663
|
+
} catch {
|
|
664
|
+
// Cache missing or unreadable — token exists but never validated
|
|
665
|
+
proLine = {
|
|
666
|
+
status: "warn",
|
|
667
|
+
label: "pro status",
|
|
668
|
+
detail: "token present, not yet validated (run a tool to trigger validation)",
|
|
669
|
+
fix: "run: /ashlr-upgrade to verify your subscription",
|
|
670
|
+
};
|
|
671
|
+
}
|
|
672
|
+
runtime.push(proLine);
|
|
673
|
+
}
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
// onnx model
|
|
677
|
+
let onnxLine: Line;
|
|
678
|
+
if (isModelPresent()) {
|
|
679
|
+
const sizeBytes = modelSizeBytes();
|
|
680
|
+
const sizeMB = sizeBytes != null ? ` · ${(sizeBytes / 1_000_000).toFixed(0)} MB` : "";
|
|
681
|
+
// Check if onnxruntime-node is also installed
|
|
682
|
+
let onnxRtInstalled = false;
|
|
683
|
+
try { require("onnxruntime-node"); onnxRtInstalled = true; } catch { /* not installed */ }
|
|
684
|
+
if (onnxRtInstalled) {
|
|
685
|
+
onnxLine = {
|
|
686
|
+
status: "ok",
|
|
687
|
+
label: "onnx model",
|
|
688
|
+
detail: `distilbart-cnn-6-6 present${sizeMB} · onnxruntime-node installed`,
|
|
689
|
+
};
|
|
690
|
+
} else {
|
|
691
|
+
onnxLine = {
|
|
692
|
+
status: "warn",
|
|
693
|
+
label: "onnx model",
|
|
694
|
+
detail: `distilbart-cnn-6-6 present${sizeMB} · onnxruntime-node NOT installed`,
|
|
695
|
+
fix: "bun add onnxruntime-node # or: npm install onnxruntime-node",
|
|
696
|
+
};
|
|
697
|
+
}
|
|
698
|
+
} else {
|
|
699
|
+
// Only warn (not fail) — most users with ANTHROPIC_API_KEY don't need ONNX.
|
|
700
|
+
const hasAnthropicKey =
|
|
701
|
+
!!(process.env.ANTHROPIC_API_KEY) ||
|
|
702
|
+
existsSync(join(home, ".claude", ".credentials.json"));
|
|
703
|
+
if (hasAnthropicKey) {
|
|
704
|
+
onnxLine = {
|
|
705
|
+
status: "ok",
|
|
706
|
+
label: "onnx model",
|
|
707
|
+
detail: `not installed · using Anthropic provider (${modelDir()} absent)`,
|
|
708
|
+
};
|
|
709
|
+
} else {
|
|
710
|
+
onnxLine = {
|
|
711
|
+
status: "warn",
|
|
712
|
+
label: "onnx model",
|
|
713
|
+
detail: `not installed · no Anthropic key found — offline summarization unavailable`,
|
|
714
|
+
fix: `bun run ${join(root, "scripts/install-onnx-model.ts")} # downloads ~300MB`,
|
|
715
|
+
};
|
|
716
|
+
}
|
|
717
|
+
}
|
|
718
|
+
runtime.push(onnxLine);
|
|
719
|
+
|
|
720
|
+
// project hint (cwd-clamp)
|
|
721
|
+
{
|
|
722
|
+
const hint = checkProjectHint(home);
|
|
723
|
+
let hintLine: Line;
|
|
724
|
+
switch (hint.state) {
|
|
725
|
+
case "ok": {
|
|
726
|
+
const ageH = hint.ageMs !== undefined ? ` · updated ${(hint.ageMs / 3600_000).toFixed(1)}h ago` : "";
|
|
727
|
+
hintLine = { status: "ok", label: "project hint", detail: `${hint.projectDir}${ageH}` };
|
|
728
|
+
break;
|
|
729
|
+
}
|
|
730
|
+
case "stale": {
|
|
731
|
+
const ageH = hint.ageMs !== undefined ? ` (${(hint.ageMs / 3600_000).toFixed(1)}h ago)` : "";
|
|
732
|
+
hintLine = {
|
|
733
|
+
status: "warn",
|
|
734
|
+
label: "project hint",
|
|
735
|
+
detail: `stale${ageH} · cwd-clamp only allows paths under ${hint.projectDir}; if you've moved to a different project this session, ashlr__* tool calls will be refused`,
|
|
736
|
+
fix: "restart Claude Code or run /ashlr-update to refresh the project hint",
|
|
737
|
+
};
|
|
738
|
+
break;
|
|
739
|
+
}
|
|
740
|
+
case "invalid":
|
|
741
|
+
hintLine = {
|
|
742
|
+
status: "warn",
|
|
743
|
+
label: "project hint",
|
|
744
|
+
detail: `projectDir no longer exists (${hint.projectDir}) — cwd-clamp falls back to MCP server cwd; all project paths refused`,
|
|
745
|
+
fix: "restart Claude Code so the SessionStart hook writes a fresh hint",
|
|
746
|
+
};
|
|
747
|
+
break;
|
|
748
|
+
case "missing":
|
|
749
|
+
default:
|
|
750
|
+
hintLine = {
|
|
751
|
+
status: "warn",
|
|
752
|
+
label: "project hint",
|
|
753
|
+
detail: "~/.ashlr/last-project.json absent — cwd-clamp falls back to MCP server cwd; ashlr__* calls to project paths will be refused until SessionStart hook fires",
|
|
754
|
+
fix: "restart Claude Code to trigger the SessionStart hook",
|
|
755
|
+
};
|
|
756
|
+
break;
|
|
757
|
+
}
|
|
758
|
+
runtime.push(hintLine);
|
|
759
|
+
}
|
|
760
|
+
|
|
761
|
+
sections.push({ title: "runtime state", lines: runtime });
|
|
762
|
+
|
|
763
|
+
// ----- cloud -----
|
|
764
|
+
{
|
|
765
|
+
const cloudLines: Line[] = [];
|
|
766
|
+
const apiUrl = process.env["ASHLR_API_URL"] ?? "https://api.ashlr.ai";
|
|
767
|
+
const disabled = process.env["ASHLR_API_URL_DISABLE"] === "1";
|
|
768
|
+
|
|
769
|
+
if (disabled) {
|
|
770
|
+
cloudLines.push({
|
|
771
|
+
status: "warn",
|
|
772
|
+
label: "cloud",
|
|
773
|
+
detail: "disabled (ASHLR_API_URL_DISABLE=1)",
|
|
774
|
+
});
|
|
775
|
+
} else {
|
|
776
|
+
const pingFn = opts.pingCloud ?? ((url, ms) => pingCloud(url, ms));
|
|
777
|
+
const result = await pingFn(apiUrl, 3000);
|
|
778
|
+
if (result.ok) {
|
|
779
|
+
cloudLines.push({
|
|
780
|
+
status: "ok",
|
|
781
|
+
label: "cloud",
|
|
782
|
+
detail: `healthy (${result.ms}ms) · ${apiUrl}`,
|
|
783
|
+
});
|
|
784
|
+
} else {
|
|
785
|
+
cloudLines.push({
|
|
786
|
+
status: "warn",
|
|
787
|
+
label: "cloud",
|
|
788
|
+
detail: `unreachable (${result.error ?? "error"}) · ${apiUrl}`,
|
|
789
|
+
fix: "set ASHLR_API_URL_DISABLE=1 to suppress, or check https://status.ashlr.ai",
|
|
790
|
+
});
|
|
791
|
+
}
|
|
792
|
+
}
|
|
793
|
+
sections.push({ title: "cloud", lines: cloudLines });
|
|
794
|
+
}
|
|
795
|
+
|
|
796
|
+
// ----- summarizer routing -----
|
|
797
|
+
// Surface which LLM provider would be selected for this user *right now*,
|
|
798
|
+
// so it's clear whether the cloud LLM (Pro) is in use, Anthropic-direct,
|
|
799
|
+
// ONNX, or local (LM Studio / Ollama). Falls back silently on any error so
|
|
800
|
+
// the doctor stays robust.
|
|
801
|
+
{
|
|
802
|
+
const routingLines: Line[] = [];
|
|
803
|
+
try {
|
|
804
|
+
const provider = await selectProvider();
|
|
805
|
+
const overrideEnv = process.env["ASHLR_LLM_PROVIDER"];
|
|
806
|
+
const overrideNote = overrideEnv ? ` (ASHLR_LLM_PROVIDER=${overrideEnv})` : "";
|
|
807
|
+
let detail: string;
|
|
808
|
+
let status: Status = "ok";
|
|
809
|
+
let fix: string | undefined;
|
|
810
|
+
switch (provider.name) {
|
|
811
|
+
case "anthropic":
|
|
812
|
+
detail = `provider: anthropic (your ANTHROPIC_API_KEY)${overrideNote}`;
|
|
813
|
+
break;
|
|
814
|
+
case "cloud":
|
|
815
|
+
detail = `provider: cloud (Pro · /llm/summarize)${overrideNote}`;
|
|
816
|
+
break;
|
|
817
|
+
case "onnx":
|
|
818
|
+
detail = `provider: onnx (offline summarizer)${overrideNote}`;
|
|
819
|
+
break;
|
|
820
|
+
case "local":
|
|
821
|
+
detail = `provider: local (${process.env.ASHLR_LLM_URL ?? "http://localhost:1234/v1"})${overrideNote}`;
|
|
822
|
+
break;
|
|
823
|
+
case "none":
|
|
824
|
+
default:
|
|
825
|
+
status = "warn";
|
|
826
|
+
detail = `provider: none — summaries fall back to byte-truncation${overrideNote}`;
|
|
827
|
+
fix = "run /ashlr-upgrade for cloud LLM (Pro), or /ashlr-ollama-setup for local";
|
|
828
|
+
break;
|
|
829
|
+
}
|
|
830
|
+
routingLines.push({ status, label: "summarizer", detail, ...(fix ? { fix } : {}) });
|
|
831
|
+
} catch {
|
|
832
|
+
routingLines.push({
|
|
833
|
+
status: "warn",
|
|
834
|
+
label: "summarizer",
|
|
835
|
+
detail: "provider selection failed — see logs",
|
|
836
|
+
});
|
|
837
|
+
}
|
|
838
|
+
sections.push({ title: "summarizer routing", lines: routingLines });
|
|
839
|
+
}
|
|
840
|
+
|
|
841
|
+
// ----- hooks -----
|
|
842
|
+
// tool-redirect.ts was retired in v1.18+: its functionality (nudge mode,
|
|
843
|
+
// kill-switch, Read/Grep/Edit messages) is now absorbed into
|
|
844
|
+
// pretooluse-{read,grep,edit}.ts via hooks/pretooluse-common.ts.
|
|
845
|
+
const hookFiles = [
|
|
846
|
+
"session-start.ts",
|
|
847
|
+
"pretooluse-read.ts",
|
|
848
|
+
"pretooluse-grep.ts",
|
|
849
|
+
"pretooluse-edit.ts",
|
|
850
|
+
"commit-attribution.ts",
|
|
851
|
+
"edit-batching-nudge.ts",
|
|
852
|
+
];
|
|
853
|
+
const hookLines: Line[] = [];
|
|
854
|
+
const notExec: string[] = [];
|
|
855
|
+
for (const h of hookFiles) {
|
|
856
|
+
const p = join(root, "hooks", h);
|
|
857
|
+
if (!existsSync(p)) {
|
|
858
|
+
hookLines.push({
|
|
859
|
+
status: "fail",
|
|
860
|
+
label: h,
|
|
861
|
+
detail: "missing",
|
|
862
|
+
fix: `cd ${root} && git checkout hooks/${h}`,
|
|
863
|
+
});
|
|
864
|
+
continue;
|
|
865
|
+
}
|
|
866
|
+
if (!isExecutable(p)) {
|
|
867
|
+
notExec.push(p);
|
|
868
|
+
hookLines.push({
|
|
869
|
+
status: "warn",
|
|
870
|
+
label: h,
|
|
871
|
+
detail: "not executable",
|
|
872
|
+
fix: `chmod +x ${p}`,
|
|
873
|
+
});
|
|
874
|
+
} else {
|
|
875
|
+
hookLines.push({ status: "ok", label: h, detail: "" });
|
|
876
|
+
}
|
|
877
|
+
}
|
|
878
|
+
if (notExec.length > 1) {
|
|
879
|
+
// Collapse into a single combined fix for convenience (still keep per-line fixes too)
|
|
880
|
+
hookLines.push({
|
|
881
|
+
status: "warn",
|
|
882
|
+
label: "fix all",
|
|
883
|
+
detail: `${notExec.length} hooks not executable`,
|
|
884
|
+
fix: `chmod +x ${notExec.join(" ")}`,
|
|
885
|
+
});
|
|
886
|
+
}
|
|
887
|
+
sections.push({ title: "hooks", lines: hookLines });
|
|
888
|
+
|
|
889
|
+
// ----- hook performance (new in v1.30) -----
|
|
890
|
+
// Reads ~/.ashlr/hook-timings.jsonl to surface slow / erroring hooks. Honors
|
|
891
|
+
// the v1.29 "timeout" outcome — a hook hitting the 2s safety net surfaces
|
|
892
|
+
// here so the user can investigate via ~/.ashlr/hook-errors.jsonl.
|
|
893
|
+
try {
|
|
894
|
+
const { findings, totalCalls } = getDoctorHookHealth({});
|
|
895
|
+
if (totalCalls > 0) {
|
|
896
|
+
const perfLines: Line[] = findings.length > 0
|
|
897
|
+
? findings
|
|
898
|
+
: [{
|
|
899
|
+
status: "ok",
|
|
900
|
+
label: "all hooks",
|
|
901
|
+
detail: `${totalCalls} calls in last 24h, all under 50ms p95`,
|
|
902
|
+
}];
|
|
903
|
+
sections.push({ title: "hook performance", lines: perfLines });
|
|
904
|
+
}
|
|
905
|
+
// When totalCalls === 0 we skip the section entirely — a fresh install
|
|
906
|
+
// shouldn't see a confusing "no records yet" line.
|
|
907
|
+
} catch {
|
|
908
|
+
/* hook-timings is best-effort — never fail doctor on it */
|
|
909
|
+
}
|
|
910
|
+
|
|
911
|
+
let warnings = 0;
|
|
912
|
+
let failures = 0;
|
|
913
|
+
for (const s of sections) {
|
|
914
|
+
for (const l of s.lines) {
|
|
915
|
+
if (l.status === "warn") warnings++;
|
|
916
|
+
else if (l.status === "fail") failures++;
|
|
917
|
+
}
|
|
918
|
+
}
|
|
919
|
+
|
|
920
|
+
return { header, sections, warnings, failures };
|
|
921
|
+
}
|
|
922
|
+
|
|
923
|
+
// ---------- formatting ----------
|
|
924
|
+
|
|
925
|
+
export function formatReport(report: Report): string {
|
|
926
|
+
const out: string[] = [];
|
|
927
|
+
// Header — keep header text intact (tests match "ashlr doctor") but colorize
|
|
928
|
+
// the bits after.
|
|
929
|
+
out.push(isColorEnabled() ? c.bold(c.brightMagenta(report.header)) : report.header);
|
|
930
|
+
out.push("");
|
|
931
|
+
// column width: 17 chars for label inside each section for alignment
|
|
932
|
+
const LABEL_W = 15;
|
|
933
|
+
for (const section of report.sections) {
|
|
934
|
+
out.push(isColorEnabled() ? c.bold(c.cyan(section.title)) : section.title);
|
|
935
|
+
for (const line of section.lines) {
|
|
936
|
+
// pad the raw label first so columns stay aligned regardless of color
|
|
937
|
+
// escape codes.
|
|
938
|
+
const paddedLabel = line.label.padEnd(LABEL_W, " ");
|
|
939
|
+
const label = isColorEnabled() ? c.dim(paddedLabel) : paddedLabel;
|
|
940
|
+
const detail = line.detail ? ` ${line.detail}` : "";
|
|
941
|
+
out.push(` ${coloredGlyph(line.status)} ${label}${detail}`);
|
|
942
|
+
if (line.fix && line.status !== "ok") {
|
|
943
|
+
const fixLabel = isColorEnabled() ? c.yellow("fix:") : "fix:";
|
|
944
|
+
const fixText = isColorEnabled() ? c.dim(line.fix) : line.fix;
|
|
945
|
+
out.push(` ${fixLabel} ${fixText}`);
|
|
946
|
+
}
|
|
947
|
+
}
|
|
948
|
+
out.push("");
|
|
949
|
+
}
|
|
950
|
+
|
|
951
|
+
// Summary — colored counts, boxed when we have a TTY.
|
|
952
|
+
const wText = `${report.warnings} warning${report.warnings === 1 ? "" : "s"}`;
|
|
953
|
+
const fText = `${report.failures} failure${report.failures === 1 ? "" : "s"}`;
|
|
954
|
+
if (isColorEnabled()) {
|
|
955
|
+
const w = report.warnings === 0 ? c.dim(wText) : c.yellow(wText);
|
|
956
|
+
const f = report.failures === 0 ? c.dim(fText) : c.red(fText);
|
|
957
|
+
const summary = `${w} ${c.dim(sym.dot)} ${f}`;
|
|
958
|
+
// Overall status word ahead of the counts.
|
|
959
|
+
let tone: string;
|
|
960
|
+
let color: (s: string) => string;
|
|
961
|
+
if (report.failures > 0) { tone = "unhealthy"; color = c.red; }
|
|
962
|
+
else if (report.warnings > 0) { tone = "degraded"; color = c.yellow; }
|
|
963
|
+
else { tone = "healthy"; color = c.green; }
|
|
964
|
+
const titleWord = c.bold(color(tone));
|
|
965
|
+
const body = `${titleWord} ${summary}`;
|
|
966
|
+
out.push(box(body, { title: "summary", color }));
|
|
967
|
+
} else {
|
|
968
|
+
out.push(`${wText} · ${fText}`);
|
|
969
|
+
}
|
|
970
|
+
return out.join("\n");
|
|
971
|
+
}
|
|
972
|
+
|
|
973
|
+
// ---------- main ----------
|
|
974
|
+
|
|
975
|
+
async function main(): Promise<number> {
|
|
976
|
+
const here = dirname(fileURLToPath(import.meta.url));
|
|
977
|
+
const root = resolvePluginRoot(here);
|
|
978
|
+
if (!root) {
|
|
979
|
+
process.stderr.write(
|
|
980
|
+
"ashlr doctor: could not locate plugin root.\n" +
|
|
981
|
+
" set CLAUDE_PLUGIN_ROOT to the plugin install directory, or run this script from inside it.\n",
|
|
982
|
+
);
|
|
983
|
+
return 2;
|
|
984
|
+
}
|
|
985
|
+
const report = await buildReport({ root });
|
|
986
|
+
process.stdout.write(formatReport(report) + "\n");
|
|
987
|
+
return report.failures > 0 ? 1 : 0;
|
|
988
|
+
}
|
|
989
|
+
|
|
990
|
+
if (import.meta.main) {
|
|
991
|
+
main().then((code) => process.exit(code));
|
|
992
|
+
}
|