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,1691 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
/**
|
|
3
|
+
* ashlr onboarding wizard — guides first-time users through setup.
|
|
4
|
+
*
|
|
5
|
+
* Designed to be driven by the /ashlr-start skill. Each section emits
|
|
6
|
+
* structured output: plain text blocks + [ASHLR_*] markers the skill
|
|
7
|
+
* uses to drive user Q&A and take action.
|
|
8
|
+
*
|
|
9
|
+
* Usage:
|
|
10
|
+
* bun run scripts/onboarding-wizard.ts # interactive
|
|
11
|
+
* bun run scripts/onboarding-wizard.ts --no-interactive
|
|
12
|
+
* bun run scripts/onboarding-wizard.ts --reset # delete stamp
|
|
13
|
+
*
|
|
14
|
+
* Stdout: the wizard transcript (pipe-safe, 72-char width).
|
|
15
|
+
* Stderr: timing / debug info.
|
|
16
|
+
*
|
|
17
|
+
* Contract: exits 0 on success, 1 only on fatal I/O errors.
|
|
18
|
+
* Never throws to the caller.
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
import { existsSync, mkdirSync, readdirSync, readFileSync, statSync, writeFileSync } from "fs";
|
|
22
|
+
import { readFile, unlink } from "fs/promises";
|
|
23
|
+
import { homedir } from "os";
|
|
24
|
+
import { basename, dirname, join } from "path";
|
|
25
|
+
import { createInterface } from "readline";
|
|
26
|
+
|
|
27
|
+
// ---------------------------------------------------------------------------
|
|
28
|
+
// Constants
|
|
29
|
+
// ---------------------------------------------------------------------------
|
|
30
|
+
|
|
31
|
+
export const STAMP_FILENAME = "installed-at";
|
|
32
|
+
export const WIDTH = 72;
|
|
33
|
+
export const YES_TIMEOUT_MS = 5000;
|
|
34
|
+
/**
|
|
35
|
+
* Permissions prompt timeout is longer than the generic YES_TIMEOUT_MS so
|
|
36
|
+
* the user has time to register that a grant is happening before it
|
|
37
|
+
* auto-accepts. Paired with a once-per-second visible countdown so the
|
|
38
|
+
* "it just auto-approved without asking me" UX bug can't recur.
|
|
39
|
+
*/
|
|
40
|
+
export const PERMISSIONS_COUNTDOWN_MS = 30_000;
|
|
41
|
+
const TOTAL_STEPS = 9;
|
|
42
|
+
|
|
43
|
+
// ---------------------------------------------------------------------------
|
|
44
|
+
// Paths
|
|
45
|
+
// ---------------------------------------------------------------------------
|
|
46
|
+
|
|
47
|
+
export function stampPath(home: string = homedir()): string {
|
|
48
|
+
return join(home, ".ashlr", STAMP_FILENAME);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function ashlrDir(home: string = homedir()): string {
|
|
52
|
+
return join(home, ".ashlr");
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Path to the restart-required hint file. The wizard writes this when it
|
|
57
|
+
* finishes; the SessionStart hook reads + clears it to detect when a user
|
|
58
|
+
* completed the wizard but didn't actually restart Claude Code (and is
|
|
59
|
+
* therefore about to fall back to built-in Read/Edit/Grep without realizing).
|
|
60
|
+
*/
|
|
61
|
+
export function restartRequiredPath(home: string = homedir()): string {
|
|
62
|
+
return join(home, ".ashlr", "restart-required");
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Read this plugin's package.json version. Best-effort — returns "unknown"
|
|
67
|
+
* on any failure so wizard rendering never crashes on a corrupted install.
|
|
68
|
+
*/
|
|
69
|
+
export function readPackageVersionSafe(): string {
|
|
70
|
+
try {
|
|
71
|
+
// package.json lives one directory up from scripts/
|
|
72
|
+
const pkgPath = join(import.meta.dir, "..", "package.json");
|
|
73
|
+
const raw = readFileSync(pkgPath, "utf-8");
|
|
74
|
+
const parsed = JSON.parse(raw) as { version?: unknown };
|
|
75
|
+
if (typeof parsed.version === "string" && parsed.version.trim()) {
|
|
76
|
+
return parsed.version.trim();
|
|
77
|
+
}
|
|
78
|
+
} catch {
|
|
79
|
+
/* ignore — best-effort */
|
|
80
|
+
}
|
|
81
|
+
return "unknown";
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Write the restart-required hint file with the current timestamp + pid +
|
|
86
|
+
* wizard version. Best-effort: wrapped in try/catch so a readonly HOME
|
|
87
|
+
* never errors the wizard.
|
|
88
|
+
*
|
|
89
|
+
* Shape: { writtenAt: ISO8601, wizardVersion: string, pid: number }
|
|
90
|
+
*
|
|
91
|
+
* Consumed by hooks/session-start.ts, which compares writtenAt + pid against
|
|
92
|
+
* its own to decide whether the user actually restarted Claude Code.
|
|
93
|
+
*/
|
|
94
|
+
export function writeRestartRequired(home: string = homedir()): void {
|
|
95
|
+
try {
|
|
96
|
+
mkdirSync(ashlrDir(home), { recursive: true });
|
|
97
|
+
const payload = {
|
|
98
|
+
writtenAt: new Date().toISOString(),
|
|
99
|
+
wizardVersion: readPackageVersionSafe(),
|
|
100
|
+
pid: process.pid,
|
|
101
|
+
};
|
|
102
|
+
writeFileSync(restartRequiredPath(home), JSON.stringify(payload, null, 2) + "\n");
|
|
103
|
+
} catch {
|
|
104
|
+
/* best-effort — readonly HOME, full disk, etc. must never error wizard */
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// ---------------------------------------------------------------------------
|
|
109
|
+
// Onboarding state machine
|
|
110
|
+
// ---------------------------------------------------------------------------
|
|
111
|
+
|
|
112
|
+
export interface OnboardingState {
|
|
113
|
+
started: boolean;
|
|
114
|
+
completed: boolean;
|
|
115
|
+
completedAt?: string;
|
|
116
|
+
lastStep?: number;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
export function onboardingStatePath(home: string = homedir()): string {
|
|
120
|
+
return join(home, ".ashlr", "onboarding.json");
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
export function readOnboardingState(home: string = homedir()): OnboardingState | null {
|
|
124
|
+
try {
|
|
125
|
+
const p = onboardingStatePath(home);
|
|
126
|
+
if (!existsSync(p)) return null;
|
|
127
|
+
const raw = readFileSync(p, "utf-8");
|
|
128
|
+
const parsed = JSON.parse(raw);
|
|
129
|
+
if (parsed && typeof parsed === "object") return parsed as OnboardingState;
|
|
130
|
+
} catch {
|
|
131
|
+
/* treat as missing */
|
|
132
|
+
}
|
|
133
|
+
return null;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
export function writeOnboardingState(state: OnboardingState, home: string = homedir()): void {
|
|
137
|
+
try {
|
|
138
|
+
const dir = ashlrDir(home);
|
|
139
|
+
mkdirSync(dir, { recursive: true });
|
|
140
|
+
writeFileSync(onboardingStatePath(home), JSON.stringify(state, null, 2) + "\n");
|
|
141
|
+
} catch {
|
|
142
|
+
/* best-effort */
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Mark that the wizard has started (idempotent — only writes if not already started).
|
|
148
|
+
* Call at the beginning of runWizard.
|
|
149
|
+
*/
|
|
150
|
+
export function markOnboardingStarted(home: string = homedir()): void {
|
|
151
|
+
const existing = readOnboardingState(home) ?? { started: false, completed: false };
|
|
152
|
+
if (!existing.started) {
|
|
153
|
+
writeOnboardingState({ ...existing, started: true }, home);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Mark progress within the wizard. lastStep is the 1-based step number just completed.
|
|
159
|
+
*/
|
|
160
|
+
export function markOnboardingStep(step: number, home: string = homedir()): void {
|
|
161
|
+
const existing = readOnboardingState(home) ?? { started: true, completed: false };
|
|
162
|
+
writeOnboardingState({ ...existing, started: true, lastStep: step }, home);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Mark the wizard as fully completed.
|
|
167
|
+
*/
|
|
168
|
+
export function markOnboardingCompleted(home: string = homedir()): void {
|
|
169
|
+
writeOnboardingState(
|
|
170
|
+
{ started: true, completed: true, completedAt: new Date().toISOString() },
|
|
171
|
+
home,
|
|
172
|
+
);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// ---------------------------------------------------------------------------
|
|
176
|
+
// Stamp helpers
|
|
177
|
+
// ---------------------------------------------------------------------------
|
|
178
|
+
|
|
179
|
+
export function isFirstRun(home: string = homedir()): boolean {
|
|
180
|
+
return !existsSync(stampPath(home));
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
export function writeStamp(home: string = homedir()): void {
|
|
184
|
+
try {
|
|
185
|
+
const dir = ashlrDir(home);
|
|
186
|
+
mkdirSync(dir, { recursive: true });
|
|
187
|
+
writeFileSync(stampPath(home), new Date().toISOString());
|
|
188
|
+
} catch {
|
|
189
|
+
/* best-effort */
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
export async function deleteStamp(home: string = homedir()): Promise<void> {
|
|
194
|
+
try {
|
|
195
|
+
await unlink(stampPath(home));
|
|
196
|
+
} catch {
|
|
197
|
+
/* already gone */
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// ---------------------------------------------------------------------------
|
|
202
|
+
// Formatting
|
|
203
|
+
// ---------------------------------------------------------------------------
|
|
204
|
+
|
|
205
|
+
function divider(step: number, label: string): string {
|
|
206
|
+
const tag = `STEP ${step}/${TOTAL_STEPS}: ${label}`;
|
|
207
|
+
const rem = Math.max(0, WIDTH - 8 - tag.length);
|
|
208
|
+
return `${"▬".repeat(4)} ${tag} ${"▬".repeat(Math.max(4, rem))}`;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
function wrap(text: string, width: number = WIDTH): string {
|
|
212
|
+
const words = text.split(" ");
|
|
213
|
+
const lines: string[] = [];
|
|
214
|
+
let line = "";
|
|
215
|
+
for (const word of words) {
|
|
216
|
+
if (line.length === 0) {
|
|
217
|
+
line = word;
|
|
218
|
+
} else if (line.length + 1 + word.length <= width) {
|
|
219
|
+
line += " " + word;
|
|
220
|
+
} else {
|
|
221
|
+
lines.push(line);
|
|
222
|
+
line = word;
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
if (line) lines.push(line);
|
|
226
|
+
return lines.join("\n");
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
// ---------------------------------------------------------------------------
|
|
230
|
+
// Plugin root
|
|
231
|
+
// ---------------------------------------------------------------------------
|
|
232
|
+
|
|
233
|
+
export function resolvePluginRoot(): string {
|
|
234
|
+
const env = process.env.CLAUDE_PLUGIN_ROOT;
|
|
235
|
+
if (env && existsSync(join(env, ".claude-plugin/plugin.json"))) return env;
|
|
236
|
+
// Walk up from this script's location
|
|
237
|
+
let dir = import.meta.dir;
|
|
238
|
+
for (let i = 0; i < 10; i++) {
|
|
239
|
+
if (existsSync(join(dir, ".claude-plugin/plugin.json"))) return dir;
|
|
240
|
+
const parent = join(dir, "..");
|
|
241
|
+
if (parent === dir) break;
|
|
242
|
+
dir = parent;
|
|
243
|
+
}
|
|
244
|
+
return dirname(import.meta.dir);
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
// ---------------------------------------------------------------------------
|
|
248
|
+
// Source file counting
|
|
249
|
+
// ---------------------------------------------------------------------------
|
|
250
|
+
|
|
251
|
+
const SRC_EXTS = new Set([
|
|
252
|
+
".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs",
|
|
253
|
+
".py", ".rb", ".go", ".rs", ".java", ".kt",
|
|
254
|
+
".swift", ".c", ".cpp", ".h", ".cs", ".php",
|
|
255
|
+
]);
|
|
256
|
+
|
|
257
|
+
const SKIP_DIRS = new Set([
|
|
258
|
+
"node_modules", ".git", "dist", "build", "out",
|
|
259
|
+
".next", ".nuxt", "coverage", ".ashlrcode",
|
|
260
|
+
]);
|
|
261
|
+
|
|
262
|
+
export function countSourceFiles(dir: string, maxScan = 500): number {
|
|
263
|
+
let count = 0;
|
|
264
|
+
const queue: string[] = [dir];
|
|
265
|
+
while (queue.length > 0 && count <= maxScan) {
|
|
266
|
+
const current = queue.shift()!;
|
|
267
|
+
let names: string[];
|
|
268
|
+
try {
|
|
269
|
+
names = readdirSync(current) as string[];
|
|
270
|
+
} catch {
|
|
271
|
+
continue;
|
|
272
|
+
}
|
|
273
|
+
for (const name of names) {
|
|
274
|
+
if (SKIP_DIRS.has(name)) continue;
|
|
275
|
+
const full = join(current, name);
|
|
276
|
+
let isDir = false;
|
|
277
|
+
try {
|
|
278
|
+
isDir = statSync(full).isDirectory();
|
|
279
|
+
} catch {
|
|
280
|
+
continue;
|
|
281
|
+
}
|
|
282
|
+
if (isDir) {
|
|
283
|
+
queue.push(full);
|
|
284
|
+
} else {
|
|
285
|
+
const ext = "." + name.split(".").pop()!.toLowerCase();
|
|
286
|
+
if (SRC_EXTS.has(ext)) count++;
|
|
287
|
+
if (count > maxScan) break;
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
return count;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
// ---------------------------------------------------------------------------
|
|
295
|
+
// Doctor check (lightweight local subset — no MCP probing)
|
|
296
|
+
// ---------------------------------------------------------------------------
|
|
297
|
+
|
|
298
|
+
export interface DoctorResult {
|
|
299
|
+
pluginRoot: string | null;
|
|
300
|
+
hasDeps: boolean;
|
|
301
|
+
allowlistOk: boolean;
|
|
302
|
+
genomePresent: boolean;
|
|
303
|
+
issues: string[];
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
export async function runDoctorCheck(
|
|
307
|
+
opts: { home?: string; cwd?: string; pluginRoot?: string } = {}
|
|
308
|
+
): Promise<DoctorResult> {
|
|
309
|
+
const home = opts.home ?? homedir();
|
|
310
|
+
const cwd = opts.cwd ?? process.cwd();
|
|
311
|
+
const pluginRoot = opts.pluginRoot ?? resolvePluginRoot();
|
|
312
|
+
|
|
313
|
+
const issues: string[] = [];
|
|
314
|
+
|
|
315
|
+
// Plugin root
|
|
316
|
+
const rootOk = pluginRoot !== null && existsSync(join(pluginRoot, ".claude-plugin/plugin.json"));
|
|
317
|
+
if (!rootOk) issues.push("Plugin root not found — set CLAUDE_PLUGIN_ROOT");
|
|
318
|
+
|
|
319
|
+
// Dependencies
|
|
320
|
+
const hasDeps = existsSync(join(pluginRoot ?? "", "node_modules/@modelcontextprotocol/sdk"));
|
|
321
|
+
if (!hasDeps) issues.push(`Dependencies missing — run: cd "${pluginRoot}" && bun install`);
|
|
322
|
+
|
|
323
|
+
// Allowlist
|
|
324
|
+
const settingsPath = join(home, ".claude/settings.json");
|
|
325
|
+
let allowlistOk = false;
|
|
326
|
+
try {
|
|
327
|
+
if (existsSync(settingsPath)) {
|
|
328
|
+
const raw = await readFile(settingsPath, "utf8");
|
|
329
|
+
const s = JSON.parse(raw) as { permissions?: { allow?: string[] } };
|
|
330
|
+
const allow = s?.permissions?.allow ?? [];
|
|
331
|
+
allowlistOk = allow.some((e: string) => /^mcp__ashlr(-|__)/.test(e) || e === "mcp__ashlr-*");
|
|
332
|
+
}
|
|
333
|
+
} catch {
|
|
334
|
+
/* treat as not present */
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
// Genome
|
|
338
|
+
const genomePresent = existsSync(join(cwd, ".ashlrcode", "genome"));
|
|
339
|
+
|
|
340
|
+
return { pluginRoot, hasDeps, allowlistOk, genomePresent, issues };
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
// ---------------------------------------------------------------------------
|
|
344
|
+
// Live demo: find a readable source file
|
|
345
|
+
// ---------------------------------------------------------------------------
|
|
346
|
+
|
|
347
|
+
export function findDemoFile(cwd: string): string | null {
|
|
348
|
+
const candidates = [
|
|
349
|
+
join(cwd, "scripts/session-greet.ts"),
|
|
350
|
+
join(cwd, "scripts/doctor.ts"),
|
|
351
|
+
join(cwd, "hooks/session-start.ts"),
|
|
352
|
+
];
|
|
353
|
+
for (const p of candidates) {
|
|
354
|
+
if (existsSync(p)) return p;
|
|
355
|
+
}
|
|
356
|
+
// Fall back: first .ts file found (non-test, non-node_modules)
|
|
357
|
+
const queue: string[] = [cwd];
|
|
358
|
+
while (queue.length > 0) {
|
|
359
|
+
const dir = queue.shift()!;
|
|
360
|
+
let names: string[];
|
|
361
|
+
try {
|
|
362
|
+
names = readdirSync(dir) as string[];
|
|
363
|
+
} catch {
|
|
364
|
+
continue;
|
|
365
|
+
}
|
|
366
|
+
for (const name of names) {
|
|
367
|
+
if (SKIP_DIRS.has(name)) continue;
|
|
368
|
+
if (name.startsWith("__tests__")) continue;
|
|
369
|
+
const full = join(dir, name);
|
|
370
|
+
let isDir = false;
|
|
371
|
+
try {
|
|
372
|
+
isDir = statSync(full).isDirectory();
|
|
373
|
+
} catch {
|
|
374
|
+
continue;
|
|
375
|
+
}
|
|
376
|
+
if (isDir) {
|
|
377
|
+
queue.push(full);
|
|
378
|
+
} else if (name.endsWith(".ts") && !name.endsWith(".test.ts")) {
|
|
379
|
+
return full;
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
return null;
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
export function fileSizeBytes(p: string): number {
|
|
387
|
+
try {
|
|
388
|
+
return statSync(p).size;
|
|
389
|
+
} catch {
|
|
390
|
+
return 0;
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
// Approximate read payload: ashlr__read returns head+tail ~25% of original
|
|
395
|
+
// for large files. We model this without actually calling the MCP tool so the
|
|
396
|
+
// wizard script is self-contained and can run without MCP active. The real
|
|
397
|
+
// live-demo path (runRealReadDemo) supersedes this estimate whenever the
|
|
398
|
+
// efficiency-server is reachable; the estimate is the fallback.
|
|
399
|
+
export function estimateReadPayload(sizeBytes: number): number {
|
|
400
|
+
if (sizeBytes <= 4096) return sizeBytes; // small file: full content
|
|
401
|
+
// snipCompact: ~30 head lines + ~20 tail lines ≈ 50 lines * ~60 chars = 3000
|
|
402
|
+
// plus elision marker. Conservative estimate: 40% of original, min 3KB.
|
|
403
|
+
return Math.max(3000, Math.round(sizeBytes * 0.35));
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
/** Result of the real live demo — either a measured payload size or an error. */
|
|
407
|
+
export interface RealReadDemoResult {
|
|
408
|
+
/** Bytes returned by ashlr__read. null when the real call failed. */
|
|
409
|
+
payloadBytes: number | null;
|
|
410
|
+
/** First ~240 chars of the compact payload for display. null on failure. */
|
|
411
|
+
sample: string | null;
|
|
412
|
+
/** Non-fatal failure reason (logged as [ASHLR_WARN], fall back to estimate). */
|
|
413
|
+
error: string | null;
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
/**
|
|
417
|
+
* Invoke the real ashlr__read MCP tool via a subprocess against the
|
|
418
|
+
* efficiency-server. We shell out instead of importing the server module
|
|
419
|
+
* directly because the wizard runs as a plain script and shouldn't pull the
|
|
420
|
+
* full MCP stack into memory. The server is invoked with a throwaway stdio
|
|
421
|
+
* transport, handed exactly one tools/call request, and its stdout parsed.
|
|
422
|
+
*
|
|
423
|
+
* Falls back cleanly when:
|
|
424
|
+
* - The plugin root can't be resolved.
|
|
425
|
+
* - The efficiency-server script is missing.
|
|
426
|
+
* - The spawn times out (12 s ceiling — long enough for cold Bun spawns
|
|
427
|
+
* on slower laptops, still keeps the wizard well under 60 s overall).
|
|
428
|
+
* - The JSON response is malformed.
|
|
429
|
+
*
|
|
430
|
+
* On any failure returns `error` set and the caller renders the fake
|
|
431
|
+
* estimate so onboarding still tells a coherent story.
|
|
432
|
+
*/
|
|
433
|
+
export async function runRealReadDemo(
|
|
434
|
+
demoFile: string,
|
|
435
|
+
opts: { pluginRoot?: string; timeoutMs?: number } = {},
|
|
436
|
+
): Promise<RealReadDemoResult> {
|
|
437
|
+
const timeoutMs = opts.timeoutMs ?? 12000;
|
|
438
|
+
const pluginRoot = opts.pluginRoot ?? resolvePluginRoot();
|
|
439
|
+
const serverPath = join(pluginRoot, "servers/efficiency-server.ts");
|
|
440
|
+
if (!existsSync(serverPath)) {
|
|
441
|
+
return { payloadBytes: null, sample: null, error: "efficiency-server not found" };
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
// Minimal JSON-RPC request: initialize then tools/call → ashlr__read.
|
|
445
|
+
// The server follows the MCP stdio protocol so we write framed lines.
|
|
446
|
+
// We pipe a single request and close stdin; server exits via EOF.
|
|
447
|
+
const initReq = {
|
|
448
|
+
jsonrpc: "2.0", id: 1, method: "initialize",
|
|
449
|
+
params: { protocolVersion: "2024-11-05", capabilities: {}, clientInfo: { name: "ashlr-wizard", version: "0" } },
|
|
450
|
+
};
|
|
451
|
+
const toolReq = {
|
|
452
|
+
jsonrpc: "2.0", id: 2, method: "tools/call",
|
|
453
|
+
params: { name: "ashlr__read", arguments: { path: demoFile } },
|
|
454
|
+
};
|
|
455
|
+
const payload = JSON.stringify(initReq) + "\n" + JSON.stringify(toolReq) + "\n";
|
|
456
|
+
|
|
457
|
+
let childStdout = "";
|
|
458
|
+
try {
|
|
459
|
+
const { spawn } = await import("child_process");
|
|
460
|
+
const child = spawn("bun", ["run", serverPath], {
|
|
461
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
462
|
+
env: { ...process.env, ASHLR_WIZARD_DEMO: "1" },
|
|
463
|
+
});
|
|
464
|
+
child.stdout.on("data", (chunk: Buffer) => { childStdout += chunk.toString("utf8"); });
|
|
465
|
+
child.stdin.write(payload);
|
|
466
|
+
child.stdin.end();
|
|
467
|
+
|
|
468
|
+
await new Promise<void>((resolve) => {
|
|
469
|
+
const t = setTimeout(() => { try { child.kill("SIGTERM"); } catch { /* ignore */ } resolve(); }, timeoutMs);
|
|
470
|
+
child.on("exit", () => { clearTimeout(t); resolve(); });
|
|
471
|
+
child.on("error", () => { clearTimeout(t); resolve(); });
|
|
472
|
+
});
|
|
473
|
+
} catch (err) {
|
|
474
|
+
return { payloadBytes: null, sample: null, error: err instanceof Error ? err.message : String(err) };
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
// Parse the stream for the id=2 response.
|
|
478
|
+
type ReadResp = { result?: { content?: Array<{ type: string; text?: string }> } };
|
|
479
|
+
let readResponse: ReadResp | null = null;
|
|
480
|
+
for (const raw of childStdout.split("\n")) {
|
|
481
|
+
const line = raw.trim();
|
|
482
|
+
if (!line || line[0] !== "{") continue;
|
|
483
|
+
try {
|
|
484
|
+
const msg = JSON.parse(line) as { id?: number } & ReadResp;
|
|
485
|
+
if (msg.id === 2 && msg.result) {
|
|
486
|
+
readResponse = msg;
|
|
487
|
+
break;
|
|
488
|
+
}
|
|
489
|
+
} catch {
|
|
490
|
+
/* skip non-JSON frame */
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
if (!readResponse?.result?.content) {
|
|
494
|
+
return { payloadBytes: null, sample: null, error: "no tool response" };
|
|
495
|
+
}
|
|
496
|
+
const text = (readResponse.result.content[0]?.text) ?? "";
|
|
497
|
+
if (text.length === 0) {
|
|
498
|
+
return { payloadBytes: null, sample: null, error: "empty payload" };
|
|
499
|
+
}
|
|
500
|
+
const sample = text.slice(0, 240);
|
|
501
|
+
return { payloadBytes: Buffer.byteLength(text, "utf8"), sample, error: null };
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
// ---------------------------------------------------------------------------
|
|
505
|
+
// Interactive confirmation
|
|
506
|
+
// ---------------------------------------------------------------------------
|
|
507
|
+
|
|
508
|
+
export async function askYesNo(
|
|
509
|
+
question: string,
|
|
510
|
+
defaultYes: boolean = true,
|
|
511
|
+
timeoutMs: number = YES_TIMEOUT_MS,
|
|
512
|
+
interactive: boolean = true,
|
|
513
|
+
): Promise<boolean> {
|
|
514
|
+
if (!interactive) return defaultYes;
|
|
515
|
+
|
|
516
|
+
const hint = defaultYes ? "Y/n" : "y/N";
|
|
517
|
+
process.stdout.write(`${question} [${hint}]: `);
|
|
518
|
+
|
|
519
|
+
return new Promise<boolean>((resolve) => {
|
|
520
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
521
|
+
let settled = false;
|
|
522
|
+
|
|
523
|
+
const timer = setTimeout(() => {
|
|
524
|
+
if (!settled) {
|
|
525
|
+
settled = true;
|
|
526
|
+
rl.close();
|
|
527
|
+
process.stdout.write(`(timeout — defaulting to ${defaultYes ? "yes" : "no"})\n`);
|
|
528
|
+
resolve(defaultYes);
|
|
529
|
+
}
|
|
530
|
+
}, timeoutMs);
|
|
531
|
+
|
|
532
|
+
rl.once("line", (line) => {
|
|
533
|
+
if (!settled) {
|
|
534
|
+
settled = true;
|
|
535
|
+
clearTimeout(timer);
|
|
536
|
+
rl.close();
|
|
537
|
+
const trimmed = line.trim().toLowerCase();
|
|
538
|
+
if (trimmed === "") resolve(defaultYes);
|
|
539
|
+
else resolve(trimmed === "y" || trimmed === "yes");
|
|
540
|
+
}
|
|
541
|
+
});
|
|
542
|
+
|
|
543
|
+
rl.once("close", () => {
|
|
544
|
+
if (!settled) {
|
|
545
|
+
settled = true;
|
|
546
|
+
clearTimeout(timer);
|
|
547
|
+
resolve(defaultYes);
|
|
548
|
+
}
|
|
549
|
+
});
|
|
550
|
+
});
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
/**
|
|
554
|
+
* Yes/no prompt with a visible per-second countdown before auto-accept.
|
|
555
|
+
*
|
|
556
|
+
* Used for the permissions grant so the user can't miss that a grant is
|
|
557
|
+
* about to happen — the previous 5-second silent timeout shipped users
|
|
558
|
+
* a "ashlr auto-approved without asking me" experience. Typing y/Enter
|
|
559
|
+
* accepts early, n rejects, timeout = accept.
|
|
560
|
+
*
|
|
561
|
+
* The countdown prints one line per second using a carriage return so the
|
|
562
|
+
* terminal only ever shows the current count, not a vertical stack. When
|
|
563
|
+
* stdin is consumed the line is overwritten with a final status message.
|
|
564
|
+
*/
|
|
565
|
+
export async function askYesNoWithCountdown(
|
|
566
|
+
question: string,
|
|
567
|
+
totalMs: number = PERMISSIONS_COUNTDOWN_MS,
|
|
568
|
+
interactive: boolean = true,
|
|
569
|
+
): Promise<boolean> {
|
|
570
|
+
if (!interactive) return true;
|
|
571
|
+
|
|
572
|
+
const totalSec = Math.max(1, Math.round(totalMs / 1000));
|
|
573
|
+
process.stdout.write(`${question} [Y/n]\n`);
|
|
574
|
+
process.stdout.write(`(Auto-accepting in ${totalSec}... press y/Enter to accept, n to deny)\n`);
|
|
575
|
+
|
|
576
|
+
return new Promise<boolean>((resolve) => {
|
|
577
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
578
|
+
let settled = false;
|
|
579
|
+
let remaining = totalSec;
|
|
580
|
+
|
|
581
|
+
const finish = (accepted: boolean, msg?: string): void => {
|
|
582
|
+
if (settled) return;
|
|
583
|
+
settled = true;
|
|
584
|
+
clearInterval(tick);
|
|
585
|
+
rl.close();
|
|
586
|
+
// Clear the countdown line and print the final disposition so the user
|
|
587
|
+
// sees an unambiguous "what happened" line in the transcript.
|
|
588
|
+
process.stdout.write("\r\x1b[2K");
|
|
589
|
+
if (msg) process.stdout.write(msg + "\n");
|
|
590
|
+
resolve(accepted);
|
|
591
|
+
};
|
|
592
|
+
|
|
593
|
+
const tick = setInterval(() => {
|
|
594
|
+
remaining--;
|
|
595
|
+
if (remaining <= 0) {
|
|
596
|
+
finish(true, "(timeout — auto-accepting permissions)");
|
|
597
|
+
return;
|
|
598
|
+
}
|
|
599
|
+
// \r + CSI 2K clears the current line so counters overwrite cleanly.
|
|
600
|
+
process.stdout.write(`\rAuto-accepting in ${remaining}... `);
|
|
601
|
+
}, 1000);
|
|
602
|
+
|
|
603
|
+
rl.once("line", (line) => {
|
|
604
|
+
const trimmed = line.trim().toLowerCase();
|
|
605
|
+
if (trimmed === "n" || trimmed === "no") {
|
|
606
|
+
finish(false, "(declined)");
|
|
607
|
+
} else {
|
|
608
|
+
finish(true, "(accepted)");
|
|
609
|
+
}
|
|
610
|
+
});
|
|
611
|
+
|
|
612
|
+
rl.once("close", () => {
|
|
613
|
+
if (!settled) finish(true, "(stream closed — auto-accepting)");
|
|
614
|
+
});
|
|
615
|
+
});
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
// ---------------------------------------------------------------------------
|
|
619
|
+
// Section renderers
|
|
620
|
+
// ---------------------------------------------------------------------------
|
|
621
|
+
|
|
622
|
+
function out(s: string): void {
|
|
623
|
+
process.stdout.write(s + "\n");
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
function blank(): void {
|
|
627
|
+
out("");
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
// Step 0: greeting
|
|
631
|
+
export function renderGreeting(): void {
|
|
632
|
+
blank();
|
|
633
|
+
out("▬".repeat(WIDTH));
|
|
634
|
+
out(wrap("You just installed ashlr. Let's show you what it does."));
|
|
635
|
+
out(wrap(
|
|
636
|
+
"This wizard takes about 60 seconds. Press Enter to accept " +
|
|
637
|
+
"defaults at each prompt."
|
|
638
|
+
));
|
|
639
|
+
out("▬".repeat(WIDTH));
|
|
640
|
+
blank();
|
|
641
|
+
out("ashlr in 60 seconds:");
|
|
642
|
+
out(" 1. /ashlr-doctor — verify install (10s)");
|
|
643
|
+
out(" 2. status line install — see savings as you code (15s, restart required)");
|
|
644
|
+
out(" 3. /ashlr-genome-init — boost grep savings 40% → 84% (30s)");
|
|
645
|
+
blank();
|
|
646
|
+
out(wrap(
|
|
647
|
+
"Core MCP tools (all return compressed output to save tokens):"
|
|
648
|
+
));
|
|
649
|
+
out(" ashlr__read — smart head+tail file reader");
|
|
650
|
+
out(" ashlr__grep — filtered search with line limits");
|
|
651
|
+
out(" ashlr__edit — compressed edit acknowledgements");
|
|
652
|
+
out(" ashlr__diff — compact diff output");
|
|
653
|
+
out(" ashlr__bash — bash with summarized output");
|
|
654
|
+
blank();
|
|
655
|
+
out(wrap(
|
|
656
|
+
"New in this version:"
|
|
657
|
+
));
|
|
658
|
+
out(" ashlr__websearch — compressed web search results");
|
|
659
|
+
out(" ashlr__task_list — compressed task list output");
|
|
660
|
+
out(" ashlr__task_get — compressed task detail output");
|
|
661
|
+
out(" ashlr__notebook_edit — compressed notebook cell edits");
|
|
662
|
+
out(" ashlr__write — compressed file write acknowledgements");
|
|
663
|
+
blank();
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
// Step 1: doctor check
|
|
667
|
+
export function renderDoctorOutput(result: DoctorResult): void {
|
|
668
|
+
out(divider(1, "Doctor check"));
|
|
669
|
+
blank();
|
|
670
|
+
out(`Plugin root: ${result.pluginRoot ?? "(not found)"}`);
|
|
671
|
+
out(`Dependencies: ${result.hasDeps ? "installed" : "MISSING"}`);
|
|
672
|
+
out(`Allowlist: ${result.allowlistOk ? "auto-approved" : "not configured"}`);
|
|
673
|
+
out(`Genome: ${result.genomePresent ? "present" : "not initialized"}`);
|
|
674
|
+
blank();
|
|
675
|
+
if (result.issues.length === 0) {
|
|
676
|
+
out("[ASHLR_OK] doctor-passed");
|
|
677
|
+
} else {
|
|
678
|
+
for (const issue of result.issues) {
|
|
679
|
+
out(`[ASHLR_WARN] ${issue}`);
|
|
680
|
+
}
|
|
681
|
+
}
|
|
682
|
+
blank();
|
|
683
|
+
}
|
|
684
|
+
|
|
685
|
+
// Step 2: permissions
|
|
686
|
+
export function renderPermissionsSection(allowlistOk: boolean): void {
|
|
687
|
+
out(divider(2, "Permissions"));
|
|
688
|
+
blank();
|
|
689
|
+
if (allowlistOk) {
|
|
690
|
+
out(wrap(
|
|
691
|
+
"Your ~/.claude/settings.json already auto-approves all ashlr " +
|
|
692
|
+
"tools. No action needed."
|
|
693
|
+
));
|
|
694
|
+
out("[ASHLR_OK] permissions-ok");
|
|
695
|
+
} else {
|
|
696
|
+
out(wrap(
|
|
697
|
+
"~/.claude/settings.json does not auto-approve ashlr tools. " +
|
|
698
|
+
"Without this, Claude Code prompts you for every ashlr__read, " +
|
|
699
|
+
"ashlr__grep, and ashlr__edit call — dozens of prompts per session."
|
|
700
|
+
));
|
|
701
|
+
blank();
|
|
702
|
+
out("[ASHLR_PROMPT: Auto-approve all ashlr tools? (y/n, default y)]");
|
|
703
|
+
}
|
|
704
|
+
blank();
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
// Step 3: status-line offer
|
|
708
|
+
export function renderStatusLineSection(statusLineInstalled: boolean): void {
|
|
709
|
+
out(divider(3, "Status line"));
|
|
710
|
+
blank();
|
|
711
|
+
if (statusLineInstalled) {
|
|
712
|
+
out(wrap(
|
|
713
|
+
"Status line already configured — live savings counter is active."
|
|
714
|
+
));
|
|
715
|
+
out("[ASHLR_OK] status-line-present");
|
|
716
|
+
blank();
|
|
717
|
+
return;
|
|
718
|
+
}
|
|
719
|
+
out(wrap(
|
|
720
|
+
"Show real-time savings in your status bar — recommended. " +
|
|
721
|
+
"The status line displays a live counter of tokens and cost saved " +
|
|
722
|
+
"at the bottom of every Claude Code session."
|
|
723
|
+
));
|
|
724
|
+
blank();
|
|
725
|
+
out("[ASHLR_PROMPT: Install status line? (Y/n, default y)]");
|
|
726
|
+
blank();
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
/**
|
|
730
|
+
* End-of-wizard "restart enforcement" callout. The LOUD block addresses
|
|
731
|
+
* UX-audit cliff #2: users finished the wizard, ignored the soft callout,
|
|
732
|
+
* ran their next tool call, and saw built-in Read/Edit/Grep fire instead
|
|
733
|
+
* of ashlr's tools — making the plugin look broken when the user just
|
|
734
|
+
* didn't quit Claude Code.
|
|
735
|
+
*
|
|
736
|
+
* Side effect: writes ~/.ashlr/restart-required as a hint file. The
|
|
737
|
+
* SessionStart hook reads it on the next session start; if the pid matches
|
|
738
|
+
* (same shell, no restart) it warns the user, otherwise it silently clears
|
|
739
|
+
* the hint.
|
|
740
|
+
*/
|
|
741
|
+
export function renderRestartCallout(home: string = homedir()): void {
|
|
742
|
+
blank();
|
|
743
|
+
out("═══════════════════════════════════════════════════════════════════");
|
|
744
|
+
out(" RESTART REQUIRED — your next tool call will NOT use ashlr until");
|
|
745
|
+
out(" you fully quit and reopen Claude Code (not just close the window).");
|
|
746
|
+
out("");
|
|
747
|
+
out(" Plugin tools register on session start; without a restart, the");
|
|
748
|
+
out(" built-in Read/Edit/Grep run instead and you'll see no savings.");
|
|
749
|
+
out("═══════════════════════════════════════════════════════════════════");
|
|
750
|
+
blank();
|
|
751
|
+
writeRestartRequired(home);
|
|
752
|
+
}
|
|
753
|
+
|
|
754
|
+
// Step 4: live demo
|
|
755
|
+
export function renderLiveDemoSection(
|
|
756
|
+
demoFile: string | null,
|
|
757
|
+
sizeBytes: number,
|
|
758
|
+
payloadBytes: number,
|
|
759
|
+
opts: { real?: boolean; sample?: string | null; error?: string | null } = {},
|
|
760
|
+
): void {
|
|
761
|
+
out(divider(4, "Live demo"));
|
|
762
|
+
blank();
|
|
763
|
+
if (!demoFile) {
|
|
764
|
+
out(wrap(
|
|
765
|
+
"No source files found in the current directory to demo. " +
|
|
766
|
+
"Skipping read comparison."
|
|
767
|
+
));
|
|
768
|
+
out("[ASHLR_OK] demo-skipped");
|
|
769
|
+
blank();
|
|
770
|
+
return;
|
|
771
|
+
}
|
|
772
|
+
|
|
773
|
+
const pct = sizeBytes > 0 ? Math.round((payloadBytes / sizeBytes) * 100) : 100;
|
|
774
|
+
const saved = Math.max(0, sizeBytes - payloadBytes);
|
|
775
|
+
const shortName = demoFile.replace(homedir(), "~");
|
|
776
|
+
const readLabel = opts.real ? "ashlr__read:" : "ashlr__read:";
|
|
777
|
+
const realityTag = opts.real ? " (live)" : " (estimate)";
|
|
778
|
+
|
|
779
|
+
out(`File: ${shortName}`);
|
|
780
|
+
out(`Disk size: ${sizeBytes.toLocaleString()} bytes`);
|
|
781
|
+
out(`${readLabel} ${payloadBytes.toLocaleString()} bytes returned (~${pct}% of file)${realityTag}`);
|
|
782
|
+
out(`Saved: ${saved.toLocaleString()} bytes not sent to the model`);
|
|
783
|
+
blank();
|
|
784
|
+
if (opts.real && opts.sample) {
|
|
785
|
+
out(wrap(
|
|
786
|
+
"Live output (first 240 chars of the compact payload):"
|
|
787
|
+
));
|
|
788
|
+
blank();
|
|
789
|
+
// Display the sample inside a faux fenced-preview block using ▸ so the
|
|
790
|
+
// transcript stays plain-text pipeable.
|
|
791
|
+
for (const line of opts.sample.split("\n").slice(0, 8)) {
|
|
792
|
+
out("▸ " + line.replace(/\s+$/, ""));
|
|
793
|
+
}
|
|
794
|
+
blank();
|
|
795
|
+
} else if (opts.error) {
|
|
796
|
+
out(wrap(
|
|
797
|
+
`Live read failed (${opts.error}); showing an estimated payload size ` +
|
|
798
|
+
"based on the snipCompact model instead."
|
|
799
|
+
));
|
|
800
|
+
blank();
|
|
801
|
+
}
|
|
802
|
+
out(wrap(
|
|
803
|
+
"ashlr__read returns a snipCompact view: full head + full tail + " +
|
|
804
|
+
"elided middle. The model sees the structure and entry/exit points " +
|
|
805
|
+
"of every file without ingesting the full body."
|
|
806
|
+
));
|
|
807
|
+
out("[ASHLR_OK] demo-complete");
|
|
808
|
+
blank();
|
|
809
|
+
}
|
|
810
|
+
|
|
811
|
+
// Step 5: genome offer
|
|
812
|
+
//
|
|
813
|
+
// `aha` is an optional inline before/after token demo used to make the value
|
|
814
|
+
// statement concrete: a real file from the user's repo with the
|
|
815
|
+
// "without genome / with genome" token counts side-by-side. Computed in
|
|
816
|
+
// runWizard from data we already have (demoFile + fileSizeBytes) so the
|
|
817
|
+
// render path stays sub-1-second and doesn't fire any new I/O. Skip the demo
|
|
818
|
+
// (pass undefined) when no demo file was found — the one-liner value
|
|
819
|
+
// statement is still shown.
|
|
820
|
+
export interface GenomeAhaDemo {
|
|
821
|
+
filePath: string;
|
|
822
|
+
sizeBytes: number;
|
|
823
|
+
/** Token estimate without genome (raw file, ~4 chars/token). */
|
|
824
|
+
withoutGenomeTokens: number;
|
|
825
|
+
/** Token estimate with genome (snipCompact ~20%, floor at 200 tok). */
|
|
826
|
+
withGenomeTokens: number;
|
|
827
|
+
}
|
|
828
|
+
|
|
829
|
+
/**
|
|
830
|
+
* Precompute the genome-aha before/after demo from numbers the wizard already
|
|
831
|
+
* has. Pure function, sub-millisecond. Returns null when no demo file exists
|
|
832
|
+
* or the file is too small to meaningfully demo against (< 4KB).
|
|
833
|
+
*
|
|
834
|
+
* Token model: ~4 chars per token (industry rule of thumb). snipCompact
|
|
835
|
+
* typically returns ~20% of the original for files > a few KB, so we
|
|
836
|
+
* multiply by 0.2 with a 200-token floor (snipCompact always retains
|
|
837
|
+
* head+tail markers).
|
|
838
|
+
*/
|
|
839
|
+
export function computeGenomeAhaDemo(
|
|
840
|
+
demoFile: string | null,
|
|
841
|
+
sizeBytes: number,
|
|
842
|
+
): GenomeAhaDemo | null {
|
|
843
|
+
if (!demoFile) return null;
|
|
844
|
+
if (sizeBytes < 4096) return null; // too small to be a convincing demo
|
|
845
|
+
const withoutGenomeTokens = Math.round(sizeBytes / 4);
|
|
846
|
+
const withGenomeTokens = Math.max(200, Math.round((sizeBytes * 0.2) / 4));
|
|
847
|
+
return { filePath: demoFile, sizeBytes, withoutGenomeTokens, withGenomeTokens };
|
|
848
|
+
}
|
|
849
|
+
|
|
850
|
+
export function renderGenomeSection(
|
|
851
|
+
srcFileCount: number,
|
|
852
|
+
genomePresent: boolean,
|
|
853
|
+
aha?: GenomeAhaDemo | null,
|
|
854
|
+
): void {
|
|
855
|
+
out(divider(5, "Genome"));
|
|
856
|
+
blank();
|
|
857
|
+
if (genomePresent) {
|
|
858
|
+
out(wrap("Genome already initialized in this project. You're all set."));
|
|
859
|
+
out("[ASHLR_OK] genome-present");
|
|
860
|
+
blank();
|
|
861
|
+
return;
|
|
862
|
+
}
|
|
863
|
+
|
|
864
|
+
// Value statement — always shown, even on small repos. Users skip genome
|
|
865
|
+
// init because the prompt never explains the magnitude of the win. The
|
|
866
|
+
// "~84%" number is the measured grep token-savings ceiling across our
|
|
867
|
+
// benchmark repos (see CHANGELOG v1.23 "honest headline -57%" → genome
|
|
868
|
+
// path can reach ~84% on warm caches with dense embeddings).
|
|
869
|
+
out(wrap(
|
|
870
|
+
"Genome unlocks ~84% token savings on grep across this codebase by " +
|
|
871
|
+
"pre-indexing symbol definitions — Claude retrieves targeted " +
|
|
872
|
+
"excerpts instead of raw file content."
|
|
873
|
+
));
|
|
874
|
+
blank();
|
|
875
|
+
|
|
876
|
+
// Inline before/after demo when we have a real file to point at. Numbers
|
|
877
|
+
// are precomputed (see computeGenomeAhaDemo) so render stays sub-1s.
|
|
878
|
+
if (aha) {
|
|
879
|
+
const shortName = aha.filePath.replace(homedir(), "~");
|
|
880
|
+
out(`Example: ${shortName} (${aha.sizeBytes.toLocaleString()} bytes)`);
|
|
881
|
+
out(` Without genome: ~${aha.withoutGenomeTokens.toLocaleString()} tokens`);
|
|
882
|
+
out(` With genome: ~${aha.withGenomeTokens.toLocaleString()} tokens`);
|
|
883
|
+
blank();
|
|
884
|
+
}
|
|
885
|
+
|
|
886
|
+
if (srcFileCount < 10) {
|
|
887
|
+
// Small/greenfield repos used to be silently skipped here, which meant
|
|
888
|
+
// brand-new projects never saw ashlr's strongest feature. Offer it
|
|
889
|
+
// anyway with a soft caveat — the default flips to "no" so the user
|
|
890
|
+
// has to opt in, and the genome is cheap to nuke if abandoned (just
|
|
891
|
+
// delete .ashlrcode/genome/).
|
|
892
|
+
out(
|
|
893
|
+
wrap(
|
|
894
|
+
`Only ${srcFileCount} source file${srcFileCount === 1 ? "" : "s"} found ` +
|
|
895
|
+
"in the current directory. Savings compound with repo size, so the " +
|
|
896
|
+
"win on a small/greenfield project is modest right now — but " +
|
|
897
|
+
"initializing one seeds the index for when the project grows."
|
|
898
|
+
)
|
|
899
|
+
);
|
|
900
|
+
blank();
|
|
901
|
+
out("[ASHLR_PROMPT: Initialize a genome anyway? (y/n, default n)]");
|
|
902
|
+
blank();
|
|
903
|
+
return;
|
|
904
|
+
}
|
|
905
|
+
|
|
906
|
+
out(
|
|
907
|
+
wrap(
|
|
908
|
+
`Found ${srcFileCount} source files. Initializing the genome now is a ` +
|
|
909
|
+
"one-time ~30 s indexing pass; every subsequent grep call benefits."
|
|
910
|
+
)
|
|
911
|
+
);
|
|
912
|
+
blank();
|
|
913
|
+
out("[ASHLR_PROMPT: Initialize a genome for this project? (y/n, default y)]");
|
|
914
|
+
blank();
|
|
915
|
+
}
|
|
916
|
+
|
|
917
|
+
// ---------------------------------------------------------------------------
|
|
918
|
+
// Telemetry opt-in (Step 6) — config: { "telemetry": "opt-in" | "off" }
|
|
919
|
+
//
|
|
920
|
+
// See docs/telemetry.md for the full contract: ASHLR_TELEMETRY=on/off (env)
|
|
921
|
+
// or ~/.ashlr/config.json { "telemetry": "opt-in" | "off" }. Strictly opt-in;
|
|
922
|
+
// default is OFF. We persist via the same ~/.ashlr/config.json file the
|
|
923
|
+
// Ollama step writes to (preserve sibling keys).
|
|
924
|
+
// ---------------------------------------------------------------------------
|
|
925
|
+
|
|
926
|
+
export interface TelemetryOfferState {
|
|
927
|
+
/** Telemetry already set to opt-in via config or env — skip the prompt. */
|
|
928
|
+
alreadyOptedIn: boolean;
|
|
929
|
+
/** Telemetry already set to "off" via config or env — skip the prompt. */
|
|
930
|
+
alreadyOptedOut: boolean;
|
|
931
|
+
/** Config file we'd write to if the user accepts. */
|
|
932
|
+
configPath: string;
|
|
933
|
+
}
|
|
934
|
+
|
|
935
|
+
/**
|
|
936
|
+
* Inspect ~/.ashlr/config.json + ASHLR_TELEMETRY env to decide whether to
|
|
937
|
+
* surface the consent prompt. We respect any prior explicit choice so the
|
|
938
|
+
* wizard never overrides an existing opt-out and never re-prompts an
|
|
939
|
+
* already-opted-in user.
|
|
940
|
+
*/
|
|
941
|
+
export function detectTelemetryState(
|
|
942
|
+
home: string = homedir(),
|
|
943
|
+
env: NodeJS.ProcessEnv = process.env,
|
|
944
|
+
): TelemetryOfferState {
|
|
945
|
+
const envVal = (env.ASHLR_TELEMETRY ?? "").toLowerCase().trim();
|
|
946
|
+
const envOn = envVal === "on" || envVal === "1";
|
|
947
|
+
const envOff = envVal === "off" || envVal === "0";
|
|
948
|
+
|
|
949
|
+
let cfg: Record<string, unknown> = {};
|
|
950
|
+
try {
|
|
951
|
+
const path = join(home, ".ashlr", "config.json");
|
|
952
|
+
if (existsSync(path)) {
|
|
953
|
+
const raw = readFileSync(path, "utf-8");
|
|
954
|
+
const parsed = JSON.parse(raw);
|
|
955
|
+
if (parsed && typeof parsed === "object") cfg = parsed as Record<string, unknown>;
|
|
956
|
+
}
|
|
957
|
+
} catch {
|
|
958
|
+
/* treat as empty */
|
|
959
|
+
}
|
|
960
|
+
|
|
961
|
+
const cfgOptIn = cfg.telemetry === "opt-in";
|
|
962
|
+
const cfgOff = cfg.telemetry === "off";
|
|
963
|
+
|
|
964
|
+
return {
|
|
965
|
+
alreadyOptedIn: envOn || cfgOptIn,
|
|
966
|
+
alreadyOptedOut: envOff || cfgOff,
|
|
967
|
+
configPath: join(home, ".ashlr", "config.json"),
|
|
968
|
+
};
|
|
969
|
+
}
|
|
970
|
+
|
|
971
|
+
/**
|
|
972
|
+
* Persist telemetry consent to ~/.ashlr/config.json — preserving sibling
|
|
973
|
+
* keys (e.g. ASHLR_EMBED_URL from the Ollama step). Best-effort: failures
|
|
974
|
+
* are logged via [ASHLR_WARN] and don't block the wizard.
|
|
975
|
+
*/
|
|
976
|
+
export async function writeTelemetryChoice(
|
|
977
|
+
choice: "opt-in" | "off",
|
|
978
|
+
home: string = homedir(),
|
|
979
|
+
): Promise<{ ok: boolean; path: string; error?: string }> {
|
|
980
|
+
const path = join(home, ".ashlr", "config.json");
|
|
981
|
+
try {
|
|
982
|
+
mkdirSync(ashlrDir(home), { recursive: true });
|
|
983
|
+
let existing: Record<string, unknown> = {};
|
|
984
|
+
if (existsSync(path)) {
|
|
985
|
+
try {
|
|
986
|
+
const raw = await readFile(path, "utf8");
|
|
987
|
+
const parsed = JSON.parse(raw);
|
|
988
|
+
if (parsed && typeof parsed === "object") existing = parsed as Record<string, unknown>;
|
|
989
|
+
} catch {
|
|
990
|
+
/* overwrite corrupt */
|
|
991
|
+
}
|
|
992
|
+
}
|
|
993
|
+
existing["telemetry"] = choice;
|
|
994
|
+
writeFileSync(path, JSON.stringify(existing, null, 2) + "\n");
|
|
995
|
+
return { ok: true, path };
|
|
996
|
+
} catch (err) {
|
|
997
|
+
return { ok: false, path, error: err instanceof Error ? err.message : String(err) };
|
|
998
|
+
}
|
|
999
|
+
}
|
|
1000
|
+
|
|
1001
|
+
// Step 6: telemetry consent
|
|
1002
|
+
export function renderTelemetrySection(state: TelemetryOfferState): void {
|
|
1003
|
+
out(divider(6, "Telemetry"));
|
|
1004
|
+
blank();
|
|
1005
|
+
if (state.alreadyOptedIn) {
|
|
1006
|
+
out(wrap(
|
|
1007
|
+
"Telemetry opt-in already active. To disable: ASHLR_TELEMETRY=off or " +
|
|
1008
|
+
"~/.ashlr/config.json { \"telemetry\": \"off\" }."
|
|
1009
|
+
));
|
|
1010
|
+
out("[ASHLR_OK] telemetry-already-opted-in");
|
|
1011
|
+
blank();
|
|
1012
|
+
return;
|
|
1013
|
+
}
|
|
1014
|
+
if (state.alreadyOptedOut) {
|
|
1015
|
+
out(wrap(
|
|
1016
|
+
"Telemetry already disabled. Nothing is collected. See " +
|
|
1017
|
+
"docs/telemetry.md to re-enable any time."
|
|
1018
|
+
));
|
|
1019
|
+
out("[ASHLR_OK] telemetry-already-opted-out");
|
|
1020
|
+
blank();
|
|
1021
|
+
return;
|
|
1022
|
+
}
|
|
1023
|
+
out(wrap(
|
|
1024
|
+
"Share anonymized session stats with the ashlr team to improve the " +
|
|
1025
|
+
"plugin? We collect tool-shape metrics only: tool name, raw/compact " +
|
|
1026
|
+
"byte counts, fall-back flags, duration. Never paths, never content, " +
|
|
1027
|
+
"never identifiers. Full schema: docs/telemetry.md."
|
|
1028
|
+
));
|
|
1029
|
+
blank();
|
|
1030
|
+
out(wrap(
|
|
1031
|
+
"Default is no. You can opt out anytime by setting ASHLR_TELEMETRY=off " +
|
|
1032
|
+
"or running /ashlr-settings telemetry off."
|
|
1033
|
+
));
|
|
1034
|
+
blank();
|
|
1035
|
+
out("[ASHLR_PROMPT: Share anonymized session stats? (y/N, default n)]");
|
|
1036
|
+
blank();
|
|
1037
|
+
}
|
|
1038
|
+
|
|
1039
|
+
/**
|
|
1040
|
+
* Outcome of step 5 Ollama detection so the wizard can decide whether to
|
|
1041
|
+
* prompt, skip, or surface an install hint.
|
|
1042
|
+
*/
|
|
1043
|
+
export interface OllamaOfferState {
|
|
1044
|
+
/** Already configured via env: offer is skipped. */
|
|
1045
|
+
alreadyConfigured: boolean;
|
|
1046
|
+
/** `which ollama` resolved to a binary on PATH. */
|
|
1047
|
+
installed: boolean;
|
|
1048
|
+
/** Config file we'd write to if the user accepts. */
|
|
1049
|
+
configPath: string;
|
|
1050
|
+
}
|
|
1051
|
+
|
|
1052
|
+
export function detectOllamaState(
|
|
1053
|
+
home: string = homedir(),
|
|
1054
|
+
env: NodeJS.ProcessEnv = process.env,
|
|
1055
|
+
): OllamaOfferState {
|
|
1056
|
+
const alreadyConfigured =
|
|
1057
|
+
!!(env.ASHLR_EMBED_URL && env.ASHLR_EMBED_URL.trim().length > 0) ||
|
|
1058
|
+
!!(env.OLLAMA_HOST && env.OLLAMA_HOST.trim().length > 0);
|
|
1059
|
+
let installed = false;
|
|
1060
|
+
try {
|
|
1061
|
+
// spawnSync `which` (POSIX) / `where` (Windows) synchronously — cheap and
|
|
1062
|
+
// avoids pulling in `bun`. We don't care about the resolved path, just the
|
|
1063
|
+
// exit code. A 5 s timeout prevents hangs on slow Windows CI runners.
|
|
1064
|
+
const { spawnSync } = require("child_process") as typeof import("child_process");
|
|
1065
|
+
const cmd = process.platform === "win32" ? "where" : "which";
|
|
1066
|
+
const res = spawnSync(cmd, ["ollama"], { stdio: "ignore", timeout: 5_000 });
|
|
1067
|
+
installed = res.status === 0;
|
|
1068
|
+
} catch {
|
|
1069
|
+
installed = false;
|
|
1070
|
+
}
|
|
1071
|
+
return {
|
|
1072
|
+
alreadyConfigured,
|
|
1073
|
+
installed,
|
|
1074
|
+
configPath: join(home, ".ashlr", "config.json"),
|
|
1075
|
+
};
|
|
1076
|
+
}
|
|
1077
|
+
|
|
1078
|
+
/**
|
|
1079
|
+
* Persist ASHLR_EMBED_URL pointing at the local Ollama daemon. We write a
|
|
1080
|
+
* plain JSON blob the CLI bootstrap reads at startup so the flag survives
|
|
1081
|
+
* across sessions without the user editing their shell rc. Best-effort:
|
|
1082
|
+
* failures are logged and skipped so onboarding keeps flowing.
|
|
1083
|
+
*/
|
|
1084
|
+
export async function enableOllamaEmbeddings(
|
|
1085
|
+
home: string = homedir(),
|
|
1086
|
+
): Promise<{ ok: boolean; path: string; error?: string }> {
|
|
1087
|
+
const path = join(home, ".ashlr", "config.json");
|
|
1088
|
+
try {
|
|
1089
|
+
mkdirSync(ashlrDir(home), { recursive: true });
|
|
1090
|
+
let existing: Record<string, unknown> = {};
|
|
1091
|
+
if (existsSync(path)) {
|
|
1092
|
+
try {
|
|
1093
|
+
const raw = await readFile(path, "utf8");
|
|
1094
|
+
const parsed = JSON.parse(raw);
|
|
1095
|
+
if (parsed && typeof parsed === "object") existing = parsed as Record<string, unknown>;
|
|
1096
|
+
} catch {
|
|
1097
|
+
/* overwrite corrupt */
|
|
1098
|
+
}
|
|
1099
|
+
}
|
|
1100
|
+
existing["ASHLR_EMBED_URL"] = "http://localhost:11434/api/embeddings";
|
|
1101
|
+
writeFileSync(path, JSON.stringify(existing, null, 2) + "\n");
|
|
1102
|
+
return { ok: true, path };
|
|
1103
|
+
} catch (err) {
|
|
1104
|
+
return { ok: false, path, error: err instanceof Error ? err.message : String(err) };
|
|
1105
|
+
}
|
|
1106
|
+
}
|
|
1107
|
+
|
|
1108
|
+
/**
|
|
1109
|
+
* Detect whether `gh auth status` succeeds (GitHub CLI is logged in).
|
|
1110
|
+
* Returns true when gh is installed and authenticated, false otherwise.
|
|
1111
|
+
* Never throws.
|
|
1112
|
+
*/
|
|
1113
|
+
export function detectGhAuthState(): boolean {
|
|
1114
|
+
try {
|
|
1115
|
+
const { spawnSync } = require("child_process") as typeof import("child_process");
|
|
1116
|
+
// A 10 s timeout prevents hangs on Windows CI when gh is installed but
|
|
1117
|
+
// the auth check blocks on network (no internet access on hosted runners).
|
|
1118
|
+
const res = spawnSync("gh", ["auth", "status"], { stdio: "ignore", timeout: 10_000 });
|
|
1119
|
+
return res.status === 0;
|
|
1120
|
+
} catch {
|
|
1121
|
+
return false;
|
|
1122
|
+
}
|
|
1123
|
+
}
|
|
1124
|
+
|
|
1125
|
+
// Step 6: Ollama offer (dense embeddings)
|
|
1126
|
+
export function renderOllamaSection(state: OllamaOfferState): void {
|
|
1127
|
+
out(divider(7, "Embeddings"));
|
|
1128
|
+
blank();
|
|
1129
|
+
if (state.alreadyConfigured) {
|
|
1130
|
+
out(wrap(
|
|
1131
|
+
"Embeddings endpoint already configured via ASHLR_EMBED_URL or " +
|
|
1132
|
+
"OLLAMA_HOST. Skipping."
|
|
1133
|
+
));
|
|
1134
|
+
out("[ASHLR_OK] ollama-already-configured");
|
|
1135
|
+
blank();
|
|
1136
|
+
return;
|
|
1137
|
+
}
|
|
1138
|
+
out(wrap(
|
|
1139
|
+
"ashlr can route grep queries through local Ollama for dense " +
|
|
1140
|
+
"embeddings — ~10x better semantic recall than BM25 on larger repos, " +
|
|
1141
|
+
"100% local, zero cost."
|
|
1142
|
+
));
|
|
1143
|
+
blank();
|
|
1144
|
+
if (state.installed) {
|
|
1145
|
+
out(wrap(
|
|
1146
|
+
"Ollama detected on PATH. We can wire it up by writing " +
|
|
1147
|
+
"ASHLR_EMBED_URL=http://localhost:11434/api/embeddings to " +
|
|
1148
|
+
`${state.configPath}.`
|
|
1149
|
+
));
|
|
1150
|
+
blank();
|
|
1151
|
+
out("[ASHLR_PROMPT: Enable dense embeddings via Ollama? (y/n, default y)]");
|
|
1152
|
+
} else {
|
|
1153
|
+
out(wrap(
|
|
1154
|
+
"Ollama not detected. Install from https://ollama.com (free, " +
|
|
1155
|
+
"~150MB), then re-run this wizard or execute /ashlr-ollama-setup."
|
|
1156
|
+
));
|
|
1157
|
+
out("[ASHLR_OK] ollama-not-installed");
|
|
1158
|
+
}
|
|
1159
|
+
blank();
|
|
1160
|
+
}
|
|
1161
|
+
|
|
1162
|
+
// Step 7: pro teaser
|
|
1163
|
+
//
|
|
1164
|
+
// Renders different content based on tier:
|
|
1165
|
+
// - Free users: 30-second team-genome value prop + inline "Try Pro?" prompt.
|
|
1166
|
+
// - Pro/Team users: team-invite tip only.
|
|
1167
|
+
//
|
|
1168
|
+
// Returns the conversion outcome for telemetry: "y" | "n" | "skip".
|
|
1169
|
+
export async function renderProTeaser(
|
|
1170
|
+
opts: { isPro?: boolean; interactive?: boolean } = {},
|
|
1171
|
+
): Promise<"y" | "n" | "skip"> {
|
|
1172
|
+
out(divider(8, "Pro plan"));
|
|
1173
|
+
blank();
|
|
1174
|
+
|
|
1175
|
+
if (opts.isPro) {
|
|
1176
|
+
// Already Pro — show team-invite tip instead.
|
|
1177
|
+
out(wrap(
|
|
1178
|
+
"You're on Pro. Run /ashlr-team-invite to bring your teammates aboard " +
|
|
1179
|
+
"and share your genome across the whole team."
|
|
1180
|
+
));
|
|
1181
|
+
blank();
|
|
1182
|
+
return "skip";
|
|
1183
|
+
}
|
|
1184
|
+
|
|
1185
|
+
// Free tier — team-genome value prop.
|
|
1186
|
+
out(wrap(
|
|
1187
|
+
"Free ashlr builds a genome that lives on your machine. Pro lifts it " +
|
|
1188
|
+
"into a shared encrypted layer so every teammate learns from everyone " +
|
|
1189
|
+
"else's sessions — new engineers onboard into context, not silence."
|
|
1190
|
+
));
|
|
1191
|
+
blank();
|
|
1192
|
+
out("Pro also includes:");
|
|
1193
|
+
out(" - Weekly team digest of genome learnings and streak milestones");
|
|
1194
|
+
out(" - Cross-machine savings sync and /ashlr-dashboard history");
|
|
1195
|
+
out(" - Cloud LLM summarizer (no local Ollama needed)");
|
|
1196
|
+
blank();
|
|
1197
|
+
out("Pro: $12/mo · Pro Team: $24/user/mo (min 3) · 7-day free trial, no card.");
|
|
1198
|
+
blank();
|
|
1199
|
+
|
|
1200
|
+
// Inline conversion: prompt unless non-interactive.
|
|
1201
|
+
if (opts.interactive === false) {
|
|
1202
|
+
out("Run /ashlr-upgrade to start a 7-day free Pro trial in 90 seconds.");
|
|
1203
|
+
blank();
|
|
1204
|
+
return "skip";
|
|
1205
|
+
}
|
|
1206
|
+
|
|
1207
|
+
out("[ASHLR_PROMPT: Try Pro free for 7 days? (y/N)]");
|
|
1208
|
+
const answer = await askYesNo(
|
|
1209
|
+
"Try Pro free for 7 days?",
|
|
1210
|
+
false, // default: no
|
|
1211
|
+
YES_TIMEOUT_MS,
|
|
1212
|
+
true, // interactive
|
|
1213
|
+
);
|
|
1214
|
+
|
|
1215
|
+
if (answer) {
|
|
1216
|
+
blank();
|
|
1217
|
+
out(wrap(
|
|
1218
|
+
"Starting the upgrade flow. Launching /ashlr-upgrade — this takes " +
|
|
1219
|
+
"about 90 seconds."
|
|
1220
|
+
));
|
|
1221
|
+
blank();
|
|
1222
|
+
out("[ASHLR_ACTION: run /ashlr-upgrade]");
|
|
1223
|
+
blank();
|
|
1224
|
+
return "y";
|
|
1225
|
+
} else {
|
|
1226
|
+
blank();
|
|
1227
|
+
out("No worries. Run /ashlr-upgrade any time to start your free trial.");
|
|
1228
|
+
blank();
|
|
1229
|
+
return "n";
|
|
1230
|
+
}
|
|
1231
|
+
}
|
|
1232
|
+
|
|
1233
|
+
// Step 8: final message
|
|
1234
|
+
export function renderFinalMessage(): void {
|
|
1235
|
+
out(divider(9, "Done"));
|
|
1236
|
+
blank();
|
|
1237
|
+
out("▬".repeat(WIDTH));
|
|
1238
|
+
out(wrap(
|
|
1239
|
+
"Run /ashlr-savings anytime to see running totals. The status " +
|
|
1240
|
+
"line at the bottom of your terminal shows live counters."
|
|
1241
|
+
));
|
|
1242
|
+
out("Happy coding.");
|
|
1243
|
+
out("▬".repeat(WIDTH));
|
|
1244
|
+
blank();
|
|
1245
|
+
}
|
|
1246
|
+
|
|
1247
|
+
// ---------------------------------------------------------------------------
|
|
1248
|
+
// Main wizard orchestrator
|
|
1249
|
+
// ---------------------------------------------------------------------------
|
|
1250
|
+
|
|
1251
|
+
export interface SkippedStep {
|
|
1252
|
+
step: string;
|
|
1253
|
+
reason: string;
|
|
1254
|
+
/** What to run to activate this feature. */
|
|
1255
|
+
hint: string;
|
|
1256
|
+
}
|
|
1257
|
+
|
|
1258
|
+
export interface WizardOpts {
|
|
1259
|
+
interactive: boolean;
|
|
1260
|
+
home?: string;
|
|
1261
|
+
cwd?: string;
|
|
1262
|
+
pluginRoot?: string;
|
|
1263
|
+
/** Override permission installer call (for tests) */
|
|
1264
|
+
installPermsFn?: () => Promise<void>;
|
|
1265
|
+
/** Override status-line installer call (for tests) */
|
|
1266
|
+
installStatusLineFn?: () => Promise<void>;
|
|
1267
|
+
/** Override genome init call (for tests) */
|
|
1268
|
+
genomeInitFn?: () => Promise<void>;
|
|
1269
|
+
/**
|
|
1270
|
+
* Override the real `ashlr__read` demo subprocess (for tests). When set
|
|
1271
|
+
* the wizard calls this instead of spawning the MCP server.
|
|
1272
|
+
*/
|
|
1273
|
+
realReadDemoFn?: (demoFile: string) => Promise<RealReadDemoResult>;
|
|
1274
|
+
/**
|
|
1275
|
+
* Override the Ollama config writer (for tests) so the real HOME isn't
|
|
1276
|
+
* mutated and the test can observe the call.
|
|
1277
|
+
*/
|
|
1278
|
+
enableOllamaFn?: () => Promise<{ ok: boolean; path: string; error?: string }>;
|
|
1279
|
+
}
|
|
1280
|
+
|
|
1281
|
+
export async function runWizard(opts: WizardOpts): Promise<void> {
|
|
1282
|
+
const home = opts.home ?? homedir();
|
|
1283
|
+
const cwd = opts.cwd ?? process.cwd();
|
|
1284
|
+
const interactive = opts.interactive;
|
|
1285
|
+
|
|
1286
|
+
// Telemetry helper — respects ASHLR_SESSION_LOG=0 kill switch via logEvent.
|
|
1287
|
+
async function emitWizardStep(
|
|
1288
|
+
step_name: string,
|
|
1289
|
+
outcome: "completed" | "skipped" | "error",
|
|
1290
|
+
): Promise<void> {
|
|
1291
|
+
try {
|
|
1292
|
+
const { logEvent } = await import("../servers/_events.ts");
|
|
1293
|
+
await logEvent("wizard_step", {
|
|
1294
|
+
tool: "onboarding-wizard",
|
|
1295
|
+
extra: { step_name, outcome, ts: Date.now() },
|
|
1296
|
+
});
|
|
1297
|
+
} catch {
|
|
1298
|
+
/* best-effort — never block the wizard */
|
|
1299
|
+
}
|
|
1300
|
+
}
|
|
1301
|
+
|
|
1302
|
+
// Track wizard steps that were silently skipped so we can surface them
|
|
1303
|
+
// in a summary at the end. Each entry has a step name, a skip reason,
|
|
1304
|
+
// and a one-liner on what to run to activate the feature later.
|
|
1305
|
+
const skipped: SkippedStep[] = [];
|
|
1306
|
+
|
|
1307
|
+
// Record that wizard has started so session-start banner can show "finish setup".
|
|
1308
|
+
markOnboardingStarted(home);
|
|
1309
|
+
|
|
1310
|
+
// --- Greeting ---
|
|
1311
|
+
renderGreeting();
|
|
1312
|
+
await emitWizardStep("intro", "completed");
|
|
1313
|
+
|
|
1314
|
+
// --- Step 1: Doctor ---
|
|
1315
|
+
const doctor = await runDoctorCheck({ home, cwd, pluginRoot: opts.pluginRoot });
|
|
1316
|
+
renderDoctorOutput(doctor);
|
|
1317
|
+
markOnboardingStep(1, home);
|
|
1318
|
+
await emitWizardStep("doctor", doctor.issues.length === 0 ? "completed" : "error");
|
|
1319
|
+
|
|
1320
|
+
// --- Step 2: Permissions ---
|
|
1321
|
+
renderPermissionsSection(doctor.allowlistOk);
|
|
1322
|
+
markOnboardingStep(2, home);
|
|
1323
|
+
if (!doctor.allowlistOk) {
|
|
1324
|
+
// 30-second visible countdown so the grant can't feel silent. Swapped
|
|
1325
|
+
// from the generic 5s askYesNo because users reported missing the fact
|
|
1326
|
+
// that permissions were granted on their behalf.
|
|
1327
|
+
const doInstall = await askYesNoWithCountdown(
|
|
1328
|
+
"Auto-approve all ashlr tools?",
|
|
1329
|
+
PERMISSIONS_COUNTDOWN_MS,
|
|
1330
|
+
interactive,
|
|
1331
|
+
);
|
|
1332
|
+
if (doInstall) {
|
|
1333
|
+
if (opts.installPermsFn) {
|
|
1334
|
+
await opts.installPermsFn();
|
|
1335
|
+
} else {
|
|
1336
|
+
const pluginRoot = opts.pluginRoot ?? resolvePluginRoot();
|
|
1337
|
+
const { installPermissions } = await import("./install-permissions.ts");
|
|
1338
|
+
try {
|
|
1339
|
+
const result = await installPermissions({ pluginRoot });
|
|
1340
|
+
if (result.added.length > 0) {
|
|
1341
|
+
out(
|
|
1342
|
+
wrap(
|
|
1343
|
+
`Added ${result.added.length} permission entr${result.added.length === 1 ? "y" : "ies"}.`
|
|
1344
|
+
)
|
|
1345
|
+
);
|
|
1346
|
+
} else {
|
|
1347
|
+
out("All ashlr permissions already present.");
|
|
1348
|
+
}
|
|
1349
|
+
} catch {
|
|
1350
|
+
out("[ASHLR_WARN] Permission install failed — run /ashlr-allow manually.");
|
|
1351
|
+
}
|
|
1352
|
+
}
|
|
1353
|
+
renderRestartCallout();
|
|
1354
|
+
await emitWizardStep("permissions", "completed");
|
|
1355
|
+
} else {
|
|
1356
|
+
out(wrap(
|
|
1357
|
+
"Skipped. Run /ashlr-allow any time to add permissions."
|
|
1358
|
+
));
|
|
1359
|
+
skipped.push({
|
|
1360
|
+
step: "Permissions",
|
|
1361
|
+
reason: "ashlr tools require per-call approval (increases friction)",
|
|
1362
|
+
hint: "run /ashlr-allow to auto-approve all ashlr tools",
|
|
1363
|
+
});
|
|
1364
|
+
await emitWizardStep("permissions", "skipped");
|
|
1365
|
+
}
|
|
1366
|
+
blank();
|
|
1367
|
+
} else {
|
|
1368
|
+
await emitWizardStep("permissions", "completed");
|
|
1369
|
+
}
|
|
1370
|
+
|
|
1371
|
+
// --- Step 3: Status line ---
|
|
1372
|
+
// Check if status line is already configured in ~/.claude/settings.json.
|
|
1373
|
+
let statusLineInstalled = false;
|
|
1374
|
+
try {
|
|
1375
|
+
const settingsPath = join(home, ".claude", "settings.json");
|
|
1376
|
+
if (existsSync(settingsPath)) {
|
|
1377
|
+
const raw = readFileSync(settingsPath, "utf-8");
|
|
1378
|
+
const s = JSON.parse(raw) as { statusLine?: unknown };
|
|
1379
|
+
const sl = s?.statusLine as { command?: string } | undefined;
|
|
1380
|
+
statusLineInstalled = !!(sl?.command && sl.command.includes("savings-status-line"));
|
|
1381
|
+
}
|
|
1382
|
+
} catch {
|
|
1383
|
+
/* treat as not installed */
|
|
1384
|
+
}
|
|
1385
|
+
|
|
1386
|
+
renderStatusLineSection(statusLineInstalled);
|
|
1387
|
+
markOnboardingStep(3, home);
|
|
1388
|
+
|
|
1389
|
+
if (!statusLineInstalled) {
|
|
1390
|
+
// Default ON — opt-out with "n". User presses Enter or "y" to install.
|
|
1391
|
+
const doStatusLine = await askYesNo(
|
|
1392
|
+
"Install status line?",
|
|
1393
|
+
true, // default yes
|
|
1394
|
+
YES_TIMEOUT_MS,
|
|
1395
|
+
interactive,
|
|
1396
|
+
);
|
|
1397
|
+
if (doStatusLine) {
|
|
1398
|
+
if (opts.installStatusLineFn) {
|
|
1399
|
+
await opts.installStatusLineFn();
|
|
1400
|
+
} else {
|
|
1401
|
+
const pluginRoot = opts.pluginRoot ?? resolvePluginRoot();
|
|
1402
|
+
const { spawnSync } = await import("child_process");
|
|
1403
|
+
const res = spawnSync(
|
|
1404
|
+
"bun",
|
|
1405
|
+
["run", join(pluginRoot, "scripts/install-status-line.ts")],
|
|
1406
|
+
{ stdio: ["ignore", "pipe", "pipe"], timeout: 15_000 },
|
|
1407
|
+
);
|
|
1408
|
+
if (res.status === 0) {
|
|
1409
|
+
out("Status line installed.");
|
|
1410
|
+
} else {
|
|
1411
|
+
out("[ASHLR_WARN] Status line install failed — run /ashlr-allow manually.");
|
|
1412
|
+
}
|
|
1413
|
+
}
|
|
1414
|
+
renderRestartCallout();
|
|
1415
|
+
await emitWizardStep("status_line", "completed");
|
|
1416
|
+
} else {
|
|
1417
|
+
out(wrap("Skipped. Run /ashlr-status-line any time to install."));
|
|
1418
|
+
skipped.push({
|
|
1419
|
+
step: "Status line",
|
|
1420
|
+
reason: "live savings counter not active",
|
|
1421
|
+
hint: "run bun scripts/install-status-line.ts, then restart Claude Code",
|
|
1422
|
+
});
|
|
1423
|
+
await emitWizardStep("status_line", "skipped");
|
|
1424
|
+
}
|
|
1425
|
+
blank();
|
|
1426
|
+
} else {
|
|
1427
|
+
await emitWizardStep("status_line", "completed");
|
|
1428
|
+
}
|
|
1429
|
+
|
|
1430
|
+
// --- Step 4: Live demo ---
|
|
1431
|
+
// Attempt the real ashlr__read call first so users see actual bytes returned.
|
|
1432
|
+
// Fall back to the snipCompact estimate only when spawn fails or no .ts file.
|
|
1433
|
+
const demoFile = findDemoFile(cwd);
|
|
1434
|
+
const sizeBytes = demoFile ? fileSizeBytes(demoFile) : 0;
|
|
1435
|
+
let payloadBytes = estimateReadPayload(sizeBytes);
|
|
1436
|
+
let demoReal = false;
|
|
1437
|
+
let demoSample: string | null = null;
|
|
1438
|
+
let demoError: string | null = null;
|
|
1439
|
+
if (demoFile) {
|
|
1440
|
+
// Print a status line BEFORE the spawn so users on slow laptops don't
|
|
1441
|
+
// sit in front of a blank terminal while Bun cold-starts the
|
|
1442
|
+
// efficiency-server. Without this hint a 5–10s pause looks like a hang.
|
|
1443
|
+
process.stdout.write("Warming up efficiency-server (cold-start can take a few seconds)...\n");
|
|
1444
|
+
try {
|
|
1445
|
+
const realFn = opts.realReadDemoFn ?? ((p: string) => runRealReadDemo(p, { pluginRoot: opts.pluginRoot }));
|
|
1446
|
+
const real = await realFn(demoFile);
|
|
1447
|
+
if (real.error || real.payloadBytes === null) {
|
|
1448
|
+
demoError = real.error ?? "unknown";
|
|
1449
|
+
} else {
|
|
1450
|
+
payloadBytes = real.payloadBytes;
|
|
1451
|
+
demoReal = true;
|
|
1452
|
+
demoSample = real.sample;
|
|
1453
|
+
}
|
|
1454
|
+
} catch (err) {
|
|
1455
|
+
demoError = err instanceof Error ? err.message : String(err);
|
|
1456
|
+
}
|
|
1457
|
+
}
|
|
1458
|
+
renderLiveDemoSection(demoFile, sizeBytes, payloadBytes, {
|
|
1459
|
+
real: demoReal,
|
|
1460
|
+
sample: demoSample,
|
|
1461
|
+
error: demoError,
|
|
1462
|
+
});
|
|
1463
|
+
|
|
1464
|
+
// --- Step 5: Genome offer ---
|
|
1465
|
+
const srcFileCount = countSourceFiles(cwd);
|
|
1466
|
+
const ahaDemo = computeGenomeAhaDemo(demoFile, sizeBytes);
|
|
1467
|
+
renderGenomeSection(srcFileCount, doctor.genomePresent, ahaDemo);
|
|
1468
|
+
|
|
1469
|
+
if (!doctor.genomePresent) {
|
|
1470
|
+
// Default flips from "yes" on healthy-size repos to "no" on small ones —
|
|
1471
|
+
// small/greenfield projects can opt in but won't get genomes forced on
|
|
1472
|
+
// them. renderGenomeSection() prints a tailored prompt for each case.
|
|
1473
|
+
const defaultYes = srcFileCount >= 10;
|
|
1474
|
+
const doGenome = await askYesNo(
|
|
1475
|
+
defaultYes ? "Initialize a genome?" : "Initialize a genome anyway?",
|
|
1476
|
+
defaultYes,
|
|
1477
|
+
YES_TIMEOUT_MS,
|
|
1478
|
+
interactive,
|
|
1479
|
+
);
|
|
1480
|
+
if (doGenome) {
|
|
1481
|
+
if (opts.genomeInitFn) {
|
|
1482
|
+
await opts.genomeInitFn();
|
|
1483
|
+
} else {
|
|
1484
|
+
const pluginRoot = opts.pluginRoot ?? resolvePluginRoot();
|
|
1485
|
+
out(wrap(
|
|
1486
|
+
"Running /ashlr-genome-init... " +
|
|
1487
|
+
"(this may take 15-30 seconds on large repos)"
|
|
1488
|
+
));
|
|
1489
|
+
const { spawnSync } = await import("child_process");
|
|
1490
|
+
const res = spawnSync(
|
|
1491
|
+
"bun",
|
|
1492
|
+
["run", join(pluginRoot, "scripts/genome-init.ts"), "--dir", cwd, "--minimal"],
|
|
1493
|
+
{ stdio: ["ignore", "pipe", "pipe"], timeout: 60_000 },
|
|
1494
|
+
);
|
|
1495
|
+
if (res.status === 0) {
|
|
1496
|
+
out("Genome initialized.");
|
|
1497
|
+
} else {
|
|
1498
|
+
out("[ASHLR_WARN] Genome init failed — run /ashlr-genome-init manually.");
|
|
1499
|
+
}
|
|
1500
|
+
}
|
|
1501
|
+
await emitWizardStep("genome_init", "completed");
|
|
1502
|
+
} else {
|
|
1503
|
+
out(wrap("Skipped. Run /ashlr-genome-init any time to index this project."));
|
|
1504
|
+
await emitWizardStep("genome_init", "skipped");
|
|
1505
|
+
}
|
|
1506
|
+
blank();
|
|
1507
|
+
} else {
|
|
1508
|
+
await emitWizardStep("genome_init", "completed");
|
|
1509
|
+
}
|
|
1510
|
+
|
|
1511
|
+
// --- Step 6: Telemetry opt-in ---
|
|
1512
|
+
// Strictly opt-in. Default OFF. Respect any prior explicit choice — never
|
|
1513
|
+
// re-prompt an already-decided user. See docs/telemetry.md for contract.
|
|
1514
|
+
markOnboardingStep(6, home);
|
|
1515
|
+
const telemetryState = detectTelemetryState(home);
|
|
1516
|
+
renderTelemetrySection(telemetryState);
|
|
1517
|
+
if (!telemetryState.alreadyOptedIn && !telemetryState.alreadyOptedOut) {
|
|
1518
|
+
const optIn = await askYesNo(
|
|
1519
|
+
"Share anonymized session stats?",
|
|
1520
|
+
false, // default: NO (be conservative on consent)
|
|
1521
|
+
YES_TIMEOUT_MS,
|
|
1522
|
+
interactive,
|
|
1523
|
+
);
|
|
1524
|
+
const choice: "opt-in" | "off" = optIn ? "opt-in" : "off";
|
|
1525
|
+
const res = await writeTelemetryChoice(choice, home);
|
|
1526
|
+
if (!res.ok) {
|
|
1527
|
+
out(`[ASHLR_WARN] Could not write telemetry choice — ${res.error ?? "unknown error"}`);
|
|
1528
|
+
} else if (optIn) {
|
|
1529
|
+
out(wrap(`Telemetry enabled. Wrote { "telemetry": "opt-in" } to ${res.path}.`));
|
|
1530
|
+
} else {
|
|
1531
|
+
out(wrap("Telemetry stays off. Nothing will be collected."));
|
|
1532
|
+
}
|
|
1533
|
+
await emitWizardStep("telemetry", optIn ? "completed" : "skipped");
|
|
1534
|
+
blank();
|
|
1535
|
+
} else {
|
|
1536
|
+
await emitWizardStep("telemetry", "completed");
|
|
1537
|
+
}
|
|
1538
|
+
|
|
1539
|
+
// --- Step 7: Ollama / dense embeddings offer ---
|
|
1540
|
+
markOnboardingStep(7, home);
|
|
1541
|
+
const ollamaState = detectOllamaState(home);
|
|
1542
|
+
renderOllamaSection(ollamaState);
|
|
1543
|
+
if (!ollamaState.alreadyConfigured && !ollamaState.installed) {
|
|
1544
|
+
// Ollama not installed — track as a skipped step so the summary
|
|
1545
|
+
// surfaces it with an install hint.
|
|
1546
|
+
skipped.push({
|
|
1547
|
+
step: "Dense embeddings (Ollama)",
|
|
1548
|
+
reason: "Ollama not found on PATH",
|
|
1549
|
+
hint: "brew install ollama (or visit https://ollama.com), then re-run /ashlr-start",
|
|
1550
|
+
});
|
|
1551
|
+
}
|
|
1552
|
+
if (!ollamaState.alreadyConfigured && ollamaState.installed) {
|
|
1553
|
+
const doEnable = await askYesNo(
|
|
1554
|
+
"Enable dense embeddings via Ollama?",
|
|
1555
|
+
true,
|
|
1556
|
+
YES_TIMEOUT_MS,
|
|
1557
|
+
interactive,
|
|
1558
|
+
);
|
|
1559
|
+
if (doEnable) {
|
|
1560
|
+
const enableFn = opts.enableOllamaFn ?? (() => enableOllamaEmbeddings(home));
|
|
1561
|
+
const res = await enableFn();
|
|
1562
|
+
if (res.ok) {
|
|
1563
|
+
out(wrap(`Wrote ASHLR_EMBED_URL to ${res.path}. Restart Claude Code to pick it up.`));
|
|
1564
|
+
} else {
|
|
1565
|
+
out(`[ASHLR_WARN] Could not write Ollama config — ${res.error ?? "unknown error"}`);
|
|
1566
|
+
}
|
|
1567
|
+
} else {
|
|
1568
|
+
out(wrap("Skipped. Run /ashlr-ollama-setup any time to revisit."));
|
|
1569
|
+
skipped.push({
|
|
1570
|
+
step: "Dense embeddings (Ollama)",
|
|
1571
|
+
reason: "declined during wizard",
|
|
1572
|
+
hint: "run /ashlr-ollama-setup any time to enable",
|
|
1573
|
+
});
|
|
1574
|
+
}
|
|
1575
|
+
blank();
|
|
1576
|
+
}
|
|
1577
|
+
|
|
1578
|
+
// Check GitHub CLI auth status and track as skipped if not logged in.
|
|
1579
|
+
const ghAuthed = detectGhAuthState();
|
|
1580
|
+
if (!ghAuthed) {
|
|
1581
|
+
skipped.push({
|
|
1582
|
+
step: "GitHub integration",
|
|
1583
|
+
reason: "gh CLI not authenticated",
|
|
1584
|
+
hint: "run: gh auth login",
|
|
1585
|
+
});
|
|
1586
|
+
}
|
|
1587
|
+
|
|
1588
|
+
// --- Step 8: Pro teaser ---
|
|
1589
|
+
markOnboardingStep(8, home);
|
|
1590
|
+
// Detect tier via sync best-effort check (non-blocking; wizard never waits on network).
|
|
1591
|
+
let isPro = false;
|
|
1592
|
+
try {
|
|
1593
|
+
const { isProSync } = await import("../servers/_pro.ts");
|
|
1594
|
+
isPro = isProSync(home);
|
|
1595
|
+
} catch {
|
|
1596
|
+
/* best-effort — treat as free on any error */
|
|
1597
|
+
}
|
|
1598
|
+
const proTeaserOutcome = await renderProTeaser({ isPro, interactive });
|
|
1599
|
+
// Emit conversion telemetry: tool_call with tool_name "wizard_pro_pitch" or
|
|
1600
|
+
// "wizard_pro_team_pitch" depending on whether user is already Pro.
|
|
1601
|
+
try {
|
|
1602
|
+
const { logEvent } = await import("../servers/_events.ts");
|
|
1603
|
+
const toolName = isPro ? "wizard_pro_team_pitch" : "wizard_pro_pitch";
|
|
1604
|
+
await logEvent("tool_call", {
|
|
1605
|
+
tool: toolName,
|
|
1606
|
+
extra: { outcome: proTeaserOutcome, ts: Date.now() },
|
|
1607
|
+
});
|
|
1608
|
+
} catch {
|
|
1609
|
+
/* best-effort */
|
|
1610
|
+
}
|
|
1611
|
+
await emitWizardStep("pro_teaser", "completed");
|
|
1612
|
+
|
|
1613
|
+
// --- Step 9: Final ---
|
|
1614
|
+
// Always print the loud RESTART REQUIRED callout at the end — addresses
|
|
1615
|
+
// UX-audit cliff #2 where users finish the wizard, ignore the soft hint,
|
|
1616
|
+
// and find their next tool call falls through to built-in Read/Edit/Grep.
|
|
1617
|
+
// Also writes ~/.ashlr/restart-required so the next SessionStart hook can
|
|
1618
|
+
// detect a missed restart and re-warn (same shell pid → user did not quit).
|
|
1619
|
+
renderRestartCallout(home);
|
|
1620
|
+
renderFinalMessage();
|
|
1621
|
+
markOnboardingCompleted(home);
|
|
1622
|
+
// WAD-D lead indicator: stamp the onboarding-completion timestamp on
|
|
1623
|
+
// stats.json so the next daily heartbeat can ship it. Best-effort —
|
|
1624
|
+
// never break the wizard.
|
|
1625
|
+
try {
|
|
1626
|
+
const { markOnboardingComplete } = await import("../servers/_stats.ts");
|
|
1627
|
+
await markOnboardingComplete();
|
|
1628
|
+
} catch {
|
|
1629
|
+
/* best-effort */
|
|
1630
|
+
}
|
|
1631
|
+
await emitWizardStep("complete", "completed");
|
|
1632
|
+
|
|
1633
|
+
// Skipped-features summary: print a "Heads up" block whenever any
|
|
1634
|
+
// wizard steps were silently bypassed. Each item gets a one-liner on
|
|
1635
|
+
// what to run later so users aren't left wondering.
|
|
1636
|
+
if (skipped.length > 0) {
|
|
1637
|
+
out("▬".repeat(WIDTH));
|
|
1638
|
+
out("Heads up — these features aren't active yet:");
|
|
1639
|
+
blank();
|
|
1640
|
+
for (const s of skipped) {
|
|
1641
|
+
out(` • ${s.step}: ${s.reason}`);
|
|
1642
|
+
out(` → ${s.hint}`);
|
|
1643
|
+
}
|
|
1644
|
+
blank();
|
|
1645
|
+
out("▬".repeat(WIDTH));
|
|
1646
|
+
blank();
|
|
1647
|
+
}
|
|
1648
|
+
}
|
|
1649
|
+
|
|
1650
|
+
// ---------------------------------------------------------------------------
|
|
1651
|
+
// --reset mode
|
|
1652
|
+
// ---------------------------------------------------------------------------
|
|
1653
|
+
|
|
1654
|
+
async function handleReset(home: string): Promise<void> {
|
|
1655
|
+
await deleteStamp(home);
|
|
1656
|
+
process.stdout.write(
|
|
1657
|
+
`Stamp deleted: ${stampPath(home)}\n` +
|
|
1658
|
+
"Next session will trigger the onboarding wizard again.\n",
|
|
1659
|
+
);
|
|
1660
|
+
}
|
|
1661
|
+
|
|
1662
|
+
// ---------------------------------------------------------------------------
|
|
1663
|
+
// CLI entry
|
|
1664
|
+
// ---------------------------------------------------------------------------
|
|
1665
|
+
|
|
1666
|
+
async function main(): Promise<number> {
|
|
1667
|
+
const args = process.argv.slice(2);
|
|
1668
|
+
const noInteractive = args.includes("--no-interactive");
|
|
1669
|
+
const reset = args.includes("--reset");
|
|
1670
|
+
const home = homedir();
|
|
1671
|
+
|
|
1672
|
+
if (reset) {
|
|
1673
|
+
await handleReset(home);
|
|
1674
|
+
return 0;
|
|
1675
|
+
}
|
|
1676
|
+
|
|
1677
|
+
try {
|
|
1678
|
+
await runWizard({ interactive: !noInteractive, home });
|
|
1679
|
+
return 0;
|
|
1680
|
+
} catch (err) {
|
|
1681
|
+
process.stderr.write(
|
|
1682
|
+
`ashlr onboarding-wizard: fatal error — ${err instanceof Error ? err.message : String(err)}\n`,
|
|
1683
|
+
);
|
|
1684
|
+
return 1;
|
|
1685
|
+
}
|
|
1686
|
+
}
|
|
1687
|
+
|
|
1688
|
+
if (import.meta.main) {
|
|
1689
|
+
const code = await main();
|
|
1690
|
+
process.exit(code);
|
|
1691
|
+
}
|