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,318 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
/**
|
|
3
|
+
* sessionend-savings-nudge.ts — Proactive savings nudge at session end.
|
|
4
|
+
*
|
|
5
|
+
* Fires at SessionEnd (wired via session-end-consolidate.ts) when:
|
|
6
|
+
* - Session-time milestones: current session active > 2h or > 7h.
|
|
7
|
+
* - Lifetime savings thresholds: $5, $25, $100.
|
|
8
|
+
*
|
|
9
|
+
* Dedupe:
|
|
10
|
+
* - Session-time nudges: tracked via `last_session_nudge_ts` in the
|
|
11
|
+
* nudge state file (once per threshold crossing per session start).
|
|
12
|
+
* - Lifetime thresholds: tracked via `lifetime_nudge_ticks` array in
|
|
13
|
+
* ~/.ashlr/savings-nudge-state.json — appended once, never re-fires.
|
|
14
|
+
*
|
|
15
|
+
* Output: prints nudge text to stdout (Claude Code SessionEnd hook channel).
|
|
16
|
+
* Does NOT fire when total lifetime savings = 0 (no activity yet).
|
|
17
|
+
*
|
|
18
|
+
* Telemetry: emits a `tool_call` event with tool_name: "savings_nudge_fired"
|
|
19
|
+
* and payload describing the threshold/milestone. Gated on telemetry opt-in.
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
import { existsSync, mkdirSync, readFileSync, renameSync, writeFileSync } from "fs";
|
|
23
|
+
import { homedir } from "os";
|
|
24
|
+
import { join } from "path";
|
|
25
|
+
import { costFor } from "../servers/_pricing.ts";
|
|
26
|
+
import { recordTelemetryEvent } from "../servers/_telemetry.ts";
|
|
27
|
+
|
|
28
|
+
// ---------------------------------------------------------------------------
|
|
29
|
+
// Types
|
|
30
|
+
// ---------------------------------------------------------------------------
|
|
31
|
+
|
|
32
|
+
export interface NudgeState {
|
|
33
|
+
/** ISO timestamps of session-start times where we already nudged. */
|
|
34
|
+
last_session_nudge_ts: string | null;
|
|
35
|
+
/** Lifetime $ thresholds already fired (never re-fire). */
|
|
36
|
+
lifetime_nudge_ticks: number[];
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// ---------------------------------------------------------------------------
|
|
40
|
+
// Thresholds
|
|
41
|
+
// ---------------------------------------------------------------------------
|
|
42
|
+
|
|
43
|
+
/** Session-time milestones in ms. Order matters: check 7h before 2h so we
|
|
44
|
+
* fire the stronger message when applicable. */
|
|
45
|
+
const SESSION_MILESTONES_MS: { ms: number; label: string }[] = [
|
|
46
|
+
{ ms: 7 * 60 * 60_000, label: "7h" },
|
|
47
|
+
{ ms: 2 * 60 * 60_000, label: "2h" },
|
|
48
|
+
];
|
|
49
|
+
|
|
50
|
+
/** Lifetime $ thresholds (ascending). Fire once per threshold crossing. */
|
|
51
|
+
const LIFETIME_THRESHOLDS_USD: number[] = [5, 25, 100];
|
|
52
|
+
|
|
53
|
+
// ---------------------------------------------------------------------------
|
|
54
|
+
// Paths
|
|
55
|
+
// ---------------------------------------------------------------------------
|
|
56
|
+
|
|
57
|
+
export function nudgeStatePath(homeDir: string = homedir()): string {
|
|
58
|
+
return join(homeDir, ".ashlr", "savings-nudge-state.json");
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// ---------------------------------------------------------------------------
|
|
62
|
+
// IO
|
|
63
|
+
// ---------------------------------------------------------------------------
|
|
64
|
+
|
|
65
|
+
export function readNudgeState(homeDir: string = homedir()): NudgeState {
|
|
66
|
+
try {
|
|
67
|
+
const p = nudgeStatePath(homeDir);
|
|
68
|
+
if (!existsSync(p)) return defaultState();
|
|
69
|
+
const raw = JSON.parse(readFileSync(p, "utf-8")) as Partial<NudgeState>;
|
|
70
|
+
return {
|
|
71
|
+
last_session_nudge_ts:
|
|
72
|
+
typeof raw.last_session_nudge_ts === "string" ? raw.last_session_nudge_ts : null,
|
|
73
|
+
lifetime_nudge_ticks: Array.isArray(raw.lifetime_nudge_ticks)
|
|
74
|
+
? (raw.lifetime_nudge_ticks as unknown[]).filter(
|
|
75
|
+
(v): v is number => typeof v === "number",
|
|
76
|
+
)
|
|
77
|
+
: [],
|
|
78
|
+
};
|
|
79
|
+
} catch {
|
|
80
|
+
return defaultState();
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function defaultState(): NudgeState {
|
|
85
|
+
return { last_session_nudge_ts: null, lifetime_nudge_ticks: [] };
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export function writeNudgeState(state: NudgeState, homeDir: string = homedir()): void {
|
|
89
|
+
try {
|
|
90
|
+
mkdirSync(join(homeDir, ".ashlr"), { recursive: true });
|
|
91
|
+
const finalPath = nudgeStatePath(homeDir);
|
|
92
|
+
const tmpPath = `${finalPath}.${process.pid}.tmp`;
|
|
93
|
+
writeFileSync(tmpPath, JSON.stringify(state, null, 2));
|
|
94
|
+
renameSync(tmpPath, finalPath);
|
|
95
|
+
} catch {
|
|
96
|
+
/* best-effort */
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// ---------------------------------------------------------------------------
|
|
101
|
+
// Stats reader (minimal — only what we need)
|
|
102
|
+
// ---------------------------------------------------------------------------
|
|
103
|
+
|
|
104
|
+
interface StatsFile {
|
|
105
|
+
schemaVersion?: number;
|
|
106
|
+
sessions?: Record<string, { startedAt?: string; tokensSaved?: number }>;
|
|
107
|
+
lifetime?: { tokensSaved?: number };
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
export function readStats(homeDir: string = homedir()): StatsFile | null {
|
|
111
|
+
try {
|
|
112
|
+
const p = join(homeDir, ".ashlr", "stats.json");
|
|
113
|
+
if (!existsSync(p)) return null;
|
|
114
|
+
return JSON.parse(readFileSync(p, "utf-8")) as StatsFile;
|
|
115
|
+
} catch {
|
|
116
|
+
return null;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// ---------------------------------------------------------------------------
|
|
121
|
+
// Nudge text builders
|
|
122
|
+
// ---------------------------------------------------------------------------
|
|
123
|
+
|
|
124
|
+
function buildNudge(lead: string): string {
|
|
125
|
+
return `${lead}\nRun /ashlr-savings for a breakdown, or /ashlr-tier to see what Pro adds for $12/mo.`;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
export function buildSessionMilestoneNudge(label: string): string {
|
|
129
|
+
return buildNudge(
|
|
130
|
+
`You've been coding for ${label} straight — ashlr has been saving tokens the whole time.`,
|
|
131
|
+
);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
export function buildLifetimeThresholdNudge(_threshold: number, dollars: number): string {
|
|
135
|
+
const formatted =
|
|
136
|
+
dollars < 100 ? `$${dollars.toFixed(2)}` : `$${Math.round(dollars)}`;
|
|
137
|
+
return buildNudge(`You've saved ${formatted} in API tokens with ashlr. Keep it up!`);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// ---------------------------------------------------------------------------
|
|
141
|
+
// Core logic
|
|
142
|
+
// ---------------------------------------------------------------------------
|
|
143
|
+
|
|
144
|
+
export interface NudgeCheckResult {
|
|
145
|
+
/** Nudge text to emit, or null if nothing should fire. */
|
|
146
|
+
nudgeText: string | null;
|
|
147
|
+
/** Type of nudge for telemetry. */
|
|
148
|
+
kind: "session_milestone" | "lifetime_threshold" | null;
|
|
149
|
+
/** The threshold/milestone label for telemetry display ("$5", "2h"). */
|
|
150
|
+
thresholdLabel: string | null;
|
|
151
|
+
/** The numeric lifetime threshold (USD) — null for session milestones. */
|
|
152
|
+
thresholdValue: number | null;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Pure function — determines if/what nudge to fire given the current state.
|
|
157
|
+
* Does not perform any IO. Called by checkAndFireNudge which handles IO.
|
|
158
|
+
*
|
|
159
|
+
* @param stats - stats file contents
|
|
160
|
+
* @param state - current nudge state
|
|
161
|
+
* @param sessionStartedAt - ISO string of current session start (from stats)
|
|
162
|
+
* @param nowMs - current timestamp in ms
|
|
163
|
+
*/
|
|
164
|
+
export function computeNudge(
|
|
165
|
+
lifetimeTokensSaved: number,
|
|
166
|
+
sessionDurationMs: number,
|
|
167
|
+
state: NudgeState,
|
|
168
|
+
sessionStartedAt: string,
|
|
169
|
+
nowMs: number,
|
|
170
|
+
): NudgeCheckResult {
|
|
171
|
+
const none: NudgeCheckResult = {
|
|
172
|
+
nudgeText: null,
|
|
173
|
+
kind: null,
|
|
174
|
+
thresholdLabel: null,
|
|
175
|
+
thresholdValue: null,
|
|
176
|
+
};
|
|
177
|
+
|
|
178
|
+
// No savings yet → never nudge.
|
|
179
|
+
if (lifetimeTokensSaved <= 0) return none;
|
|
180
|
+
|
|
181
|
+
const lifetimeDollars = costFor(lifetimeTokensSaved);
|
|
182
|
+
|
|
183
|
+
// -----------------------------------------------------------------------
|
|
184
|
+
// 1. Lifetime threshold nudges (highest priority — fire once, ever).
|
|
185
|
+
// -----------------------------------------------------------------------
|
|
186
|
+
for (const threshold of LIFETIME_THRESHOLDS_USD) {
|
|
187
|
+
if (lifetimeDollars >= threshold && !state.lifetime_nudge_ticks.includes(threshold)) {
|
|
188
|
+
return {
|
|
189
|
+
nudgeText: buildLifetimeThresholdNudge(threshold, lifetimeDollars),
|
|
190
|
+
kind: "lifetime_threshold",
|
|
191
|
+
thresholdLabel: `$${threshold}`,
|
|
192
|
+
thresholdValue: threshold,
|
|
193
|
+
};
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// -----------------------------------------------------------------------
|
|
198
|
+
// 2. Session-time milestone nudges (once per session per milestone).
|
|
199
|
+
// -----------------------------------------------------------------------
|
|
200
|
+
// Dedupe: we use `last_session_nudge_ts` to track when we nudged in the
|
|
201
|
+
// context of THIS session start. If the sessionStartedAt matches, we've
|
|
202
|
+
// already nudged this session.
|
|
203
|
+
const alreadyNudgedThisSession = state.last_session_nudge_ts === sessionStartedAt;
|
|
204
|
+
if (!alreadyNudgedThisSession) {
|
|
205
|
+
for (const { ms, label } of SESSION_MILESTONES_MS) {
|
|
206
|
+
if (sessionDurationMs >= ms) {
|
|
207
|
+
return {
|
|
208
|
+
nudgeText: buildSessionMilestoneNudge(label),
|
|
209
|
+
kind: "session_milestone",
|
|
210
|
+
thresholdLabel: label,
|
|
211
|
+
thresholdValue: null,
|
|
212
|
+
};
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
return none;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/**
|
|
221
|
+
* Main entry: read stats + state, compute nudge, emit if needed, update state.
|
|
222
|
+
* Returns the nudge text that was emitted (or null).
|
|
223
|
+
*/
|
|
224
|
+
export function checkAndFireNudge(
|
|
225
|
+
homeDir: string = homedir(),
|
|
226
|
+
nowMs: number = Date.now(),
|
|
227
|
+
): string | null {
|
|
228
|
+
try {
|
|
229
|
+
const stats = readStats(homeDir);
|
|
230
|
+
if (!stats) return null;
|
|
231
|
+
|
|
232
|
+
const lifetimeTokensSaved = stats.lifetime?.tokensSaved ?? 0;
|
|
233
|
+
if (lifetimeTokensSaved <= 0) return null;
|
|
234
|
+
|
|
235
|
+
// Find the current session's startedAt. Use the session with the most
|
|
236
|
+
// recent activity (highest tokens or lexicographically latest startedAt).
|
|
237
|
+
let sessionStartedAt = "";
|
|
238
|
+
let sessionTokensSaved = 0;
|
|
239
|
+
const sessions = stats.sessions ?? {};
|
|
240
|
+
for (const bucket of Object.values(sessions)) {
|
|
241
|
+
const tok = bucket.tokensSaved ?? 0;
|
|
242
|
+
if (tok > sessionTokensSaved) {
|
|
243
|
+
sessionTokensSaved = tok;
|
|
244
|
+
sessionStartedAt = bucket.startedAt ?? "";
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
// If we can't determine session start, skip session milestone nudges.
|
|
249
|
+
// We use epoch as the fallback so duration math still works (will be 0).
|
|
250
|
+
const sessionStart = sessionStartedAt ? Date.parse(sessionStartedAt) : nowMs;
|
|
251
|
+
const sessionDurationMs = Number.isFinite(sessionStart)
|
|
252
|
+
? Math.max(0, nowMs - sessionStart)
|
|
253
|
+
: 0;
|
|
254
|
+
|
|
255
|
+
const state = readNudgeState(homeDir);
|
|
256
|
+
const result = computeNudge(
|
|
257
|
+
lifetimeTokensSaved,
|
|
258
|
+
sessionDurationMs,
|
|
259
|
+
state,
|
|
260
|
+
sessionStartedAt,
|
|
261
|
+
nowMs,
|
|
262
|
+
);
|
|
263
|
+
|
|
264
|
+
if (!result.nudgeText) return null;
|
|
265
|
+
|
|
266
|
+
// Update state — prevent re-fire. Write BEFORE printing to narrow the race
|
|
267
|
+
// window between concurrent SessionEnd hooks (e.g. two Claude Code windows
|
|
268
|
+
// closing simultaneously). The atomic write in writeNudgeState (tmp+rename)
|
|
269
|
+
// protects against partial-write corruption.
|
|
270
|
+
const updated: NudgeState = { ...state };
|
|
271
|
+
if (result.kind === "lifetime_threshold" && result.thresholdValue !== null) {
|
|
272
|
+
if (!updated.lifetime_nudge_ticks.includes(result.thresholdValue)) {
|
|
273
|
+
updated.lifetime_nudge_ticks = [
|
|
274
|
+
...updated.lifetime_nudge_ticks,
|
|
275
|
+
result.thresholdValue,
|
|
276
|
+
];
|
|
277
|
+
}
|
|
278
|
+
} else if (result.kind === "session_milestone") {
|
|
279
|
+
updated.last_session_nudge_ts = sessionStartedAt;
|
|
280
|
+
}
|
|
281
|
+
writeNudgeState(updated, homeDir);
|
|
282
|
+
|
|
283
|
+
// Emit telemetry (best-effort, gated by opt-in).
|
|
284
|
+
try {
|
|
285
|
+
recordTelemetryEvent(
|
|
286
|
+
{
|
|
287
|
+
kind: "tool_call",
|
|
288
|
+
tool: "savings_nudge_fired",
|
|
289
|
+
tool_name: "savings_nudge_fired",
|
|
290
|
+
rawBytes: 0,
|
|
291
|
+
compactBytes: 0,
|
|
292
|
+
fellBack: false,
|
|
293
|
+
providerUsed: result.kind ?? "unknown",
|
|
294
|
+
durationMs: 0,
|
|
295
|
+
},
|
|
296
|
+
homeDir,
|
|
297
|
+
);
|
|
298
|
+
} catch {
|
|
299
|
+
/* telemetry never breaks nudge */
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
return result.nudgeText;
|
|
303
|
+
} catch {
|
|
304
|
+
return null;
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
// ---------------------------------------------------------------------------
|
|
309
|
+
// CLI entry (invoked by session-end-consolidate.ts as a sub-spawn)
|
|
310
|
+
// ---------------------------------------------------------------------------
|
|
311
|
+
|
|
312
|
+
if (import.meta.main) {
|
|
313
|
+
const text = checkAndFireNudge();
|
|
314
|
+
if (text) {
|
|
315
|
+
process.stdout.write("\n" + text + "\n");
|
|
316
|
+
}
|
|
317
|
+
process.exit(0);
|
|
318
|
+
}
|
|
@@ -0,0 +1,296 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
/**
|
|
3
|
+
* sessionend-status-line-nudge.ts — One-shot SessionEnd nudge that surfaces
|
|
4
|
+
* the status-line integration to users who never discovered it.
|
|
5
|
+
*
|
|
6
|
+
* Why: the status line is opt-in. A user who installs the plugin but
|
|
7
|
+
* never edits ~/.claude/settings.json never gets a live savings ticker
|
|
8
|
+
* even though their lifetime tokens are accumulating. This nudge fires
|
|
9
|
+
* exactly once when:
|
|
10
|
+
*
|
|
11
|
+
* 1. The user has ≥ STATUS_LINE_MIN_CALLS successful ashlr tool
|
|
12
|
+
* calls in their lifetime (stats.json lifetime.calls). Avoids
|
|
13
|
+
* pestering brand-new installs that don't have savings yet.
|
|
14
|
+
* 2. They installed at least N hours ago AND within the 24h discovery
|
|
15
|
+
* window — we only surface this in the early-adopter window,
|
|
16
|
+
* after which the assumption is "they don't want the status line."
|
|
17
|
+
* 3. The Claude Code statusLine is NOT pointing at our script AND
|
|
18
|
+
* the ashlr.statusLine toggle isn't explicitly off.
|
|
19
|
+
* 4. The per-user category throttle hasn't fired before. One-shot
|
|
20
|
+
* lifetime — there's no second chance.
|
|
21
|
+
*
|
|
22
|
+
* Best-effort: any I/O error → null. Never throws. Never blocks the
|
|
23
|
+
* SessionEnd safety net (v1.29 2s budget). Bounded by tiny JSON reads
|
|
24
|
+
* only — no jsonl tailing required.
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
import { existsSync, mkdirSync, readFileSync, renameSync, writeFileSync } from "fs";
|
|
28
|
+
import { homedir } from "os";
|
|
29
|
+
import { join } from "path";
|
|
30
|
+
|
|
31
|
+
// ---------------------------------------------------------------------------
|
|
32
|
+
// Thresholds + windows
|
|
33
|
+
// ---------------------------------------------------------------------------
|
|
34
|
+
|
|
35
|
+
/** Lifetime call floor before the discovery nudge is allowed. */
|
|
36
|
+
export const STATUS_LINE_MIN_CALLS = 3;
|
|
37
|
+
/** Hours from firstRunAt within which the nudge may fire. */
|
|
38
|
+
export const STATUS_LINE_WINDOW_HOURS = 24;
|
|
39
|
+
|
|
40
|
+
// ---------------------------------------------------------------------------
|
|
41
|
+
// State — one-shot per user (no expiry, never re-fires)
|
|
42
|
+
// ---------------------------------------------------------------------------
|
|
43
|
+
|
|
44
|
+
export interface StatusLineNudgeState {
|
|
45
|
+
/** Epoch ms of the only nudge ever emitted (0 = never). */
|
|
46
|
+
last_status_line_nudge_at: number;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const defaultState = (): StatusLineNudgeState => ({
|
|
50
|
+
last_status_line_nudge_at: 0,
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
export function statusLineNudgeStatePath(homeDir: string = homedir()): string {
|
|
54
|
+
return join(homeDir, ".ashlr", "status-line-nudge-state.json");
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export function readStatusLineNudgeState(
|
|
58
|
+
homeDir: string = homedir(),
|
|
59
|
+
): StatusLineNudgeState {
|
|
60
|
+
try {
|
|
61
|
+
const p = statusLineNudgeStatePath(homeDir);
|
|
62
|
+
if (!existsSync(p)) return defaultState();
|
|
63
|
+
const raw = JSON.parse(readFileSync(p, "utf-8")) as Partial<StatusLineNudgeState>;
|
|
64
|
+
return {
|
|
65
|
+
last_status_line_nudge_at:
|
|
66
|
+
typeof raw.last_status_line_nudge_at === "number"
|
|
67
|
+
? raw.last_status_line_nudge_at
|
|
68
|
+
: 0,
|
|
69
|
+
};
|
|
70
|
+
} catch {
|
|
71
|
+
return defaultState();
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export function writeStatusLineNudgeState(
|
|
76
|
+
state: StatusLineNudgeState,
|
|
77
|
+
homeDir: string = homedir(),
|
|
78
|
+
): void {
|
|
79
|
+
try {
|
|
80
|
+
const dir = join(homeDir, ".ashlr");
|
|
81
|
+
mkdirSync(dir, { recursive: true });
|
|
82
|
+
const finalPath = statusLineNudgeStatePath(homeDir);
|
|
83
|
+
const tmpPath = `${finalPath}.${process.pid}.tmp`;
|
|
84
|
+
writeFileSync(tmpPath, JSON.stringify(state, null, 2));
|
|
85
|
+
renameSync(tmpPath, finalPath);
|
|
86
|
+
} catch {
|
|
87
|
+
/* best-effort */
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// ---------------------------------------------------------------------------
|
|
92
|
+
// File readers — small JSON files only; no tail / streaming needed.
|
|
93
|
+
// ---------------------------------------------------------------------------
|
|
94
|
+
|
|
95
|
+
export interface StatsLite {
|
|
96
|
+
lifetimeCalls: number;
|
|
97
|
+
lifetimeTokensSaved: number;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export function readStatsLite(homeDir: string = homedir()): StatsLite | null {
|
|
101
|
+
try {
|
|
102
|
+
const p = join(homeDir, ".ashlr", "stats.json");
|
|
103
|
+
if (!existsSync(p)) return null;
|
|
104
|
+
const raw = JSON.parse(readFileSync(p, "utf-8")) as {
|
|
105
|
+
lifetime?: { calls?: number; tokensSaved?: number };
|
|
106
|
+
};
|
|
107
|
+
const calls = raw.lifetime?.calls;
|
|
108
|
+
const toks = raw.lifetime?.tokensSaved;
|
|
109
|
+
return {
|
|
110
|
+
lifetimeCalls: typeof calls === "number" ? calls : 0,
|
|
111
|
+
lifetimeTokensSaved: typeof toks === "number" ? toks : 0,
|
|
112
|
+
};
|
|
113
|
+
} catch {
|
|
114
|
+
return null;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
export function readFirstRunAtMs(homeDir: string = homedir()): number | null {
|
|
119
|
+
try {
|
|
120
|
+
const p = join(homeDir, ".ashlr", "session-state.json");
|
|
121
|
+
if (!existsSync(p)) return null;
|
|
122
|
+
const raw = JSON.parse(readFileSync(p, "utf-8")) as { firstRunAt?: unknown };
|
|
123
|
+
if (typeof raw.firstRunAt !== "string") return null;
|
|
124
|
+
const t = Date.parse(raw.firstRunAt);
|
|
125
|
+
return Number.isFinite(t) ? t : null;
|
|
126
|
+
} catch {
|
|
127
|
+
return null;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Returns true when the Claude Code statusLine is already wired to our
|
|
133
|
+
* savings-status-line script (i.e. the user has either run
|
|
134
|
+
* install-status-line.ts or hand-configured it). The detection mirrors
|
|
135
|
+
* scripts/install-status-line.ts → isOurStatusLine.
|
|
136
|
+
*
|
|
137
|
+
* Also returns true when settings.ashlr.statusLine is explicitly false —
|
|
138
|
+
* we never nudge someone who has opted out. The literal "ashlr.statusLine"
|
|
139
|
+
* default is true, so a missing value is NOT an opt-out.
|
|
140
|
+
*/
|
|
141
|
+
export function isStatusLineConfigured(homeDir: string = homedir()): boolean {
|
|
142
|
+
try {
|
|
143
|
+
const p = join(homeDir, ".claude", "settings.json");
|
|
144
|
+
if (!existsSync(p)) return false;
|
|
145
|
+
const raw = JSON.parse(readFileSync(p, "utf-8")) as {
|
|
146
|
+
statusLine?: unknown;
|
|
147
|
+
ashlr?: { statusLine?: unknown };
|
|
148
|
+
};
|
|
149
|
+
// Explicit opt-out wins: never nudge.
|
|
150
|
+
if (raw.ashlr && raw.ashlr.statusLine === false) return true;
|
|
151
|
+
// Check the Claude Code statusLine pointer.
|
|
152
|
+
const sl = raw.statusLine;
|
|
153
|
+
if (!sl || typeof sl !== "object") return false;
|
|
154
|
+
const cmd = (sl as { command?: unknown }).command;
|
|
155
|
+
return typeof cmd === "string" && cmd.includes("savings-status-line");
|
|
156
|
+
} catch {
|
|
157
|
+
// Read/parse failure: assume not configured (so the nudge can fire),
|
|
158
|
+
// but the install-status-line script itself refuses to overwrite a
|
|
159
|
+
// corrupt settings.json, so the worst case is one wasted suggestion.
|
|
160
|
+
return false;
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// ---------------------------------------------------------------------------
|
|
165
|
+
// Pure: build the nudge text
|
|
166
|
+
// ---------------------------------------------------------------------------
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Format a token count tersely (e.g. "1.2K", "340", "2.1M").
|
|
170
|
+
* Mirrors scripts/savings-status-line.ts → formatTokens so the nudge
|
|
171
|
+
* shows the same scale as the future status line.
|
|
172
|
+
*/
|
|
173
|
+
export function formatTokens(n: number): string {
|
|
174
|
+
if (!Number.isFinite(n) || n <= 0) return "0";
|
|
175
|
+
if (n < 1000) return String(Math.floor(n));
|
|
176
|
+
if (n < 1_000_000) return (n / 1000).toFixed(1) + "K";
|
|
177
|
+
if (n < 1_000_000_000) return (n / 1_000_000).toFixed(2) + "M";
|
|
178
|
+
return (n / 1_000_000_000).toFixed(2) + "B";
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
export function buildStatusLineNudge(lifetimeTokens: number): string {
|
|
182
|
+
const tok = formatTokens(lifetimeTokens);
|
|
183
|
+
return [
|
|
184
|
+
`ashlr saved you +${tok} tokens already — see your savings live in your status bar.`,
|
|
185
|
+
` Run /ashlr-settings install-status-line (or --set statusLine true if already installed).`,
|
|
186
|
+
].join("\n");
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// ---------------------------------------------------------------------------
|
|
190
|
+
// Decision — pure
|
|
191
|
+
// ---------------------------------------------------------------------------
|
|
192
|
+
|
|
193
|
+
export interface StatusLineNudgeInputs {
|
|
194
|
+
stats: StatsLite | null;
|
|
195
|
+
firstRunAtMs: number | null;
|
|
196
|
+
statusLineConfigured: boolean;
|
|
197
|
+
state: StatusLineNudgeState;
|
|
198
|
+
nowMs: number;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
export interface StatusLineNudgeResult {
|
|
202
|
+
nudgeText: string | null;
|
|
203
|
+
/** Reason it didn't fire (for telemetry / debug). */
|
|
204
|
+
reason?:
|
|
205
|
+
| "fired"
|
|
206
|
+
| "no_stats"
|
|
207
|
+
| "below_min_calls"
|
|
208
|
+
| "status_line_configured"
|
|
209
|
+
| "outside_discovery_window"
|
|
210
|
+
| "already_fired";
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
export function computeStatusLineNudge(
|
|
214
|
+
inputs: StatusLineNudgeInputs,
|
|
215
|
+
): StatusLineNudgeResult {
|
|
216
|
+
// Already fired once — never re-fire.
|
|
217
|
+
if (inputs.state.last_status_line_nudge_at > 0) {
|
|
218
|
+
return { nudgeText: null, reason: "already_fired" };
|
|
219
|
+
}
|
|
220
|
+
// Status line already configured (or explicitly opted out).
|
|
221
|
+
if (inputs.statusLineConfigured) {
|
|
222
|
+
return { nudgeText: null, reason: "status_line_configured" };
|
|
223
|
+
}
|
|
224
|
+
// No stats file: brand-new install with no calls yet.
|
|
225
|
+
if (inputs.stats === null) {
|
|
226
|
+
return { nudgeText: null, reason: "no_stats" };
|
|
227
|
+
}
|
|
228
|
+
if (inputs.stats.lifetimeCalls < STATUS_LINE_MIN_CALLS) {
|
|
229
|
+
return { nudgeText: null, reason: "below_min_calls" };
|
|
230
|
+
}
|
|
231
|
+
// 24h discovery window — only fire while the user is fresh. If we
|
|
232
|
+
// have no firstRunAt at all, fall through (means session-state.json
|
|
233
|
+
// is missing; we still treat the user as fresh).
|
|
234
|
+
if (inputs.firstRunAtMs !== null) {
|
|
235
|
+
const windowMs = STATUS_LINE_WINDOW_HOURS * 60 * 60_000;
|
|
236
|
+
if (inputs.nowMs - inputs.firstRunAtMs > windowMs) {
|
|
237
|
+
return { nudgeText: null, reason: "outside_discovery_window" };
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
return {
|
|
241
|
+
nudgeText: buildStatusLineNudge(inputs.stats.lifetimeTokensSaved),
|
|
242
|
+
reason: "fired",
|
|
243
|
+
};
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
// ---------------------------------------------------------------------------
|
|
247
|
+
// Orchestrator — read state, scan files, emit, persist
|
|
248
|
+
// ---------------------------------------------------------------------------
|
|
249
|
+
|
|
250
|
+
export interface FireResult {
|
|
251
|
+
nudgeText: string | null;
|
|
252
|
+
reason?: StatusLineNudgeResult["reason"];
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
export function checkAndFireStatusLineNudge(
|
|
256
|
+
homeDir: string = homedir(),
|
|
257
|
+
nowMs: number = Date.now(),
|
|
258
|
+
): FireResult {
|
|
259
|
+
try {
|
|
260
|
+
const stats = readStatsLite(homeDir);
|
|
261
|
+
const firstRunAtMs = readFirstRunAtMs(homeDir);
|
|
262
|
+
const statusLineConfigured = isStatusLineConfigured(homeDir);
|
|
263
|
+
const state = readStatusLineNudgeState(homeDir);
|
|
264
|
+
|
|
265
|
+
const result = computeStatusLineNudge({
|
|
266
|
+
stats,
|
|
267
|
+
firstRunAtMs,
|
|
268
|
+
statusLineConfigured,
|
|
269
|
+
state,
|
|
270
|
+
nowMs,
|
|
271
|
+
});
|
|
272
|
+
|
|
273
|
+
if (result.nudgeText) {
|
|
274
|
+
writeStatusLineNudgeState(
|
|
275
|
+
{ last_status_line_nudge_at: nowMs },
|
|
276
|
+
homeDir,
|
|
277
|
+
);
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
return { nudgeText: result.nudgeText, reason: result.reason };
|
|
281
|
+
} catch {
|
|
282
|
+
return { nudgeText: null };
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
// ---------------------------------------------------------------------------
|
|
287
|
+
// CLI entry — invoked by session-end-consolidate.ts
|
|
288
|
+
// ---------------------------------------------------------------------------
|
|
289
|
+
|
|
290
|
+
if (import.meta.main) {
|
|
291
|
+
const { nudgeText } = checkAndFireStatusLineNudge();
|
|
292
|
+
if (nudgeText) {
|
|
293
|
+
process.stdout.write("\n" + nudgeText + "\n");
|
|
294
|
+
}
|
|
295
|
+
process.exit(0);
|
|
296
|
+
}
|