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,172 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* _genome-commits.ts — surface commit sections in ashlr__grep retrieval.
|
|
3
|
+
*
|
|
4
|
+
* Loads commit sections from the v2 manifest, scores them against a query
|
|
5
|
+
* with the same lightweight keyword scorer the static-section retriever
|
|
6
|
+
* uses (tags > title > summary, IDF-ish boost), and returns a small
|
|
7
|
+
* sorted slice for the grep handler to prepend to its output.
|
|
8
|
+
*
|
|
9
|
+
* Commit sections are ADDITIVE — they extend the corpus, never replace it.
|
|
10
|
+
* The grep handler formats them with a distinct `[commit <sha> - <date>]`
|
|
11
|
+
* prefix so users can tell history from static code.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { readFile } from "fs/promises";
|
|
15
|
+
import { existsSync } from "fs";
|
|
16
|
+
import { join } from "path";
|
|
17
|
+
import {
|
|
18
|
+
type CommitSection,
|
|
19
|
+
type GenomeManifestV2,
|
|
20
|
+
type SectionMetaV2,
|
|
21
|
+
loadManifestV2,
|
|
22
|
+
commitSectionAbsPath,
|
|
23
|
+
} from "./_manifest-v2";
|
|
24
|
+
import { formatFreshness } from "./_genome-freshness";
|
|
25
|
+
|
|
26
|
+
// ---------------------------------------------------------------------------
|
|
27
|
+
// Tokenization + scoring (mirrors core-efficiency/genome/retriever.ts)
|
|
28
|
+
// ---------------------------------------------------------------------------
|
|
29
|
+
|
|
30
|
+
export function tokenize(text: string): string[] {
|
|
31
|
+
return text
|
|
32
|
+
.toLowerCase()
|
|
33
|
+
.replace(/[^a-z0-9\s-]/g, " ")
|
|
34
|
+
.split(/\s+/)
|
|
35
|
+
.filter((t) => t.length > 2);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function scoreSectionMeta(meta: SectionMetaV2, queryTerms: Set<string>): number {
|
|
39
|
+
let score = 0;
|
|
40
|
+
const tagSet = new Set(meta.tags.map((t) => t.toLowerCase()));
|
|
41
|
+
const titleTerms = new Set(tokenize(meta.title));
|
|
42
|
+
const summaryTerms = new Set(tokenize(meta.summary));
|
|
43
|
+
for (const term of queryTerms) {
|
|
44
|
+
if (tagSet.has(term)) score += 3;
|
|
45
|
+
if (titleTerms.has(term)) score += 2;
|
|
46
|
+
if (summaryTerms.has(term)) score += 1;
|
|
47
|
+
}
|
|
48
|
+
return score;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// ---------------------------------------------------------------------------
|
|
52
|
+
// Public API
|
|
53
|
+
// ---------------------------------------------------------------------------
|
|
54
|
+
|
|
55
|
+
export interface RetrievedCommit {
|
|
56
|
+
sha: string;
|
|
57
|
+
shortSha: string;
|
|
58
|
+
date: string;
|
|
59
|
+
message: string;
|
|
60
|
+
summary: string;
|
|
61
|
+
filesChanged: string[];
|
|
62
|
+
score: number;
|
|
63
|
+
/**
|
|
64
|
+
* v2 freshness — ISO timestamp of the manifest section's last refresh.
|
|
65
|
+
* Optional so legacy callers (and v1 manifests not yet re-saved) keep
|
|
66
|
+
* working without rendering a badge.
|
|
67
|
+
*/
|
|
68
|
+
lastUpdatedAt?: string;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Retrieve up to `limit` commit sections matching `query`.
|
|
73
|
+
*
|
|
74
|
+
* Returns an empty array when:
|
|
75
|
+
* - no genome exists,
|
|
76
|
+
* - the manifest is v1-only (no commit sections recorded),
|
|
77
|
+
* - no commit section's keywords overlap the query.
|
|
78
|
+
*
|
|
79
|
+
* Never throws — best-effort retrieval, callers should fall through to
|
|
80
|
+
* static-section results when this returns [].
|
|
81
|
+
*/
|
|
82
|
+
export async function retrieveCommitSections(
|
|
83
|
+
cwd: string,
|
|
84
|
+
query: string,
|
|
85
|
+
limit = 3,
|
|
86
|
+
): Promise<RetrievedCommit[]> {
|
|
87
|
+
try {
|
|
88
|
+
const manifest = await loadManifestV2(cwd);
|
|
89
|
+
if (!manifest) return [];
|
|
90
|
+
|
|
91
|
+
const commitMetas = manifest.sections.filter((s) => s.kind === "commit");
|
|
92
|
+
if (commitMetas.length === 0) return [];
|
|
93
|
+
|
|
94
|
+
const queryTerms = new Set(tokenize(query));
|
|
95
|
+
let scored: Array<{ meta: SectionMetaV2; score: number }>;
|
|
96
|
+
|
|
97
|
+
if (queryTerms.size === 0) {
|
|
98
|
+
// No meaningful query — surface the most recent commits.
|
|
99
|
+
scored = commitMetas.map((m) => ({ meta: m, score: 1 }));
|
|
100
|
+
} else {
|
|
101
|
+
scored = commitMetas
|
|
102
|
+
.map((m) => ({ meta: m, score: scoreSectionMeta(m, queryTerms) }))
|
|
103
|
+
.filter((s) => s.score > 0);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
if (scored.length === 0) return [];
|
|
107
|
+
|
|
108
|
+
// Sort by score desc, then date desc (most recent wins ties).
|
|
109
|
+
scored.sort((a, b) => {
|
|
110
|
+
if (b.score !== a.score) return b.score - a.score;
|
|
111
|
+
const ad = a.meta.lastUpdatedAt ?? a.meta.updatedAt;
|
|
112
|
+
const bd = b.meta.lastUpdatedAt ?? b.meta.updatedAt;
|
|
113
|
+
return bd > ad ? 1 : bd < ad ? -1 : 0;
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
const out: RetrievedCommit[] = [];
|
|
117
|
+
for (const { meta, score } of scored.slice(0, limit)) {
|
|
118
|
+
const sha = meta.path.replace(/^commits\//, "").replace(/\.json$/, "");
|
|
119
|
+
const abs = commitSectionAbsPath(cwd, sha);
|
|
120
|
+
if (!existsSync(abs)) continue;
|
|
121
|
+
let payload: CommitSection;
|
|
122
|
+
try {
|
|
123
|
+
payload = JSON.parse(await readFile(abs, "utf-8")) as CommitSection;
|
|
124
|
+
} catch {
|
|
125
|
+
continue;
|
|
126
|
+
}
|
|
127
|
+
out.push({
|
|
128
|
+
sha: payload.sha,
|
|
129
|
+
shortSha: payload.sha.slice(0, 7),
|
|
130
|
+
date: payload.date,
|
|
131
|
+
message: payload.message,
|
|
132
|
+
summary: payload.summary,
|
|
133
|
+
filesChanged: payload.filesChanged,
|
|
134
|
+
score,
|
|
135
|
+
// Surface the section's manifest-level freshness so callers can
|
|
136
|
+
// render a [fresh|stale: Xd] badge on the commit header.
|
|
137
|
+
lastUpdatedAt: meta.lastUpdatedAt ?? meta.updatedAt,
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
return out;
|
|
141
|
+
} catch {
|
|
142
|
+
return [];
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Format a list of retrieved commit sections as a markdown block suitable for
|
|
148
|
+
* prepending to grep output. Each commit is clearly labeled with
|
|
149
|
+
* `[commit <sha> - <date>]` so the user knows it's history, not static code.
|
|
150
|
+
*/
|
|
151
|
+
export function formatCommitsForPrompt(
|
|
152
|
+
commits: RetrievedCommit[],
|
|
153
|
+
now: Date = new Date(),
|
|
154
|
+
): string {
|
|
155
|
+
if (commits.length === 0) return "";
|
|
156
|
+
const parts = commits.map((c) => {
|
|
157
|
+
const dateShort = c.date ? c.date.slice(0, 10) : "unknown";
|
|
158
|
+
const files =
|
|
159
|
+
c.filesChanged.length === 0
|
|
160
|
+
? ""
|
|
161
|
+
: `\n_files: ${c.filesChanged.slice(0, 6).join(", ")}${c.filesChanged.length > 6 ? ` (+${c.filesChanged.length - 6})` : ""}_\n`;
|
|
162
|
+
const subject = c.message.split("\n")[0] ?? c.shortSha;
|
|
163
|
+
// Q2 prep — emit a freshness badge inside the bracketed header so the
|
|
164
|
+
// model + user can see at a glance whether the retrieved commit is
|
|
165
|
+
// current. Falls back to no badge when lastUpdatedAt is absent (legacy).
|
|
166
|
+
const badge = formatFreshness(c.lastUpdatedAt, now);
|
|
167
|
+
const badgeFragment = badge ? ` \u00b7 ${badge.slice(1, -1)}` : "";
|
|
168
|
+
return `### [commit ${c.shortSha} - ${dateShort}${badgeFragment}] ${subject}${files}
|
|
169
|
+
${c.summary}`;
|
|
170
|
+
});
|
|
171
|
+
return `## Commit History\n\n${parts.join("\n\n---\n\n")}`;
|
|
172
|
+
}
|
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* _genome-crypto-v2.ts — client-side v2 envelope encryption for team genomes.
|
|
3
|
+
*
|
|
4
|
+
* The server (Phase T1) stores opaque wrapped-DEK envelopes keyed by
|
|
5
|
+
* `(genome_id, member_user_id)`. This module implements the crypto that
|
|
6
|
+
* the CLIENT does on either side of that store:
|
|
7
|
+
*
|
|
8
|
+
* - Admin: wraps the team DEK for each recipient's X25519 pubkey once,
|
|
9
|
+
* then uploads the envelope via POST /genome/:id/key-envelope.
|
|
10
|
+
* - Member: fetches their envelope via GET /genome/:id/key-envelope,
|
|
11
|
+
* unwraps with their own X25519 privkey, uses the DEK to
|
|
12
|
+
* decrypt genome section content via the existing v1 AES-GCM
|
|
13
|
+
* content-encryption path.
|
|
14
|
+
*
|
|
15
|
+
* Construction:
|
|
16
|
+
* 1. Generate ephemeral X25519 keypair.
|
|
17
|
+
* 2. ECDH with recipient pubkey → shared secret (32 bytes).
|
|
18
|
+
* 3. HKDF-SHA256(secret, salt=empty, info="ashlr.genome.v2") → 32-byte AES key.
|
|
19
|
+
* 4. AES-256-GCM(key, nonce=12B random, aad="ashlr.genome.v2") wraps the DEK.
|
|
20
|
+
* 5. Envelope layout (base64url, concatenated):
|
|
21
|
+
* [ 1 byte version=0x01 ]
|
|
22
|
+
* [ 32 byte ephemeral-public ]
|
|
23
|
+
* [ 12 byte nonce ]
|
|
24
|
+
* [ N byte ciphertext ]
|
|
25
|
+
* [ 16 byte auth tag ]
|
|
26
|
+
*
|
|
27
|
+
* Why ephemeral: forward secrecy per envelope (compromising the recipient's
|
|
28
|
+
* private key later does not let an attacker decrypt a captured envelope
|
|
29
|
+
* unless they also captured the ephemeral private at wrap time, which we
|
|
30
|
+
* discard).
|
|
31
|
+
*
|
|
32
|
+
* Why HKDF(info="ashlr.genome.v2"): domain separation from any future v3.
|
|
33
|
+
*/
|
|
34
|
+
|
|
35
|
+
import {
|
|
36
|
+
createCipheriv,
|
|
37
|
+
createDecipheriv,
|
|
38
|
+
createPrivateKey,
|
|
39
|
+
createPublicKey,
|
|
40
|
+
diffieHellman,
|
|
41
|
+
generateKeyPairSync,
|
|
42
|
+
hkdfSync,
|
|
43
|
+
randomBytes,
|
|
44
|
+
} from "crypto";
|
|
45
|
+
|
|
46
|
+
const VERSION = 0x01;
|
|
47
|
+
const HKDF_INFO = Buffer.from("ashlr.genome.v2", "utf-8");
|
|
48
|
+
const AEAD_AAD = Buffer.from("ashlr.genome.v2", "utf-8");
|
|
49
|
+
const AEAD_NONCE_LEN = 12;
|
|
50
|
+
const AEAD_TAG_LEN = 16;
|
|
51
|
+
|
|
52
|
+
// ---------------------------------------------------------------------------
|
|
53
|
+
// base64url helpers — Node 20+ has native support via Buffer.toString("base64url").
|
|
54
|
+
// ---------------------------------------------------------------------------
|
|
55
|
+
|
|
56
|
+
function b64url(b: Buffer): string { return b.toString("base64url"); }
|
|
57
|
+
function fromB64url(s: string): Buffer { return Buffer.from(s, "base64url"); }
|
|
58
|
+
|
|
59
|
+
// ---------------------------------------------------------------------------
|
|
60
|
+
// X25519 keypair generation
|
|
61
|
+
// ---------------------------------------------------------------------------
|
|
62
|
+
|
|
63
|
+
export interface KeyPair {
|
|
64
|
+
/** Raw 32-byte X25519 public key, base64url-encoded. */
|
|
65
|
+
publicKey: string;
|
|
66
|
+
/** Raw 32-byte X25519 private key, base64url-encoded. Keep offline. */
|
|
67
|
+
privateKey: string;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Generate an X25519 keypair. Keys are returned as 32-byte raw values in
|
|
72
|
+
* base64url. Both sides of the wire use the same encoding.
|
|
73
|
+
*/
|
|
74
|
+
export function generateKeyPair(): KeyPair {
|
|
75
|
+
const { publicKey, privateKey } = generateKeyPairSync("x25519");
|
|
76
|
+
// Node's key objects are DER-wrapped by default; we want raw 32-byte values.
|
|
77
|
+
const pubDer = publicKey.export({ format: "der", type: "spki" });
|
|
78
|
+
const privDer = privateKey.export({ format: "der", type: "pkcs8" });
|
|
79
|
+
return {
|
|
80
|
+
// The last 32 bytes of an X25519 SPKI DER blob are the raw public key.
|
|
81
|
+
publicKey: b64url(pubDer.subarray(pubDer.length - 32)),
|
|
82
|
+
// The last 32 bytes of an X25519 PKCS8 DER blob are the raw private key.
|
|
83
|
+
privateKey: b64url(privDer.subarray(privDer.length - 32)),
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// ---------------------------------------------------------------------------
|
|
88
|
+
// Raw-key → Node KeyObject helpers
|
|
89
|
+
// ---------------------------------------------------------------------------
|
|
90
|
+
|
|
91
|
+
// RFC 8410 DER prefix for an X25519 public key SPKI: 12 bytes, followed by the
|
|
92
|
+
// 32-byte raw public key. Hand-rolled because Node's API wants the full SPKI.
|
|
93
|
+
const X25519_PUB_SPKI_PREFIX = Buffer.from([
|
|
94
|
+
0x30, 0x2a, 0x30, 0x05, 0x06, 0x03, 0x2b, 0x65, 0x6e, 0x03, 0x21, 0x00,
|
|
95
|
+
]);
|
|
96
|
+
|
|
97
|
+
// RFC 8410 DER prefix for an X25519 private key PKCS8: 16 bytes, followed by
|
|
98
|
+
// a 2-byte OCTET STRING header + 32-byte raw private key.
|
|
99
|
+
const X25519_PRIV_PKCS8_PREFIX = Buffer.from([
|
|
100
|
+
0x30, 0x2e, 0x02, 0x01, 0x00, 0x30, 0x05, 0x06, 0x03, 0x2b, 0x65, 0x6e,
|
|
101
|
+
0x04, 0x22, 0x04, 0x20,
|
|
102
|
+
]);
|
|
103
|
+
|
|
104
|
+
function rawToPubKeyObject(raw32: Buffer) {
|
|
105
|
+
if (raw32.length !== 32) throw new Error("X25519 pubkey must be 32 bytes");
|
|
106
|
+
return createPublicKey({
|
|
107
|
+
key: Buffer.concat([X25519_PUB_SPKI_PREFIX, raw32]),
|
|
108
|
+
format: "der",
|
|
109
|
+
type: "spki",
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function rawToPrivKeyObject(raw32: Buffer) {
|
|
114
|
+
if (raw32.length !== 32) throw new Error("X25519 privkey must be 32 bytes");
|
|
115
|
+
return createPrivateKey({
|
|
116
|
+
key: Buffer.concat([X25519_PRIV_PKCS8_PREFIX, raw32]),
|
|
117
|
+
format: "der",
|
|
118
|
+
type: "pkcs8",
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// ---------------------------------------------------------------------------
|
|
123
|
+
// Wrap / Unwrap
|
|
124
|
+
// ---------------------------------------------------------------------------
|
|
125
|
+
|
|
126
|
+
export const ENVELOPE_ALG = "x25519-hkdf-sha256-aes256gcm-v1";
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Wrap a 32-byte DEK for a recipient whose raw X25519 pubkey we have.
|
|
130
|
+
* Returns a base64url-encoded envelope string.
|
|
131
|
+
*/
|
|
132
|
+
export function wrapDek(dek: Buffer, recipientPubB64url: string): string {
|
|
133
|
+
if (dek.length !== 32) throw new Error("DEK must be 32 bytes");
|
|
134
|
+
|
|
135
|
+
const recipientPubRaw = fromB64url(recipientPubB64url);
|
|
136
|
+
if (recipientPubRaw.length !== 32) {
|
|
137
|
+
throw new Error("recipient pubkey must decode to 32 bytes");
|
|
138
|
+
}
|
|
139
|
+
const recipientPub = rawToPubKeyObject(recipientPubRaw);
|
|
140
|
+
|
|
141
|
+
// Ephemeral keypair → ECDH → HKDF → AES key.
|
|
142
|
+
const eph = generateKeyPairSync("x25519");
|
|
143
|
+
const ephPubRaw = eph.publicKey
|
|
144
|
+
.export({ format: "der", type: "spki" })
|
|
145
|
+
.subarray(-32);
|
|
146
|
+
|
|
147
|
+
const shared = diffieHellman({ privateKey: eph.privateKey, publicKey: recipientPub });
|
|
148
|
+
const aesKey = Buffer.from(hkdfSync("sha256", shared, Buffer.alloc(0), HKDF_INFO, 32));
|
|
149
|
+
|
|
150
|
+
const nonce = randomBytes(AEAD_NONCE_LEN);
|
|
151
|
+
const cipher = createCipheriv("aes-256-gcm", aesKey, nonce);
|
|
152
|
+
cipher.setAAD(AEAD_AAD);
|
|
153
|
+
const ct = Buffer.concat([cipher.update(dek), cipher.final()]);
|
|
154
|
+
const tag = cipher.getAuthTag();
|
|
155
|
+
|
|
156
|
+
const envelope = Buffer.concat([
|
|
157
|
+
Buffer.from([VERSION]),
|
|
158
|
+
ephPubRaw,
|
|
159
|
+
nonce,
|
|
160
|
+
ct,
|
|
161
|
+
tag,
|
|
162
|
+
]);
|
|
163
|
+
return b64url(envelope);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* Unwrap an envelope produced by wrapDek. Throws on version mismatch, length
|
|
168
|
+
* mismatch, or AEAD tag mismatch (including wrong recipient privkey).
|
|
169
|
+
*/
|
|
170
|
+
export function unwrapDek(envelopeB64url: string, recipientPrivB64url: string): Buffer {
|
|
171
|
+
const envelope = fromB64url(envelopeB64url);
|
|
172
|
+
// 1 version + 32 eph-pub + 12 nonce + 16 tag = 61 min; plus ciphertext (= DEK size).
|
|
173
|
+
if (envelope.length < 1 + 32 + AEAD_NONCE_LEN + AEAD_TAG_LEN) {
|
|
174
|
+
throw new Error("envelope too short");
|
|
175
|
+
}
|
|
176
|
+
if (envelope[0] !== VERSION) {
|
|
177
|
+
throw new Error(`unsupported envelope version ${envelope[0]}`);
|
|
178
|
+
}
|
|
179
|
+
const ephPubRaw = envelope.subarray(1, 33);
|
|
180
|
+
const nonce = envelope.subarray(33, 33 + AEAD_NONCE_LEN);
|
|
181
|
+
const ct = envelope.subarray(33 + AEAD_NONCE_LEN, envelope.length - AEAD_TAG_LEN);
|
|
182
|
+
const tag = envelope.subarray(envelope.length - AEAD_TAG_LEN);
|
|
183
|
+
|
|
184
|
+
const recipientPrivRaw = fromB64url(recipientPrivB64url);
|
|
185
|
+
if (recipientPrivRaw.length !== 32) {
|
|
186
|
+
throw new Error("recipient privkey must decode to 32 bytes");
|
|
187
|
+
}
|
|
188
|
+
const recipientPriv = rawToPrivKeyObject(recipientPrivRaw);
|
|
189
|
+
const ephPub = rawToPubKeyObject(ephPubRaw);
|
|
190
|
+
|
|
191
|
+
const shared = diffieHellman({ privateKey: recipientPriv, publicKey: ephPub });
|
|
192
|
+
const aesKey = Buffer.from(hkdfSync("sha256", shared, Buffer.alloc(0), HKDF_INFO, 32));
|
|
193
|
+
|
|
194
|
+
const decipher = createDecipheriv("aes-256-gcm", aesKey, nonce);
|
|
195
|
+
decipher.setAAD(AEAD_AAD);
|
|
196
|
+
decipher.setAuthTag(tag);
|
|
197
|
+
const pt = Buffer.concat([decipher.update(ct), decipher.final()]);
|
|
198
|
+
if (pt.length !== 32) throw new Error("unwrapped DEK has wrong length");
|
|
199
|
+
return pt;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
// ---------------------------------------------------------------------------
|
|
203
|
+
// Local key storage — ~/.ashlr/member-keys/<userId>.json
|
|
204
|
+
// ---------------------------------------------------------------------------
|
|
205
|
+
|
|
206
|
+
import { chmodSync, existsSync, mkdirSync, readFileSync, writeFileSync } from "fs";
|
|
207
|
+
import { homedir } from "os";
|
|
208
|
+
import { dirname, join } from "path";
|
|
209
|
+
|
|
210
|
+
export function memberKeyPath(userId: string): string {
|
|
211
|
+
return join(process.env.HOME ?? homedir(), ".ashlr", "member-keys", `${userId}.json`);
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
export interface StoredKeypair {
|
|
215
|
+
userId: string;
|
|
216
|
+
publicKey: string;
|
|
217
|
+
privateKey: string;
|
|
218
|
+
alg: string; // "x25519-v1"
|
|
219
|
+
createdAt: string; // ISO
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/**
|
|
223
|
+
* Persist a keypair to disk. File mode 0600 because it contains the private
|
|
224
|
+
* key. Directory created lazily with mode 0700.
|
|
225
|
+
*/
|
|
226
|
+
export function saveKeypair(kp: StoredKeypair): string {
|
|
227
|
+
const path = memberKeyPath(kp.userId);
|
|
228
|
+
mkdirSync(dirname(path), { recursive: true, mode: 0o700 });
|
|
229
|
+
writeFileSync(path, JSON.stringify(kp, null, 2), { encoding: "utf-8", mode: 0o600 });
|
|
230
|
+
try { chmodSync(path, 0o600); } catch { /* best-effort on fs that doesn't support it */ }
|
|
231
|
+
return path;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
export function loadKeypair(userId: string): StoredKeypair | null {
|
|
235
|
+
const path = memberKeyPath(userId);
|
|
236
|
+
if (!existsSync(path)) return null;
|
|
237
|
+
try {
|
|
238
|
+
return JSON.parse(readFileSync(path, "utf-8")) as StoredKeypair;
|
|
239
|
+
} catch {
|
|
240
|
+
return null;
|
|
241
|
+
}
|
|
242
|
+
}
|
|
@@ -0,0 +1,259 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* _genome-crypto.ts — Client-side AES-256-GCM encryption for team genome sections.
|
|
3
|
+
*
|
|
4
|
+
* Threat model:
|
|
5
|
+
* - Server operator cannot read section content without the team key.
|
|
6
|
+
* - Compromised Redis / S3 / Postgres exposes only ciphertext + nonce + auth tag.
|
|
7
|
+
* - TLS protects the transport layer; encryption here provides defence-in-depth.
|
|
8
|
+
* - A user who leaves the team loses access to new content after a key rotation.
|
|
9
|
+
*
|
|
10
|
+
* v1 model: one symmetric AES-256-GCM key per genome, stored at
|
|
11
|
+
* ~/.ashlr/team-keys/<genomeId>.key (mode 0600, 32 raw bytes)
|
|
12
|
+
*
|
|
13
|
+
* Wire format (serializeBlob):
|
|
14
|
+
* base64url(<version_byte> : <12-byte nonce> : <16-byte authTag> : <ciphertext>)
|
|
15
|
+
* The version prefix allows future algorithm migration without breaking parsers.
|
|
16
|
+
*
|
|
17
|
+
* Multi-user model (v2, not shipped):
|
|
18
|
+
* Each member holds an X25519 keypair. The team key is wrapped per member:
|
|
19
|
+
* envelope[memberId] = encrypt(memberPublicKey, teamKey)
|
|
20
|
+
* Admin manages envelopes. Member rotation = re-wrap envelope (no re-encrypt needed).
|
|
21
|
+
* Full protocol documented in docs/team-genome.md.
|
|
22
|
+
*
|
|
23
|
+
* SECURITY: This module NEVER logs key material, plaintext content, or nonces.
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
import { createCipheriv, createDecipheriv, randomBytes } from "crypto";
|
|
27
|
+
import { readFile, writeFile, mkdir, chmod } from "fs/promises";
|
|
28
|
+
import { existsSync } from "fs";
|
|
29
|
+
import { join, dirname } from "path";
|
|
30
|
+
import { homedir } from "os";
|
|
31
|
+
|
|
32
|
+
// Emit a per-file warning on Windows since POSIX chmod has no effect on NTFS.
|
|
33
|
+
// Note: Node/Bun's `writeFile(..., { mode: 0o600 })` silently ignores `mode`
|
|
34
|
+
// on Windows, so the file lands with whatever ACLs NTFS inherits from the
|
|
35
|
+
// parent directory. For `~/.ashlr/team-keys/` under the user profile, default
|
|
36
|
+
// ACLs typically restrict to the user + Administrators + SYSTEM — but that
|
|
37
|
+
// depends on parent-directory state (roaming profiles, domain policies, or
|
|
38
|
+
// user-created dirs may have broader ACLs). We warn per-key-file so a user
|
|
39
|
+
// rotating keys sees the reminder each time; don't throw because the file
|
|
40
|
+
// is written and functional.
|
|
41
|
+
//
|
|
42
|
+
// The user should protect `~/.ashlr/team-keys/` via BitLocker, Windows EFS,
|
|
43
|
+
// or explicit `icacls` ACLs. Document this in docs/platform-support.md.
|
|
44
|
+
function warnWindowsChmod(p: string): void {
|
|
45
|
+
process.stderr.write(
|
|
46
|
+
`[ashlr-genome-crypto] WARNING: Windows — chmod 0600 is a no-op on NTFS. ` +
|
|
47
|
+
`Key file at ${p} inherits parent-directory ACLs (typically user-only in ` +
|
|
48
|
+
`~/.ashlr/ but not guaranteed). Protect with BitLocker or EFS.\n`,
|
|
49
|
+
);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// ---------------------------------------------------------------------------
|
|
53
|
+
// Constants
|
|
54
|
+
// ---------------------------------------------------------------------------
|
|
55
|
+
|
|
56
|
+
const KEY_BYTES = 32; // AES-256
|
|
57
|
+
const NONCE_BYTES = 12; // GCM recommended nonce size
|
|
58
|
+
const AUTH_TAG_BYTES = 16; // GCM authentication tag (128-bit)
|
|
59
|
+
const BLOB_VERSION = 1;
|
|
60
|
+
|
|
61
|
+
// Regex that matches our serialized blob format (base64url)
|
|
62
|
+
// Used by the server to detect encrypted vs plaintext content.
|
|
63
|
+
export const ENCRYPTED_BLOB_RE = /^[A-Za-z0-9_-]{20,}$/;
|
|
64
|
+
|
|
65
|
+
// ---------------------------------------------------------------------------
|
|
66
|
+
// Types
|
|
67
|
+
// ---------------------------------------------------------------------------
|
|
68
|
+
|
|
69
|
+
export interface EncryptedBlob {
|
|
70
|
+
version: number;
|
|
71
|
+
nonce: Buffer;
|
|
72
|
+
authTag: Buffer;
|
|
73
|
+
ciphertext: Buffer;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// ---------------------------------------------------------------------------
|
|
77
|
+
// Key generation
|
|
78
|
+
// ---------------------------------------------------------------------------
|
|
79
|
+
|
|
80
|
+
/** Generate a new 32-byte AES-256 team key. */
|
|
81
|
+
export function generateKey(): Buffer {
|
|
82
|
+
return randomBytes(KEY_BYTES);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// ---------------------------------------------------------------------------
|
|
86
|
+
// Encrypt / Decrypt
|
|
87
|
+
// ---------------------------------------------------------------------------
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Encrypt a plaintext section with AES-256-GCM.
|
|
91
|
+
* Each call uses a fresh random nonce — never reuse a nonce with the same key.
|
|
92
|
+
*/
|
|
93
|
+
export function encryptSection(plaintext: string, key: Buffer): EncryptedBlob {
|
|
94
|
+
if (key.length !== KEY_BYTES) {
|
|
95
|
+
throw new Error(`[ashlr-genome-crypto] Key must be ${KEY_BYTES} bytes`);
|
|
96
|
+
}
|
|
97
|
+
const nonce = randomBytes(NONCE_BYTES);
|
|
98
|
+
const cipher = createCipheriv("aes-256-gcm", key, nonce);
|
|
99
|
+
const ct = Buffer.concat([cipher.update(plaintext, "utf-8"), cipher.final()]);
|
|
100
|
+
const authTag = cipher.getAuthTag();
|
|
101
|
+
return { version: BLOB_VERSION, nonce, authTag, ciphertext: ct };
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Decrypt a section blob.
|
|
106
|
+
* Throws if the auth tag does not match (tampered data or wrong key).
|
|
107
|
+
* Throws on version mismatch.
|
|
108
|
+
*/
|
|
109
|
+
export function decryptSection(blob: EncryptedBlob, key: Buffer): string {
|
|
110
|
+
if (key.length !== KEY_BYTES) {
|
|
111
|
+
throw new Error(`[ashlr-genome-crypto] Key must be ${KEY_BYTES} bytes`);
|
|
112
|
+
}
|
|
113
|
+
if (blob.version !== BLOB_VERSION) {
|
|
114
|
+
throw new Error(`[ashlr-genome-crypto] Unknown blob version: ${blob.version}`);
|
|
115
|
+
}
|
|
116
|
+
const decipher = createDecipheriv("aes-256-gcm", key, blob.nonce);
|
|
117
|
+
decipher.setAuthTag(blob.authTag);
|
|
118
|
+
try {
|
|
119
|
+
const plain = Buffer.concat([decipher.update(blob.ciphertext), decipher.final()]);
|
|
120
|
+
return plain.toString("utf-8");
|
|
121
|
+
} catch {
|
|
122
|
+
// Node throws "Unsupported state or unable to authenticate data" on auth tag failure.
|
|
123
|
+
throw new Error("[ashlr-genome-crypto] Decryption failed — wrong key or corrupt data");
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// ---------------------------------------------------------------------------
|
|
128
|
+
// Serialization
|
|
129
|
+
// ---------------------------------------------------------------------------
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Serialize an EncryptedBlob to a compact base64url string.
|
|
133
|
+
* Layout: [ version(1) | nonce(12) | authTag(16) | ciphertext(variable) ]
|
|
134
|
+
*/
|
|
135
|
+
export function serializeBlob(blob: EncryptedBlob): string {
|
|
136
|
+
const header = Buffer.alloc(1 + NONCE_BYTES + AUTH_TAG_BYTES);
|
|
137
|
+
header.writeUInt8(blob.version, 0);
|
|
138
|
+
blob.nonce.copy(header, 1);
|
|
139
|
+
blob.authTag.copy(header, 1 + NONCE_BYTES);
|
|
140
|
+
const full = Buffer.concat([header, blob.ciphertext]);
|
|
141
|
+
// base64url (no padding, URL-safe)
|
|
142
|
+
return full.toString("base64url");
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Parse a base64url blob string back into an EncryptedBlob.
|
|
147
|
+
* Throws on malformed input.
|
|
148
|
+
*/
|
|
149
|
+
export function parseBlob(str: string): EncryptedBlob {
|
|
150
|
+
let buf: Buffer;
|
|
151
|
+
try {
|
|
152
|
+
buf = Buffer.from(str, "base64url");
|
|
153
|
+
} catch {
|
|
154
|
+
throw new Error("[ashlr-genome-crypto] Invalid blob: base64url decode failed");
|
|
155
|
+
}
|
|
156
|
+
const minLen = 1 + NONCE_BYTES + AUTH_TAG_BYTES;
|
|
157
|
+
if (buf.length < minLen) {
|
|
158
|
+
throw new Error(`[ashlr-genome-crypto] Invalid blob: too short (${buf.length} bytes)`);
|
|
159
|
+
}
|
|
160
|
+
const version = buf.readUInt8(0);
|
|
161
|
+
const nonce = buf.subarray(1, 1 + NONCE_BYTES);
|
|
162
|
+
const authTag = buf.subarray(1 + NONCE_BYTES, 1 + NONCE_BYTES + AUTH_TAG_BYTES);
|
|
163
|
+
const ciphertext = buf.subarray(1 + NONCE_BYTES + AUTH_TAG_BYTES);
|
|
164
|
+
return {
|
|
165
|
+
version,
|
|
166
|
+
nonce: Buffer.from(nonce),
|
|
167
|
+
authTag: Buffer.from(authTag),
|
|
168
|
+
ciphertext: Buffer.from(ciphertext),
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// ---------------------------------------------------------------------------
|
|
173
|
+
// Key file helpers
|
|
174
|
+
// ---------------------------------------------------------------------------
|
|
175
|
+
|
|
176
|
+
function keyFilePath(genomeId: string): string {
|
|
177
|
+
return join(homedir(), ".ashlr", "team-keys", `${genomeId}.key`);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* Load a team key from ~/.ashlr/team-keys/<genomeId>.key.
|
|
182
|
+
* Returns null if the file does not exist.
|
|
183
|
+
* Throws if the file exists but is malformed.
|
|
184
|
+
*/
|
|
185
|
+
export async function loadKey(genomeId: string): Promise<Buffer | null> {
|
|
186
|
+
const p = keyFilePath(genomeId);
|
|
187
|
+
if (!existsSync(p)) return null;
|
|
188
|
+
const raw = await readFile(p);
|
|
189
|
+
if (raw.length !== KEY_BYTES) {
|
|
190
|
+
throw new Error(
|
|
191
|
+
`[ashlr-genome-crypto] Key file at ${p} has wrong length (${raw.length} bytes, expected ${KEY_BYTES})`,
|
|
192
|
+
);
|
|
193
|
+
}
|
|
194
|
+
return Buffer.from(raw);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* Save a team key to ~/.ashlr/team-keys/<genomeId>.key with mode 0600.
|
|
199
|
+
* Creates parent directories as needed.
|
|
200
|
+
*/
|
|
201
|
+
export async function saveKey(genomeId: string, key: Buffer): Promise<void> {
|
|
202
|
+
if (key.length !== KEY_BYTES) {
|
|
203
|
+
throw new Error(`[ashlr-genome-crypto] Key must be ${KEY_BYTES} bytes`);
|
|
204
|
+
}
|
|
205
|
+
const p = keyFilePath(genomeId);
|
|
206
|
+
await mkdir(dirname(p), { recursive: true });
|
|
207
|
+
await writeFile(p, key, { mode: 0o600 });
|
|
208
|
+
if (process.platform === "win32") {
|
|
209
|
+
warnWindowsChmod(p);
|
|
210
|
+
} else {
|
|
211
|
+
// chmod explicitly in case writeFile mode is masked by umask
|
|
212
|
+
await chmod(p, 0o600);
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
// ---------------------------------------------------------------------------
|
|
217
|
+
// Base32 helpers (for human-friendly key export/import)
|
|
218
|
+
// ---------------------------------------------------------------------------
|
|
219
|
+
|
|
220
|
+
// RFC 4648 base32 alphabet (uppercase, no padding issues with 32-byte keys)
|
|
221
|
+
const BASE32_ALPHABET = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567";
|
|
222
|
+
|
|
223
|
+
/** Encode a Buffer as base32 (no padding). */
|
|
224
|
+
export function encodeBase32(buf: Buffer): string {
|
|
225
|
+
let bits = 0;
|
|
226
|
+
let value = 0;
|
|
227
|
+
let output = "";
|
|
228
|
+
for (const byte of buf) {
|
|
229
|
+
value = (value << 8) | byte;
|
|
230
|
+
bits += 8;
|
|
231
|
+
while (bits >= 5) {
|
|
232
|
+
output += BASE32_ALPHABET[(value >>> (bits - 5)) & 31];
|
|
233
|
+
bits -= 5;
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
if (bits > 0) {
|
|
237
|
+
output += BASE32_ALPHABET[(value << (5 - bits)) & 31];
|
|
238
|
+
}
|
|
239
|
+
return output;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
/** Decode a base32 string to a Buffer. Throws on invalid characters. */
|
|
243
|
+
export function decodeBase32(str: string): Buffer {
|
|
244
|
+
const s = str.toUpperCase().replace(/\s/g, "");
|
|
245
|
+
const bytes: number[] = [];
|
|
246
|
+
let bits = 0;
|
|
247
|
+
let value = 0;
|
|
248
|
+
for (const ch of s) {
|
|
249
|
+
const idx = BASE32_ALPHABET.indexOf(ch);
|
|
250
|
+
if (idx < 0) throw new Error(`[ashlr-genome-crypto] Invalid base32 character: ${ch}`);
|
|
251
|
+
value = (value << 5) | idx;
|
|
252
|
+
bits += 5;
|
|
253
|
+
if (bits >= 8) {
|
|
254
|
+
bytes.push((value >>> (bits - 8)) & 255);
|
|
255
|
+
bits -= 8;
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
return Buffer.from(bytes);
|
|
259
|
+
}
|