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,719 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* _telemetry.ts — Opt-in anonymized telemetry buffer + writer.
|
|
3
|
+
*
|
|
4
|
+
* STRICTLY OPT-IN. Default OFF. Users enable via:
|
|
5
|
+
* - ~/.ashlr/config.json :: { "telemetry": "opt-in" }
|
|
6
|
+
* - env: ASHLR_TELEMETRY=on
|
|
7
|
+
*
|
|
8
|
+
* Kill switch (always respected, highest priority):
|
|
9
|
+
* - ~/.ashlr/config.json :: { "telemetry": "off" }
|
|
10
|
+
* - env: ASHLR_TELEMETRY=off
|
|
11
|
+
*
|
|
12
|
+
* What is collected:
|
|
13
|
+
* - Tool call shapes: tool name, raw/compact byte counts, fell-back flag,
|
|
14
|
+
* provider used, duration. No file paths, no patterns, no content.
|
|
15
|
+
* - Pre-tool-use block metadata: tool name, blocked-to destination, size bucket.
|
|
16
|
+
* - Pre-tool-use passthrough reason: tool name, bypass reason string.
|
|
17
|
+
* - Session header: plugin version, bun version, platform, arch.
|
|
18
|
+
*
|
|
19
|
+
* What is NEVER collected:
|
|
20
|
+
* - File paths
|
|
21
|
+
* - Grep patterns
|
|
22
|
+
* - File content or command arguments
|
|
23
|
+
* - User identifiers or repo names
|
|
24
|
+
*
|
|
25
|
+
* Design:
|
|
26
|
+
* - Synchronous append to ~/.ashlr/telemetry-buffer.jsonl (same pattern as
|
|
27
|
+
* _recent-blocks.ts — atomic for lines < PIPE_BUF).
|
|
28
|
+
* - Capped at MAX_BUFFER_LINES (5000). Oldest entries evicted when exceeded.
|
|
29
|
+
* - Eviction is lazy (checked on write) using atomic rename to avoid blocking
|
|
30
|
+
* the hot path most of the time.
|
|
31
|
+
* - Never throws. Never blocks tool functionality.
|
|
32
|
+
*/
|
|
33
|
+
|
|
34
|
+
import { appendFileSync, existsSync, mkdirSync, readFileSync, renameSync, writeFileSync } from "fs";
|
|
35
|
+
import { homedir } from "os";
|
|
36
|
+
import { dirname, join } from "path";
|
|
37
|
+
import { existsSync as fsExistsSync, readFileSync as fsReadFileSync } from "fs";
|
|
38
|
+
|
|
39
|
+
// ---------------------------------------------------------------------------
|
|
40
|
+
// Constants
|
|
41
|
+
// ---------------------------------------------------------------------------
|
|
42
|
+
|
|
43
|
+
export const MAX_BUFFER_LINES = 5000;
|
|
44
|
+
|
|
45
|
+
// Eviction is triggered when the file grows past this multiple of MAX_BUFFER_LINES.
|
|
46
|
+
// Using 1.1 (10% overage) keeps the file tight without frequent rewrites.
|
|
47
|
+
const EVICTION_FACTOR = 1.1;
|
|
48
|
+
|
|
49
|
+
// ---------------------------------------------------------------------------
|
|
50
|
+
// Types
|
|
51
|
+
// ---------------------------------------------------------------------------
|
|
52
|
+
|
|
53
|
+
/** Size bucket for pre-tool-use block events. Never raw bytes. */
|
|
54
|
+
export type SizeRange = "small" | "medium" | "large";
|
|
55
|
+
|
|
56
|
+
/** Canonical telemetry event kinds. */
|
|
57
|
+
export type TelemetryEventKind =
|
|
58
|
+
| "tool_call"
|
|
59
|
+
| "pretooluse_block"
|
|
60
|
+
| "pretooluse_passthrough"
|
|
61
|
+
| "version"
|
|
62
|
+
// "multi_turn_stale_estimate" removed in v1.30 (1.5) — wired since v1.24, never populated.
|
|
63
|
+
| "hook_perf"
|
|
64
|
+
| "genome_compression_ratio"
|
|
65
|
+
| "wizard_step"
|
|
66
|
+
| "pre_compaction_nudge_emitted";
|
|
67
|
+
|
|
68
|
+
/** tool_call payload — what the maintainer needs to tune heuristics. */
|
|
69
|
+
export interface ToolCallPayload {
|
|
70
|
+
tool: string;
|
|
71
|
+
/** The MCP tool name (e.g. "ashlr__grep"). Added in v1.30 (1.1). */
|
|
72
|
+
tool_name?: string;
|
|
73
|
+
rawBytes: number;
|
|
74
|
+
compactBytes: number;
|
|
75
|
+
fellBack: boolean;
|
|
76
|
+
providerUsed: string;
|
|
77
|
+
durationMs: number;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/** pretooluse_block payload. */
|
|
81
|
+
export interface PreToolUseBlockPayload {
|
|
82
|
+
tool: string;
|
|
83
|
+
blockedTo: string;
|
|
84
|
+
sizeRange: SizeRange;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/** pretooluse_passthrough payload. */
|
|
88
|
+
export interface PreToolUsePassthroughPayload {
|
|
89
|
+
tool: string;
|
|
90
|
+
reason: "below-threshold" | "out-of-cwd" | "plugin-tree" | "micro-edit" | "bypass";
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/** version / session-header payload. Sent once per session. */
|
|
94
|
+
export interface VersionPayload {
|
|
95
|
+
pluginVersion: string;
|
|
96
|
+
bunVersion: string;
|
|
97
|
+
platform: string;
|
|
98
|
+
arch: string;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* multi_turn_stale_estimate payload — removed in v1.30 (1.5).
|
|
103
|
+
* Kept as a type alias for backward-compat with any call sites that may still
|
|
104
|
+
* reference it; the event kind is no longer accepted by the server.
|
|
105
|
+
* @deprecated
|
|
106
|
+
*/
|
|
107
|
+
export interface MultiTurnStaleEstimatePayload {
|
|
108
|
+
sessionTurnCount: number;
|
|
109
|
+
staleBytes: number;
|
|
110
|
+
staleResults: number;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* pre_compaction_nudge_emitted payload — fired once per session when cumulative
|
|
115
|
+
* context bytes approach the auto-compact threshold (~80K tokens).
|
|
116
|
+
*/
|
|
117
|
+
export interface PreCompactionNudgePayload {
|
|
118
|
+
cumulativeBytes: number;
|
|
119
|
+
estimatedTokens: number;
|
|
120
|
+
pct: number;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* hook_perf payload — one rollup per hook name per session, emitted at SessionEnd.
|
|
125
|
+
* p50_ms / p99_ms derived from hook-timings.jsonl. Added in v1.30 (1.2).
|
|
126
|
+
*/
|
|
127
|
+
export interface HookPerfPayload {
|
|
128
|
+
hook_name: string;
|
|
129
|
+
p50_ms: number;
|
|
130
|
+
p99_ms: number;
|
|
131
|
+
count: number;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* genome_compression_ratio payload — emitted on every ashlr__grep call that
|
|
136
|
+
* routes through the genome path. Added in v1.30 (1.3).
|
|
137
|
+
*/
|
|
138
|
+
export interface GenomeCompressionRatioPayload {
|
|
139
|
+
tool: string;
|
|
140
|
+
raw_bytes: number;
|
|
141
|
+
compressed_bytes: number;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* wizard_step payload — emitted by Track 3 (Wizard Repair) for each wizard step.
|
|
146
|
+
* Schema added here so Track 3 emit sites just work. Added in v1.30 (Track 3 coord).
|
|
147
|
+
*/
|
|
148
|
+
export interface WizardStepPayload {
|
|
149
|
+
step_name: string;
|
|
150
|
+
outcome: "completed" | "skipped" | "error";
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/** Union of all typed payloads. */
|
|
154
|
+
export type TelemetryPayload =
|
|
155
|
+
| ({ kind: "tool_call" } & ToolCallPayload)
|
|
156
|
+
| ({ kind: "pretooluse_block" } & PreToolUseBlockPayload)
|
|
157
|
+
| ({ kind: "pretooluse_passthrough" } & PreToolUsePassthroughPayload)
|
|
158
|
+
| ({ kind: "version" } & VersionPayload)
|
|
159
|
+
| ({ kind: "hook_perf" } & HookPerfPayload)
|
|
160
|
+
| ({ kind: "genome_compression_ratio" } & GenomeCompressionRatioPayload)
|
|
161
|
+
| ({ kind: "wizard_step" } & WizardStepPayload)
|
|
162
|
+
| ({ kind: "pre_compaction_nudge_emitted" } & PreCompactionNudgePayload);
|
|
163
|
+
|
|
164
|
+
/** A single JSONL record in the buffer. */
|
|
165
|
+
export interface TelemetryRecord {
|
|
166
|
+
ts: number;
|
|
167
|
+
kind: TelemetryEventKind;
|
|
168
|
+
sessionId: string;
|
|
169
|
+
[key: string]: unknown;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// ---------------------------------------------------------------------------
|
|
173
|
+
// Path helpers (resolved at call-time so tests overriding $HOME work)
|
|
174
|
+
// ---------------------------------------------------------------------------
|
|
175
|
+
|
|
176
|
+
function home(): string {
|
|
177
|
+
return process.env.HOME ?? homedir();
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
export function telemetryBufferPath(homeDir: string = home()): string {
|
|
181
|
+
return join(homeDir, ".ashlr", "telemetry-buffer.jsonl");
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
export function telemetrySessionPath(homeDir: string = home()): string {
|
|
185
|
+
return join(homeDir, ".ashlr", "telemetry-session.json");
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
export function ashlrConfigPath(homeDir: string = home()): string {
|
|
189
|
+
return join(homeDir, ".ashlr", "config.json");
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// ---------------------------------------------------------------------------
|
|
193
|
+
// Config reading (shared with session-start.ts — local copy to avoid
|
|
194
|
+
// circular imports across the servers/ and hooks/ boundary)
|
|
195
|
+
// ---------------------------------------------------------------------------
|
|
196
|
+
|
|
197
|
+
function readConfig(homeDir: string = home()): Record<string, unknown> {
|
|
198
|
+
try {
|
|
199
|
+
const p = ashlrConfigPath(homeDir);
|
|
200
|
+
if (!fsExistsSync(p)) return {};
|
|
201
|
+
const raw = fsReadFileSync(p, "utf-8");
|
|
202
|
+
const parsed: unknown = JSON.parse(raw);
|
|
203
|
+
if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
|
|
204
|
+
return parsed as Record<string, unknown>;
|
|
205
|
+
}
|
|
206
|
+
} catch {
|
|
207
|
+
/* treat as empty */
|
|
208
|
+
}
|
|
209
|
+
return {};
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// ---------------------------------------------------------------------------
|
|
213
|
+
// Opt-in check
|
|
214
|
+
// ---------------------------------------------------------------------------
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
* Returns true only when the user has explicitly opted in via:
|
|
218
|
+
* ASHLR_TELEMETRY=on OR config.telemetry === "opt-in"
|
|
219
|
+
*
|
|
220
|
+
* Any other value (missing, "off", anything else) → false (opt-out / default).
|
|
221
|
+
*/
|
|
222
|
+
export function isTelemetryEnabled(homeDir: string = home()): boolean {
|
|
223
|
+
// Env kill switch takes highest priority.
|
|
224
|
+
const envVal = (process.env.ASHLR_TELEMETRY ?? "").toLowerCase().trim();
|
|
225
|
+
if (envVal === "off" || envVal === "0") return false;
|
|
226
|
+
if (envVal === "on" || envVal === "1") {
|
|
227
|
+
// Env opt-in: verify config doesn't explicitly say "off".
|
|
228
|
+
const config = readConfig(homeDir);
|
|
229
|
+
if (config.telemetry === "off") return false;
|
|
230
|
+
return true;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
// No env override: check config file.
|
|
234
|
+
const config = readConfig(homeDir);
|
|
235
|
+
return config.telemetry === "opt-in";
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
// ---------------------------------------------------------------------------
|
|
239
|
+
// Session ID
|
|
240
|
+
// ---------------------------------------------------------------------------
|
|
241
|
+
|
|
242
|
+
/**
|
|
243
|
+
* Returns the per-session hashed ID. Creates one if it doesn't exist.
|
|
244
|
+
* The session file is written on first call and deleted by the session-end hook.
|
|
245
|
+
*
|
|
246
|
+
* We use a UUID-like random string hashed via a simple XOR fold so the raw
|
|
247
|
+
* random seed never persists — only the folded hex is stored.
|
|
248
|
+
*/
|
|
249
|
+
export function getOrCreateTelemetrySessionId(homeDir: string = home()): string {
|
|
250
|
+
const sessionPath = telemetrySessionPath(homeDir);
|
|
251
|
+
try {
|
|
252
|
+
if (fsExistsSync(sessionPath)) {
|
|
253
|
+
const raw = JSON.parse(fsReadFileSync(sessionPath, "utf-8")) as { id?: unknown };
|
|
254
|
+
if (typeof raw.id === "string" && raw.id.length > 0) return raw.id;
|
|
255
|
+
}
|
|
256
|
+
} catch {
|
|
257
|
+
/* fall through to create */
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
// Create a new session ID: random seed → simple hash → hex string.
|
|
261
|
+
const seed = `${Date.now()}:${Math.random().toString(36)}:${Math.random().toString(36)}`;
|
|
262
|
+
const id = hashString(seed);
|
|
263
|
+
|
|
264
|
+
try {
|
|
265
|
+
mkdirSync(dirname(sessionPath), { recursive: true });
|
|
266
|
+
writeFileSync(sessionPath, JSON.stringify({ id, createdAt: Date.now() }), "utf-8");
|
|
267
|
+
} catch {
|
|
268
|
+
/* best-effort */
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
return id;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
/**
|
|
275
|
+
* Delete the session file on session end.
|
|
276
|
+
*/
|
|
277
|
+
export function deleteTelemetrySession(homeDir: string = home()): void {
|
|
278
|
+
try {
|
|
279
|
+
const sessionPath = telemetrySessionPath(homeDir);
|
|
280
|
+
if (fsExistsSync(sessionPath)) {
|
|
281
|
+
// Use fs.unlinkSync — keep the import footprint minimal.
|
|
282
|
+
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
|
283
|
+
const { unlinkSync } = require("fs") as typeof import("fs");
|
|
284
|
+
unlinkSync(sessionPath);
|
|
285
|
+
}
|
|
286
|
+
} catch {
|
|
287
|
+
/* best-effort */
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
// ---------------------------------------------------------------------------
|
|
292
|
+
// Simple string hash (non-crypto — for session-id generation only)
|
|
293
|
+
// ---------------------------------------------------------------------------
|
|
294
|
+
|
|
295
|
+
function hashString(s: string): string {
|
|
296
|
+
// djb2 XOR variant — good enough for a non-security opaque identifier.
|
|
297
|
+
let h1 = 5381;
|
|
298
|
+
let h2 = 52711;
|
|
299
|
+
for (let i = 0; i < s.length; i++) {
|
|
300
|
+
const c = s.charCodeAt(i);
|
|
301
|
+
h1 = ((h1 << 5) + h1) ^ c;
|
|
302
|
+
h2 = ((h2 << 5) + h2) ^ c;
|
|
303
|
+
}
|
|
304
|
+
return ((h1 >>> 0).toString(16).padStart(8, "0") +
|
|
305
|
+
(h2 >>> 0).toString(16).padStart(8, "0"));
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
// ---------------------------------------------------------------------------
|
|
309
|
+
// Buffer writer
|
|
310
|
+
// ---------------------------------------------------------------------------
|
|
311
|
+
|
|
312
|
+
/**
|
|
313
|
+
* Record one telemetry event to the local buffer.
|
|
314
|
+
*
|
|
315
|
+
* Called from event-emission sites (alongside logEvent) when telemetry is
|
|
316
|
+
* enabled. Must be cheap: synchronous append-only.
|
|
317
|
+
*
|
|
318
|
+
* Race safety: appendFileSync is atomic for writes < PIPE_BUF (~512B–4KB on
|
|
319
|
+
* POSIX, ~4KB on Windows). Our JSONL lines are ≤400B, well inside the
|
|
320
|
+
* threshold. Concurrent callers cannot interleave bytes.
|
|
321
|
+
*
|
|
322
|
+
* Eviction: when the buffer exceeds MAX_BUFFER_LINES * EVICTION_FACTOR, we
|
|
323
|
+
* do a one-shot atomic rewrite dropping oldest entries. This only runs on
|
|
324
|
+
* the write path that pushes us over the limit — typically rare.
|
|
325
|
+
*/
|
|
326
|
+
export function recordTelemetryEvent(
|
|
327
|
+
payload: TelemetryPayload,
|
|
328
|
+
homeDir: string = home(),
|
|
329
|
+
): void {
|
|
330
|
+
if (!isTelemetryEnabled(homeDir)) return;
|
|
331
|
+
|
|
332
|
+
try {
|
|
333
|
+
const bufPath = telemetryBufferPath(homeDir);
|
|
334
|
+
mkdirSync(dirname(bufPath), { recursive: true });
|
|
335
|
+
|
|
336
|
+
const sessionId = getOrCreateTelemetrySessionId(homeDir);
|
|
337
|
+
|
|
338
|
+
// Build the anonymized record. Destructure `kind` out; spread remaining.
|
|
339
|
+
const { kind, ...rest } = payload;
|
|
340
|
+
|
|
341
|
+
// Safety guard: reject any record that sneaks in a path-like string value.
|
|
342
|
+
// This is a regression guardrail — callers should never pass paths, but
|
|
343
|
+
// we double-check here as defense in depth.
|
|
344
|
+
for (const v of Object.values(rest)) {
|
|
345
|
+
if (typeof v === "string" && looksLikePath(v)) {
|
|
346
|
+
// Drop the event silently rather than risk exfiltrating a path.
|
|
347
|
+
return;
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
const record: TelemetryRecord = {
|
|
352
|
+
ts: Math.floor(Date.now() / 1000), // epoch seconds (not ms) to save bytes
|
|
353
|
+
kind,
|
|
354
|
+
sessionId,
|
|
355
|
+
...rest,
|
|
356
|
+
};
|
|
357
|
+
|
|
358
|
+
const line = JSON.stringify(record) + "\n";
|
|
359
|
+
appendFileSync(bufPath, line, "utf-8");
|
|
360
|
+
|
|
361
|
+
// Lazy eviction: check line count and prune if needed.
|
|
362
|
+
// We do this AFTER the append so the new event isn't lost.
|
|
363
|
+
_maybeEvict(bufPath);
|
|
364
|
+
} catch {
|
|
365
|
+
// Never propagate — telemetry must never break tool functionality.
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
/**
|
|
370
|
+
* Convenience wrapper for posttooluse-stale-result — records a
|
|
371
|
+
* pre_compaction_nudge_emitted event when context is approaching auto-compact.
|
|
372
|
+
* No-op when telemetry is off.
|
|
373
|
+
*/
|
|
374
|
+
export function logPreCompactionNudgeEvent(
|
|
375
|
+
payload: PreCompactionNudgePayload,
|
|
376
|
+
homeDir: string = home(),
|
|
377
|
+
): void {
|
|
378
|
+
recordTelemetryEvent({ kind: "pre_compaction_nudge_emitted", ...payload }, homeDir);
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
/**
|
|
382
|
+
* Record a hook_perf rollup event. Called from the session-end hook after
|
|
383
|
+
* reading hook-timings.jsonl and computing per-hook percentiles.
|
|
384
|
+
* No-op when telemetry is off.
|
|
385
|
+
*/
|
|
386
|
+
export function logHookPerfEvent(
|
|
387
|
+
payload: HookPerfPayload,
|
|
388
|
+
homeDir: string = home(),
|
|
389
|
+
): void {
|
|
390
|
+
recordTelemetryEvent({ kind: "hook_perf", ...payload }, homeDir);
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
/**
|
|
394
|
+
* Record a genome_compression_ratio event. Called from ashlr__grep whenever
|
|
395
|
+
* the genome path returns content.
|
|
396
|
+
* No-op when telemetry is off.
|
|
397
|
+
*/
|
|
398
|
+
export function logGenomeCompressionRatioEvent(
|
|
399
|
+
payload: GenomeCompressionRatioPayload,
|
|
400
|
+
homeDir: string = home(),
|
|
401
|
+
): void {
|
|
402
|
+
recordTelemetryEvent({ kind: "genome_compression_ratio", ...payload }, homeDir);
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
/**
|
|
406
|
+
* Returns true if `s` looks like an absolute filesystem path.
|
|
407
|
+
* Used as a safety guard to prevent accidental path exfiltration.
|
|
408
|
+
*/
|
|
409
|
+
export function looksLikePath(s: string): boolean {
|
|
410
|
+
if (s.length < 3) return false;
|
|
411
|
+
// POSIX absolute: starts with /
|
|
412
|
+
if (s.startsWith("/")) return true;
|
|
413
|
+
// Windows absolute: C:\ or \\server\share
|
|
414
|
+
if (/^[A-Za-z]:[/\\]/.test(s)) return true;
|
|
415
|
+
if (s.startsWith("\\\\")) return true;
|
|
416
|
+
return false;
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
/**
|
|
420
|
+
* Evict oldest entries when the buffer exceeds MAX_BUFFER_LINES * EVICTION_FACTOR.
|
|
421
|
+
* Uses atomic rename (write to .tmp, then rename) so concurrent readers see
|
|
422
|
+
* either the old or new file, never a partial write.
|
|
423
|
+
*
|
|
424
|
+
* Only called from recordTelemetryEvent — never on the hot read path.
|
|
425
|
+
*/
|
|
426
|
+
function _maybeEvict(bufPath: string): void {
|
|
427
|
+
try {
|
|
428
|
+
if (!existsSync(bufPath)) return;
|
|
429
|
+
const raw = readFileSync(bufPath, "utf-8");
|
|
430
|
+
const lines = raw.split("\n").filter((l) => l.trim());
|
|
431
|
+
if (lines.length <= Math.floor(MAX_BUFFER_LINES * EVICTION_FACTOR)) return;
|
|
432
|
+
|
|
433
|
+
// Keep only the most recent MAX_BUFFER_LINES entries.
|
|
434
|
+
const pruned = lines.slice(-MAX_BUFFER_LINES);
|
|
435
|
+
_atomicRewrite(bufPath, pruned);
|
|
436
|
+
} catch {
|
|
437
|
+
/* best-effort */
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
function _atomicRewrite(path: string, lines: string[]): void {
|
|
442
|
+
const tmp = path + ".tmp";
|
|
443
|
+
const content = lines.length === 0 ? "" : lines.join("\n") + "\n";
|
|
444
|
+
writeFileSync(tmp, content, "utf-8");
|
|
445
|
+
renameSync(tmp, path);
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
// ---------------------------------------------------------------------------
|
|
449
|
+
// Consent notice stamp (testable without importing the full session-start hook)
|
|
450
|
+
// ---------------------------------------------------------------------------
|
|
451
|
+
|
|
452
|
+
/** Path to the one-time consent notice stamp. */
|
|
453
|
+
export function telemetryConsentStampPath(homeDir: string = home()): string {
|
|
454
|
+
return join(homeDir, ".ashlr", "telemetry-consent-shown");
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
/**
|
|
458
|
+
* Returns a one-liner consent notice the first time telemetry is enabled,
|
|
459
|
+
* then null on all subsequent calls (stamp-gated).
|
|
460
|
+
*
|
|
461
|
+
* Strictly opt-in: returns null immediately when telemetry is off.
|
|
462
|
+
*/
|
|
463
|
+
export function maybeTelemetryConsentNotice(homeDir: string = home()): string | null {
|
|
464
|
+
if (!isTelemetryEnabled(homeDir)) return null;
|
|
465
|
+
const stampPath = telemetryConsentStampPath(homeDir);
|
|
466
|
+
try {
|
|
467
|
+
if (existsSync(stampPath)) return null;
|
|
468
|
+
mkdirSync(dirname(stampPath), { recursive: true });
|
|
469
|
+
writeFileSync(stampPath, new Date().toISOString(), "utf-8");
|
|
470
|
+
} catch {
|
|
471
|
+
/* best-effort — never break the hook */
|
|
472
|
+
}
|
|
473
|
+
return (
|
|
474
|
+
"[ashlr] Telemetry opt-in active. Collecting anonymized tool-shape metrics " +
|
|
475
|
+
"(no paths, no content). To disable: set ASHLR_TELEMETRY=off or " +
|
|
476
|
+
'~/.ashlr/config.json { "telemetry": "off" }. See docs/telemetry.md.\n'
|
|
477
|
+
);
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
// ---------------------------------------------------------------------------
|
|
481
|
+
// Buffer reader (used by the flusher)
|
|
482
|
+
// ---------------------------------------------------------------------------
|
|
483
|
+
|
|
484
|
+
/**
|
|
485
|
+
* Read all records from the buffer. Returns empty array on any error.
|
|
486
|
+
*/
|
|
487
|
+
export function readTelemetryBuffer(homeDir: string = home()): TelemetryRecord[] {
|
|
488
|
+
try {
|
|
489
|
+
const bufPath = telemetryBufferPath(homeDir);
|
|
490
|
+
if (!existsSync(bufPath)) return [];
|
|
491
|
+
const raw = readFileSync(bufPath, "utf-8");
|
|
492
|
+
const records: TelemetryRecord[] = [];
|
|
493
|
+
for (const line of raw.split("\n")) {
|
|
494
|
+
if (!line.trim()) continue;
|
|
495
|
+
try {
|
|
496
|
+
records.push(JSON.parse(line) as TelemetryRecord);
|
|
497
|
+
} catch {
|
|
498
|
+
/* skip malformed lines */
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
return records;
|
|
502
|
+
} catch {
|
|
503
|
+
return [];
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
/**
|
|
508
|
+
* Truncate the buffer to only entries newer than `horizonTs` (epoch seconds).
|
|
509
|
+
* Called by the flusher after a successful POST.
|
|
510
|
+
*/
|
|
511
|
+
export function truncateTelemetryBuffer(
|
|
512
|
+
horizonTs: number,
|
|
513
|
+
homeDir: string = home(),
|
|
514
|
+
): void {
|
|
515
|
+
try {
|
|
516
|
+
const bufPath = telemetryBufferPath(homeDir);
|
|
517
|
+
const records = readTelemetryBuffer(homeDir);
|
|
518
|
+
const kept = records.filter((r) => r.ts > horizonTs);
|
|
519
|
+
_atomicRewrite(bufPath, kept.map((r) => JSON.stringify(r)));
|
|
520
|
+
} catch {
|
|
521
|
+
/* best-effort */
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
// ---------------------------------------------------------------------------
|
|
526
|
+
// WAD-D daily heartbeat (added for the weekly-active-developers metric)
|
|
527
|
+
// ---------------------------------------------------------------------------
|
|
528
|
+
|
|
529
|
+
/**
|
|
530
|
+
* Payload posted to /stats/daily-active. Only salted-hashes + UTC date +
|
|
531
|
+
* plugin version — no PII, no raw machine_id, no raw github login.
|
|
532
|
+
*
|
|
533
|
+
* The optional leadIndicators block ships per-client aggregate signals
|
|
534
|
+
* (booleans / counts / one timestamp) that the WAD-D server-side
|
|
535
|
+
* aggregator rolls into the daily lead-indicator JSON. Older clients
|
|
536
|
+
* omit the block entirely — the server treats missing fields as null,
|
|
537
|
+
* so older clients keep working with no schema break.
|
|
538
|
+
*
|
|
539
|
+
* What we DO NOT send: per-tool call counts, file paths, raw nudge events,
|
|
540
|
+
* streak history, any identifier other than the salted hashes above.
|
|
541
|
+
*/
|
|
542
|
+
export interface DailyHeartbeatLeadIndicators {
|
|
543
|
+
/** True iff /ashlr-start onboarding wizard has completed at any point. */
|
|
544
|
+
onboarding_completed?: boolean;
|
|
545
|
+
/** True iff the ashlr status-line master switch is enabled. */
|
|
546
|
+
status_line_enabled?: boolean;
|
|
547
|
+
/** ISO timestamp of the first meaningful savings event; null when unset. */
|
|
548
|
+
first_savings_at?: string | null;
|
|
549
|
+
/** Current saving-streak length in days (capped at 0+, never negative). */
|
|
550
|
+
streak_days?: number;
|
|
551
|
+
/**
|
|
552
|
+
* /ashlr-savings slash-command invocations in the current ISO week.
|
|
553
|
+
* Reset each Monday UTC.
|
|
554
|
+
*/
|
|
555
|
+
savings_invocations_this_week?: number;
|
|
556
|
+
/**
|
|
557
|
+
* Aggregate nudge acceptance ratio (clicked / shown) in [0, 1]; computed
|
|
558
|
+
* from the local nudge-events.jsonl. Null when no nudges have been shown.
|
|
559
|
+
*/
|
|
560
|
+
nudge_accept_rate?: number | null;
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
export interface DailyHeartbeatPayload {
|
|
564
|
+
/** sha256(machine_id + quarterly_salt). 64 hex chars. */
|
|
565
|
+
machineHash: string;
|
|
566
|
+
/** sha256(github_login + quarterly_salt) when signed-in Pro/Team; else null. */
|
|
567
|
+
githubHash: string | null;
|
|
568
|
+
/** UTC date the heartbeat is for: "YYYY-MM-DD". */
|
|
569
|
+
date: string;
|
|
570
|
+
/** ashlr-plugin semver (from package.json). */
|
|
571
|
+
pluginVersion: string;
|
|
572
|
+
/**
|
|
573
|
+
* WAD-D lead-indicator block — optional for backward compat with older
|
|
574
|
+
* clients that don't ship the new fields. Server treats absence as
|
|
575
|
+
* "this client cannot report" (null) rather than "false / 0".
|
|
576
|
+
*/
|
|
577
|
+
leadIndicators?: DailyHeartbeatLeadIndicators;
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
/**
|
|
581
|
+
* Optional override of the daily-heartbeat endpoint URL. Tests inject this
|
|
582
|
+
* so they don't hit the real api.ashlr.ai. Module-private — exported only
|
|
583
|
+
* via the test hook below.
|
|
584
|
+
*/
|
|
585
|
+
let _heartbeatUrlOverride: string | null = null;
|
|
586
|
+
let _heartbeatFetchOverride: typeof fetch | null = null;
|
|
587
|
+
let _heartbeatLastEmittedDate: string | null = null;
|
|
588
|
+
|
|
589
|
+
/** Test hook: force the heartbeat to POST to a mock URL. */
|
|
590
|
+
export function _setDailyHeartbeatUrl(url: string | null): void {
|
|
591
|
+
_heartbeatUrlOverride = url;
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
/** Test hook: inject a custom fetch (e.g. to assert calls). */
|
|
595
|
+
export function _setDailyHeartbeatFetch(fn: typeof fetch | null): void {
|
|
596
|
+
_heartbeatFetchOverride = fn;
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
/** Test hook: reset the in-process last-emitted memo. */
|
|
600
|
+
export function _resetDailyHeartbeatMemo(): void {
|
|
601
|
+
_heartbeatLastEmittedDate = null;
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
/**
|
|
605
|
+
* Resolve the daily-active endpoint URL. Match the existing cloud-sync
|
|
606
|
+
* pattern (ASHLR_API_URL env override → https://api.ashlr.ai default), but
|
|
607
|
+
* routed to /stats/daily-active.
|
|
608
|
+
*
|
|
609
|
+
* The server-side handler is implemented in server/src/routes/daily-active.ts
|
|
610
|
+
* (POST /stats/daily-active, unauthenticated, idempotent upsert). Both client
|
|
611
|
+
* and server are live. The endpoint is intentionally fire-and-forget: network
|
|
612
|
+
* failures are swallowed silently and never surface to the user.
|
|
613
|
+
*/
|
|
614
|
+
function dailyHeartbeatUrl(): string {
|
|
615
|
+
if (_heartbeatUrlOverride) return _heartbeatUrlOverride;
|
|
616
|
+
const base = process.env["ASHLR_API_URL"] ?? "https://api.ashlr.ai";
|
|
617
|
+
return `${base.replace(/\/+$/, "")}/stats/daily-active`;
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
/**
|
|
621
|
+
* Fire-and-forget POST of the daily heartbeat. Gated on telemetry consent.
|
|
622
|
+
*
|
|
623
|
+
* Guarantees:
|
|
624
|
+
* - Returns immediately (does NOT await the network call).
|
|
625
|
+
* - Silent on every error path — never breaks the recordSaving hot path.
|
|
626
|
+
* - No-op when telemetry consent is "off"/missing.
|
|
627
|
+
* - In-process idempotent for the (date, machineHash) tuple — a second
|
|
628
|
+
* call for the same UTC date from the same process is a no-op even if
|
|
629
|
+
* the stats.json daily_active gate ever races.
|
|
630
|
+
*/
|
|
631
|
+
export function emitDailyHeartbeat(
|
|
632
|
+
payload: DailyHeartbeatPayload,
|
|
633
|
+
homeDir: string = home(),
|
|
634
|
+
): void {
|
|
635
|
+
try {
|
|
636
|
+
if (!isTelemetryEnabled(homeDir)) return;
|
|
637
|
+
if (_heartbeatLastEmittedDate === payload.date) return;
|
|
638
|
+
_heartbeatLastEmittedDate = payload.date;
|
|
639
|
+
|
|
640
|
+
const url = dailyHeartbeatUrl();
|
|
641
|
+
const fetcher = _heartbeatFetchOverride ?? globalThis.fetch;
|
|
642
|
+
|
|
643
|
+
// Fire-and-forget — never await, never throw.
|
|
644
|
+
// The lead_indicators block is added at the top level so the server
|
|
645
|
+
// can rev the schema by just reading the optional fields — no zod
|
|
646
|
+
// refactor needed for older clients that omit it.
|
|
647
|
+
const body: Record<string, unknown> = {
|
|
648
|
+
identity_hash: payload.machineHash,
|
|
649
|
+
github_hash: payload.githubHash,
|
|
650
|
+
date: payload.date,
|
|
651
|
+
plugin_version: payload.pluginVersion,
|
|
652
|
+
};
|
|
653
|
+
if (payload.leadIndicators) {
|
|
654
|
+
body["lead_indicators"] = sanitizeLeadIndicators(payload.leadIndicators);
|
|
655
|
+
}
|
|
656
|
+
void Promise.resolve()
|
|
657
|
+
.then(() =>
|
|
658
|
+
fetcher(url, {
|
|
659
|
+
method: "POST",
|
|
660
|
+
headers: { "Content-Type": "application/json" },
|
|
661
|
+
body: JSON.stringify(body),
|
|
662
|
+
signal: AbortSignal.timeout(10_000),
|
|
663
|
+
}),
|
|
664
|
+
)
|
|
665
|
+
.catch(() => {
|
|
666
|
+
/* network failure — drop silently */
|
|
667
|
+
});
|
|
668
|
+
} catch {
|
|
669
|
+
/* never propagate */
|
|
670
|
+
}
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
|
|
674
|
+
/**
|
|
675
|
+
* Coerce lead-indicator fields to safe JSON-serializable values. Drops any
|
|
676
|
+
* non-boolean/non-number/non-string-iso fields rather than shipping
|
|
677
|
+
* garbage. Returns a fresh object (caller's input is not mutated).
|
|
678
|
+
*
|
|
679
|
+
* Privacy: this is the LAST line of defense before bytes leave the box.
|
|
680
|
+
* We intentionally do NOT pass arbitrary keys through — only the
|
|
681
|
+
* documented set lands in the payload.
|
|
682
|
+
*/
|
|
683
|
+
export function sanitizeLeadIndicators(
|
|
684
|
+
raw: DailyHeartbeatLeadIndicators,
|
|
685
|
+
): Record<string, unknown> {
|
|
686
|
+
const out: Record<string, unknown> = {};
|
|
687
|
+
if (typeof raw.onboarding_completed === "boolean") {
|
|
688
|
+
out["onboarding_completed"] = raw.onboarding_completed;
|
|
689
|
+
}
|
|
690
|
+
if (typeof raw.status_line_enabled === "boolean") {
|
|
691
|
+
out["status_line_enabled"] = raw.status_line_enabled;
|
|
692
|
+
}
|
|
693
|
+
if (raw.first_savings_at === null) {
|
|
694
|
+
out["first_savings_at"] = null;
|
|
695
|
+
} else if (typeof raw.first_savings_at === "string" && raw.first_savings_at.length > 0) {
|
|
696
|
+
out["first_savings_at"] = raw.first_savings_at;
|
|
697
|
+
}
|
|
698
|
+
if (typeof raw.streak_days === "number" && Number.isFinite(raw.streak_days) && raw.streak_days >= 0) {
|
|
699
|
+
out["streak_days"] = Math.floor(raw.streak_days);
|
|
700
|
+
}
|
|
701
|
+
if (
|
|
702
|
+
typeof raw.savings_invocations_this_week === "number" &&
|
|
703
|
+
Number.isFinite(raw.savings_invocations_this_week) &&
|
|
704
|
+
raw.savings_invocations_this_week >= 0
|
|
705
|
+
) {
|
|
706
|
+
out["savings_invocations_this_week"] = Math.floor(raw.savings_invocations_this_week);
|
|
707
|
+
}
|
|
708
|
+
if (raw.nudge_accept_rate === null) {
|
|
709
|
+
out["nudge_accept_rate"] = null;
|
|
710
|
+
} else if (
|
|
711
|
+
typeof raw.nudge_accept_rate === "number" &&
|
|
712
|
+
Number.isFinite(raw.nudge_accept_rate) &&
|
|
713
|
+
raw.nudge_accept_rate >= 0 &&
|
|
714
|
+
raw.nudge_accept_rate <= 1
|
|
715
|
+
) {
|
|
716
|
+
out["nudge_accept_rate"] = raw.nudge_accept_rate;
|
|
717
|
+
}
|
|
718
|
+
return out;
|
|
719
|
+
}
|