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.
Files changed (499) hide show
  1. package/.agents/plugins/marketplace.json +20 -0
  2. package/.claude-plugin/marketplace.json +39 -0
  3. package/.claude-plugin/plugin.json +36 -0
  4. package/.codex/agents/README.md +9 -0
  5. package/.codex/agents/ashlr-explorer.md +36 -0
  6. package/.codex/agents/ashlr-worker.md +38 -0
  7. package/.codex-plugin/plugin.json +45 -0
  8. package/.mcp.json +14 -0
  9. package/LICENSE +21 -0
  10. package/README.md +311 -0
  11. package/agents/ashlr-code.md +69 -0
  12. package/agents/ashlr-explore.md +44 -0
  13. package/agents/ashlr-plan.md +54 -0
  14. package/bench/fixtures/notebook-large.ipynb +660 -0
  15. package/bench/fixtures/notebook-small.ipynb +39 -0
  16. package/bench/fixtures/tasklist-closed.json +62 -0
  17. package/bench/fixtures/tasklist-large-mixed.json +27 -0
  18. package/bench/fixtures/tasklist-sprint-open.json +79 -0
  19. package/bench/fixtures/websearch-bun-runtime.json +32 -0
  20. package/bench/fixtures/websearch-react-hooks-patterns.json +32 -0
  21. package/bench/fixtures/websearch-typescript-generics.json +38 -0
  22. package/bench/refs/node-sdk/.refrev +1 -0
  23. package/bench/refs/node-sdk/README.md +17 -0
  24. package/bench/refs/node-sdk/src/agent/agent.ts +163 -0
  25. package/bench/refs/node-sdk/src/agent/create-agent-ui-stream.ts +83 -0
  26. package/bench/refs/node-sdk/src/agent/tool-loop-agent.ts +238 -0
  27. package/bench/refs/node-sdk/src/embed/embed-many.ts +409 -0
  28. package/bench/refs/node-sdk/src/embed/embed-result.ts +50 -0
  29. package/bench/refs/node-sdk/src/embed/embed.ts +257 -0
  30. package/bench/refs/node-sdk/src/error/invalid-argument-error.ts +34 -0
  31. package/bench/refs/node-sdk/src/error/invalid-tool-input-error.ts +33 -0
  32. package/bench/refs/node-sdk/src/error/no-object-generated-error.ts +70 -0
  33. package/bench/refs/node-sdk/src/generate-image/generate-image-result.ts +42 -0
  34. package/bench/refs/node-sdk/src/generate-image/generate-image.ts +362 -0
  35. package/bench/refs/node-sdk/src/generate-object/generate-object-result.ts +67 -0
  36. package/bench/refs/node-sdk/src/generate-object/generate-object.ts +501 -0
  37. package/bench/refs/node-sdk/src/generate-object/inject-json-instruction.ts +30 -0
  38. package/bench/refs/node-sdk/src/generate-object/output-strategy.ts +415 -0
  39. package/bench/refs/node-sdk/src/generate-object/repair-text.ts +12 -0
  40. package/bench/refs/node-sdk/src/generate-object/stream-object.ts +960 -0
  41. package/bench/refs/node-sdk/src/generate-speech/generate-speech.ts +191 -0
  42. package/bench/refs/node-sdk/src/generate-text/generate-text.ts +1484 -0
  43. package/bench/refs/node-sdk/src/generate-text/stream-text.ts +2708 -0
  44. package/bench/refs/node-sdk/src/generate-text/tool-call.ts +59 -0
  45. package/bench/refs/node-sdk/src/generate-text/tool-result.ts +36 -0
  46. package/bench/refs/node-sdk/src/telemetry/create-telemetry-dispatcher.ts +146 -0
  47. package/bench/refs/node-sdk/src/telemetry/telemetry-registry.ts +15 -0
  48. package/bench/refs/node-sdk/src/telemetry/telemetry.ts +223 -0
  49. package/bench/refs/node-sdk/src/util/async-iterable-stream.ts +94 -0
  50. package/bench/refs/node-sdk/src/util/consume-stream.ts +31 -0
  51. package/bench/refs/node-sdk/src/util/create-stitchable-stream.ts +112 -0
  52. package/bench/refs/node-sdk/src/util/download/download.ts +74 -0
  53. package/bench/refs/node-sdk/src/util/error-handler.ts +1 -0
  54. package/bench/refs/node-sdk/src/util/fix-json.ts +401 -0
  55. package/bench/refs/node-sdk/src/util/merge-objects.ts +79 -0
  56. package/bench/refs/python-lib/.refrev +1 -0
  57. package/bench/refs/python-lib/README.md +18 -0
  58. package/bench/refs/python-lib/pandas/core/_numba/executor.py +249 -0
  59. package/bench/refs/python-lib/pandas/core/_numba/extensions.py +586 -0
  60. package/bench/refs/python-lib/pandas/core/_numba/kernels/mean_.py +198 -0
  61. package/bench/refs/python-lib/pandas/core/_numba/kernels/min_max_.py +178 -0
  62. package/bench/refs/python-lib/pandas/core/_numba/kernels/sum_.py +255 -0
  63. package/bench/refs/python-lib/pandas/core/_numba/kernels/var_.py +251 -0
  64. package/bench/refs/python-lib/pandas/core/accessor.py +520 -0
  65. package/bench/refs/python-lib/pandas/core/api.py +138 -0
  66. package/bench/refs/python-lib/pandas/core/array_algos/masked_accumulations.py +97 -0
  67. package/bench/refs/python-lib/pandas/core/array_algos/masked_reductions.py +252 -0
  68. package/bench/refs/python-lib/pandas/core/array_algos/putmask.py +150 -0
  69. package/bench/refs/python-lib/pandas/core/array_algos/quantile.py +232 -0
  70. package/bench/refs/python-lib/pandas/core/array_algos/replace.py +159 -0
  71. package/bench/refs/python-lib/pandas/core/array_algos/take.py +609 -0
  72. package/bench/refs/python-lib/pandas/core/arraylike.py +534 -0
  73. package/bench/refs/python-lib/pandas/core/arrays/_arrow_string_mixins.py +434 -0
  74. package/bench/refs/python-lib/pandas/core/arrays/_mixins.py +630 -0
  75. package/bench/refs/python-lib/pandas/core/arrays/_ranges.py +276 -0
  76. package/bench/refs/python-lib/pandas/core/arrays/_utils.py +77 -0
  77. package/bench/refs/python-lib/pandas/core/arrays/arrow/accessors.py +521 -0
  78. package/bench/refs/python-lib/pandas/core/arrays/arrow/extension_types.py +174 -0
  79. package/bench/refs/python-lib/pandas/core/arrays/boolean.py +454 -0
  80. package/bench/refs/python-lib/pandas/core/arrays/floating.py +192 -0
  81. package/bench/refs/python-lib/pandas/core/arrays/integer.py +296 -0
  82. package/bench/refs/python-lib/pandas/core/arrays/masked.py +2140 -0
  83. package/bench/refs/python-lib/pandas/core/arrays/numeric.py +305 -0
  84. package/bench/refs/python-lib/pandas/core/arrays/numpy_.py +701 -0
  85. package/bench/refs/python-lib/pandas/core/arrays/period.py +1586 -0
  86. package/bench/refs/python-lib/pandas/core/arrays/sparse/accessor.py +505 -0
  87. package/bench/refs/python-lib/pandas/core/arrays/sparse/scipy_sparse.py +208 -0
  88. package/bench/refs/rust-project/.refrev +1 -0
  89. package/bench/refs/rust-project/README.md +17 -0
  90. package/bench/refs/rust-project/tokio/src/doc/os.rs +68 -0
  91. package/bench/refs/rust-project/tokio/src/fs/dir_builder.rs +129 -0
  92. package/bench/refs/rust-project/tokio/src/fs/file.rs +1113 -0
  93. package/bench/refs/rust-project/tokio/src/fs/mocks.rs +176 -0
  94. package/bench/refs/rust-project/tokio/src/fs/mod.rs +324 -0
  95. package/bench/refs/rust-project/tokio/src/fs/open_options.rs +856 -0
  96. package/bench/refs/rust-project/tokio/src/fs/read.rs +80 -0
  97. package/bench/refs/rust-project/tokio/src/fs/read_dir.rs +357 -0
  98. package/bench/refs/rust-project/tokio/src/fs/write.rs +99 -0
  99. package/bench/refs/rust-project/tokio/src/future/maybe_done.rs +123 -0
  100. package/bench/refs/rust-project/tokio/src/future/try_join.rs +82 -0
  101. package/bench/refs/rust-project/tokio/src/io/async_buf_read.rs +117 -0
  102. package/bench/refs/rust-project/tokio/src/io/async_read.rs +133 -0
  103. package/bench/refs/rust-project/tokio/src/io/async_seek.rs +96 -0
  104. package/bench/refs/rust-project/tokio/src/io/async_write.rs +402 -0
  105. package/bench/refs/rust-project/tokio/src/io/blocking.rs +339 -0
  106. package/bench/refs/rust-project/tokio/src/io/interest.rs +345 -0
  107. package/bench/refs/rust-project/tokio/src/io/join.rs +130 -0
  108. package/bench/refs/rust-project/tokio/src/io/mod.rs +308 -0
  109. package/bench/refs/rust-project/tokio/src/io/poll_evented.rs +319 -0
  110. package/bench/refs/rust-project/tokio/src/io/read_buf.rs +344 -0
  111. package/bench/refs/rust-project/tokio/src/io/ready.rs +322 -0
  112. package/bench/refs/rust-project/tokio/src/io/split.rs +166 -0
  113. package/bench/refs/rust-project/tokio/src/io/stderr.rs +148 -0
  114. package/bench/refs/rust-project/tokio/src/io/stdin.rs +98 -0
  115. package/bench/refs/rust-project/tokio/src/io/stdio_common.rs +222 -0
  116. package/bench/refs/rust-project/tokio/src/io/stdout.rs +197 -0
  117. package/bench/refs/rust-project/tokio/src/io/util/buf_reader.rs +311 -0
  118. package/bench/refs/rust-project/tokio/src/io/util/buf_stream.rs +207 -0
  119. package/bench/refs/rust-project/tokio/src/io/util/buf_writer.rs +310 -0
  120. package/bench/refs/rust-project/tokio/src/io/util/chain.rs +144 -0
  121. package/commands/_spawn-patterns.json +32 -0
  122. package/commands/ashlr-allow.md +21 -0
  123. package/commands/ashlr-benchmark.md +53 -0
  124. package/commands/ashlr-brief.md +97 -0
  125. package/commands/ashlr-budget.md +76 -0
  126. package/commands/ashlr-compact.md +54 -0
  127. package/commands/ashlr-dashboard.md +37 -0
  128. package/commands/ashlr-doctor.md +56 -0
  129. package/commands/ashlr-eco-mode.md +92 -0
  130. package/commands/ashlr-genome-init.md +24 -0
  131. package/commands/ashlr-genome-keygen.md +36 -0
  132. package/commands/ashlr-genome-loop.md +118 -0
  133. package/commands/ashlr-genome-push.md +36 -0
  134. package/commands/ashlr-genome-rewrap.md +48 -0
  135. package/commands/ashlr-genome-team-init.md +67 -0
  136. package/commands/ashlr-handoff.md +23 -0
  137. package/commands/ashlr-help.md +86 -0
  138. package/commands/ashlr-hook-timings.md +30 -0
  139. package/commands/ashlr-leaderboard.md +50 -0
  140. package/commands/ashlr-ollama-setup.md +39 -0
  141. package/commands/ashlr-orchestrate-status.md +72 -0
  142. package/commands/ashlr-orchestrate.md +83 -0
  143. package/commands/ashlr-parallelize.md +63 -0
  144. package/commands/ashlr-report-crash.md +33 -0
  145. package/commands/ashlr-resume.md +72 -0
  146. package/commands/ashlr-savings.md +24 -0
  147. package/commands/ashlr-settings.md +50 -0
  148. package/commands/ashlr-spawn.md +61 -0
  149. package/commands/ashlr-start.md +60 -0
  150. package/commands/ashlr-status.md +64 -0
  151. package/commands/ashlr-team-invite.md +47 -0
  152. package/commands/ashlr-tier.md +122 -0
  153. package/commands/ashlr-tour.md +60 -0
  154. package/commands/ashlr-update.md +155 -0
  155. package/commands/ashlr-upgrade.md +96 -0
  156. package/commands/goal.md +58 -0
  157. package/commands/loop.md +40 -0
  158. package/docs/CNAME +1 -0
  159. package/docs/PRO_TIER.md +104 -0
  160. package/docs/README.md +21 -0
  161. package/docs/architecture.md +415 -0
  162. package/docs/ashlr-pipe.md +161 -0
  163. package/docs/assets/logo.svg +19 -0
  164. package/docs/assets/og.png +0 -0
  165. package/docs/assets/og.svg +41 -0
  166. package/docs/benchmarks-v2.json +355 -0
  167. package/docs/benchmarks.json +102 -0
  168. package/docs/benchmarks.md +211 -0
  169. package/docs/billing.md +185 -0
  170. package/docs/cloud-genome.md +155 -0
  171. package/docs/competitive-wozcode.md +92 -0
  172. package/docs/deploy.md +325 -0
  173. package/docs/email-templates.md +113 -0
  174. package/docs/embed-tuning.md +103 -0
  175. package/docs/genome.md +120 -0
  176. package/docs/github-oauth-onboarding.md +127 -0
  177. package/docs/hero-video-script.md +129 -0
  178. package/docs/index.html +2262 -0
  179. package/docs/install-prompt.md +78 -0
  180. package/docs/install-windows.md +132 -0
  181. package/docs/install.ps1 +171 -0
  182. package/docs/install.sh +146 -0
  183. package/docs/launch/README.md +114 -0
  184. package/docs/launch/hn.md +49 -0
  185. package/docs/launch/reddit.md +45 -0
  186. package/docs/launch/x.md +65 -0
  187. package/docs/launch-post.md +112 -0
  188. package/docs/legal.md +78 -0
  189. package/docs/llm-providers.md +99 -0
  190. package/docs/multi-host-mcp.md +217 -0
  191. package/docs/multi-turn-architecture.md +108 -0
  192. package/docs/operations/deployment.md +108 -0
  193. package/docs/operations/error-monitoring.md +70 -0
  194. package/docs/operations/weekly-digest.md +70 -0
  195. package/docs/operations.md +494 -0
  196. package/docs/platform-support.md +133 -0
  197. package/docs/policy-packs.md +188 -0
  198. package/docs/press-kit/fact-sheet.md +78 -0
  199. package/docs/press-kit/founder-bio.md +3 -0
  200. package/docs/press-kit/logo.svg +33 -0
  201. package/docs/press-kit/screenshots.md +146 -0
  202. package/docs/pricing.md +251 -0
  203. package/docs/pro-backend-architecture.md +523 -0
  204. package/docs/producthunt.md +52 -0
  205. package/docs/router-migration.md +110 -0
  206. package/docs/session-log-schema.md +68 -0
  207. package/docs/social.md +95 -0
  208. package/docs/team-genome.md +528 -0
  209. package/docs/telemetry.md +194 -0
  210. package/docs/test-isolation.md +100 -0
  211. package/docs/upgrade.md +159 -0
  212. package/hooks/_hook-errors.ts +87 -0
  213. package/hooks/_hook-perf-emit.ts +113 -0
  214. package/hooks/_hook-timing-ledger.ts +1133 -0
  215. package/hooks/_nudge-throttle.ts +127 -0
  216. package/hooks/_recent-blocks.ts +127 -0
  217. package/hooks/audit-upload.ts +86 -0
  218. package/hooks/codex-hooks.json +171 -0
  219. package/hooks/commit-attribution.ts +179 -0
  220. package/hooks/edit-batching-nudge.ts +154 -0
  221. package/hooks/genome-scribe-hook.ts +219 -0
  222. package/hooks/hooks.json +294 -0
  223. package/hooks/orient-nudge-hook.ts +242 -0
  224. package/hooks/policy-enforce.ts +214 -0
  225. package/hooks/post-tool-use-embedding.ts +93 -0
  226. package/hooks/post-tool-use-genome.sh +37 -0
  227. package/hooks/post-tool-use-genome.ts +69 -0
  228. package/hooks/posttooluse-correlate.ts +122 -0
  229. package/hooks/posttooluse-dedupe-output.ts +241 -0
  230. package/hooks/posttooluse-genome-refresh.ts +223 -0
  231. package/hooks/posttooluse-native-nudge.ts +205 -0
  232. package/hooks/posttooluse-stale-result.ts +277 -0
  233. package/hooks/pretooluse-bash.ts +63 -0
  234. package/hooks/pretooluse-budget-guard.ts +318 -0
  235. package/hooks/pretooluse-common.ts +985 -0
  236. package/hooks/pretooluse-eco-router.ts +196 -0
  237. package/hooks/pretooluse-edit.sh +93 -0
  238. package/hooks/pretooluse-edit.ts +197 -0
  239. package/hooks/pretooluse-glob.ts +70 -0
  240. package/hooks/pretooluse-grep.sh +85 -0
  241. package/hooks/pretooluse-grep.ts +96 -0
  242. package/hooks/pretooluse-notebookedit.ts +121 -0
  243. package/hooks/pretooluse-read.sh +118 -0
  244. package/hooks/pretooluse-read.ts +118 -0
  245. package/hooks/pretooluse-task.ts +130 -0
  246. package/hooks/pretooluse-webfetch.ts +66 -0
  247. package/hooks/pretooluse-websearch.ts +85 -0
  248. package/hooks/pretooluse-write.ts +128 -0
  249. package/hooks/pulse-emit.ts +197 -0
  250. package/hooks/session-end-consolidate.sh +35 -0
  251. package/hooks/session-end-consolidate.ts +188 -0
  252. package/hooks/session-end-stats-gc.ts +80 -0
  253. package/hooks/session-log-append.sh +94 -0
  254. package/hooks/session-log-append.ts +116 -0
  255. package/hooks/session-start.ts +993 -0
  256. package/hooks/sessionend-hook-health-nudge.ts +451 -0
  257. package/hooks/sessionend-savings-nudge.ts +318 -0
  258. package/hooks/sessionend-status-line-nudge.ts +296 -0
  259. package/hooks/sessionstart-brief.ts +140 -0
  260. package/hooks/sessionstart-cost-refactor.ts +95 -0
  261. package/hooks/sessionstart-efficient.ts +110 -0
  262. package/hooks/sessionstart-genome-author.ts +93 -0
  263. package/hooks/sessionstart-lean-tools.ts +98 -0
  264. package/hooks/sessionstart-search.ts +97 -0
  265. package/hooks/stop-accounting.ts +152 -0
  266. package/hooks/subagent-stop-rollup.ts +162 -0
  267. package/hooks/userpromptsubmit-brief-trigger.ts +156 -0
  268. package/package.json +56 -0
  269. package/ports/README.md +108 -0
  270. package/ports/cursor/mcp.json +11 -0
  271. package/ports/goose/recipe.yaml +26 -0
  272. package/scripts/ashlr-mcp.ts +40 -0
  273. package/scripts/auto-update.ts +177 -0
  274. package/scripts/baseline-scan.ts +831 -0
  275. package/scripts/bench-orchestrate.ts +377 -0
  276. package/scripts/benchmark-refs.ts +292 -0
  277. package/scripts/bootstrap.mjs +100 -0
  278. package/scripts/brief-eval.ts +234 -0
  279. package/scripts/bump-version.ts +82 -0
  280. package/scripts/bun-resolve.d.mts +11 -0
  281. package/scripts/bun-resolve.mjs +59 -0
  282. package/scripts/calibrate-grep.ts +467 -0
  283. package/scripts/capture-grep-workload.ts +260 -0
  284. package/scripts/check-tier.ts +61 -0
  285. package/scripts/check-version-sync.ts +97 -0
  286. package/scripts/cli-orchestrate-status.ts +386 -0
  287. package/scripts/cli-orchestrate.ts +303 -0
  288. package/scripts/cli.ts +478 -0
  289. package/scripts/cloud-smoke-test.ts +313 -0
  290. package/scripts/coach-report.ts +373 -0
  291. package/scripts/context-db-migrate.ts +137 -0
  292. package/scripts/context-status.ts +43 -0
  293. package/scripts/demo-run.ts +255 -0
  294. package/scripts/deploy-smoke.sh +140 -0
  295. package/scripts/doctor.ts +992 -0
  296. package/scripts/embed-file-worker.ts +104 -0
  297. package/scripts/embed-tune.ts +308 -0
  298. package/scripts/errors-report.ts +428 -0
  299. package/scripts/find-test-leak.ts +69 -0
  300. package/scripts/generate-badge.ts +357 -0
  301. package/scripts/genome-auto-consolidate.ts +533 -0
  302. package/scripts/genome-auto-propose.ts +578 -0
  303. package/scripts/genome-cloud-pull.ts +363 -0
  304. package/scripts/genome-cloud-push.ts +382 -0
  305. package/scripts/genome-cloud-sync.ts +587 -0
  306. package/scripts/genome-commit-watcher.ts +295 -0
  307. package/scripts/genome-init.ts +986 -0
  308. package/scripts/genome-key.ts +233 -0
  309. package/scripts/genome-keygen.ts +168 -0
  310. package/scripts/genome-link.ts +59 -0
  311. package/scripts/genome-refresh-worker.ts +364 -0
  312. package/scripts/genome-rewrap.ts +90 -0
  313. package/scripts/genome-synthesizer.ts +679 -0
  314. package/scripts/genome-team-init.ts +312 -0
  315. package/scripts/handoff-pack.ts +438 -0
  316. package/scripts/hook-bootstrap.mjs +75 -0
  317. package/scripts/hook-timing-reader.ts +371 -0
  318. package/scripts/hook-timings-report.ts +554 -0
  319. package/scripts/install-genome-hooks.ts +196 -0
  320. package/scripts/install-onnx-model.ts +154 -0
  321. package/scripts/install-permissions.ts +276 -0
  322. package/scripts/install-status-line.ts +150 -0
  323. package/scripts/mcp-entrypoint.sh +78 -0
  324. package/scripts/mcp-entrypoint.ts +130 -0
  325. package/scripts/mdx-angle-bracket-lint.ts +152 -0
  326. package/scripts/measure-cold-start.ts +126 -0
  327. package/scripts/migrate-stats-to-sqlite.ts +86 -0
  328. package/scripts/ollama-setup.ts +498 -0
  329. package/scripts/onboarding-wizard.ts +1691 -0
  330. package/scripts/orchestrate-emit-telemetry.ts +177 -0
  331. package/scripts/orchestrate-expand.ts +517 -0
  332. package/scripts/orchestrate-run.ts +744 -0
  333. package/scripts/print-legend.ts +50 -0
  334. package/scripts/product-metadata.ts +38 -0
  335. package/scripts/prune-old-memories.ts +208 -0
  336. package/scripts/publish.sh +88 -0
  337. package/scripts/read-calibration.ts +122 -0
  338. package/scripts/report-crash.ts +226 -0
  339. package/scripts/run-benchmark.ts +750 -0
  340. package/scripts/run-root-tests.ts +28 -0
  341. package/scripts/savings-dashboard.ts +1258 -0
  342. package/scripts/savings-report-extras.ts +447 -0
  343. package/scripts/savings-status-line.ts +873 -0
  344. package/scripts/session-event-emit.ts +266 -0
  345. package/scripts/session-greet.ts +686 -0
  346. package/scripts/session-log-report.ts +551 -0
  347. package/scripts/session-resume.ts +614 -0
  348. package/scripts/set-hook-mode.ts +83 -0
  349. package/scripts/smoke-cross-platform.ts +117 -0
  350. package/scripts/smoke-realtime.ts +257 -0
  351. package/scripts/smoke-tools.ts +21 -0
  352. package/scripts/stats-cloud-pull.ts +213 -0
  353. package/scripts/stats-cloud-sync.ts +261 -0
  354. package/scripts/telemetry-flush.ts +160 -0
  355. package/scripts/telemetry-status.ts +266 -0
  356. package/scripts/timing-budget.ts +35 -0
  357. package/scripts/ui-animation.ts +120 -0
  358. package/scripts/ui.ts +431 -0
  359. package/scripts/upgrade-flow.ts +775 -0
  360. package/scripts/weekly-digest.ts +394 -0
  361. package/servers/_accounting.ts +78 -0
  362. package/servers/_ask-router.ts +84 -0
  363. package/servers/_ast-chunker.ts +434 -0
  364. package/servers/_ast-helpers.ts +183 -0
  365. package/servers/_ast-languages.ts +153 -0
  366. package/servers/_ast-refactor/_shared.ts +221 -0
  367. package/servers/_ast-refactor/cross-file-rename.ts +560 -0
  368. package/servers/_ast-refactor/extract-function.ts +357 -0
  369. package/servers/_ast-refactor/file-local-rename.ts +172 -0
  370. package/servers/_ast-refactor.ts +41 -0
  371. package/servers/_ast-skeleton.ts +107 -0
  372. package/servers/_bash-summarizers-registry.ts +1096 -0
  373. package/servers/_crash-dump.ts +210 -0
  374. package/servers/_cwd-clamp.ts +342 -0
  375. package/servers/_edit-log.ts +28 -0
  376. package/servers/_edit-match.ts +236 -0
  377. package/servers/_edit-validate.ts +196 -0
  378. package/servers/_embed-calibration.ts +123 -0
  379. package/servers/_embedding-cache.ts +401 -0
  380. package/servers/_embedding-model.ts +329 -0
  381. package/servers/_events.ts +113 -0
  382. package/servers/_genome-cache.ts +154 -0
  383. package/servers/_genome-commits.ts +172 -0
  384. package/servers/_genome-crypto-v2.ts +242 -0
  385. package/servers/_genome-crypto.ts +259 -0
  386. package/servers/_genome-discoveries.ts +140 -0
  387. package/servers/_genome-embed-populator.ts +216 -0
  388. package/servers/_genome-freshness.ts +134 -0
  389. package/servers/_genome-issues.ts +171 -0
  390. package/servers/_genome-live.ts +263 -0
  391. package/servers/_genome-prs.ts +188 -0
  392. package/servers/_genome-search.ts +479 -0
  393. package/servers/_genome-sync.ts +297 -0
  394. package/servers/_history-tracker.ts +279 -0
  395. package/servers/_http-helpers.ts +151 -0
  396. package/servers/_identity-hash.ts +151 -0
  397. package/servers/_llm-providers/anthropic.ts +132 -0
  398. package/servers/_llm-providers/cloud.ts +195 -0
  399. package/servers/_llm-providers/index.ts +350 -0
  400. package/servers/_llm-providers/local.ts +127 -0
  401. package/servers/_llm-providers/onnx.ts +393 -0
  402. package/servers/_llm-providers/types.ts +27 -0
  403. package/servers/_manifest-v2.ts +447 -0
  404. package/servers/_mcp-host.ts +93 -0
  405. package/servers/_nudge-events.ts +451 -0
  406. package/servers/_orchestrate-executor.ts +316 -0
  407. package/servers/_prefetch.ts +435 -0
  408. package/servers/_pricing.ts +256 -0
  409. package/servers/_pro.ts +381 -0
  410. package/servers/_read-cache.ts +44 -0
  411. package/servers/_router-handlers.ts +46 -0
  412. package/servers/_router.ts +55 -0
  413. package/servers/_run-with-timeout.ts +173 -0
  414. package/servers/_savings-render.ts +214 -0
  415. package/servers/_stats-sqlite.ts +640 -0
  416. package/servers/_stats.ts +1183 -0
  417. package/servers/_status-line-cells.ts +469 -0
  418. package/servers/_streaks.ts +162 -0
  419. package/servers/_summarize.ts +208 -0
  420. package/servers/_task-graph-render.ts +217 -0
  421. package/servers/_task-graph.ts +545 -0
  422. package/servers/_telemetry.ts +719 -0
  423. package/servers/_test-parsers.ts +530 -0
  424. package/servers/_test-watch.ts +626 -0
  425. package/servers/_text-helpers.ts +37 -0
  426. package/servers/_token-measure.ts +160 -0
  427. package/servers/_tool-base.ts +248 -0
  428. package/servers/_weekly-digest.ts +391 -0
  429. package/servers/ask-server-handlers.ts +49 -0
  430. package/servers/ask-server.ts +174 -0
  431. package/servers/bash-server-handlers.ts +138 -0
  432. package/servers/bash-server.ts +1136 -0
  433. package/servers/bench.ts +186 -0
  434. package/servers/diff-semantic-server-handlers.ts +39 -0
  435. package/servers/diff-semantic-server.ts +566 -0
  436. package/servers/diff-server-handlers.ts +317 -0
  437. package/servers/diff-server.ts +20 -0
  438. package/servers/edit-server-handlers.ts +31 -0
  439. package/servers/edit-server.ts +209 -0
  440. package/servers/edit-structural-server-handlers.ts +322 -0
  441. package/servers/efficiency-server-handlers.ts +161 -0
  442. package/servers/efficiency-server.ts +1135 -0
  443. package/servers/flush-server-handlers.ts +21 -0
  444. package/servers/flush-server.ts +19 -0
  445. package/servers/genome-server-handlers.ts +118 -0
  446. package/servers/genome-server.ts +397 -0
  447. package/servers/github-server-handlers.ts +187 -0
  448. package/servers/github-server.ts +856 -0
  449. package/servers/glob-server-handlers.ts +233 -0
  450. package/servers/glob-server.ts +20 -0
  451. package/servers/grep-server-handlers.ts +61 -0
  452. package/servers/grep-server.ts +564 -0
  453. package/servers/http-server-handlers.ts +36 -0
  454. package/servers/http-server.ts +149 -0
  455. package/servers/logs-server-handlers.ts +42 -0
  456. package/servers/logs-server.ts +359 -0
  457. package/servers/ls-server-handlers.ts +166 -0
  458. package/servers/ls-server.ts +20 -0
  459. package/servers/multi-edit-server-handlers.ts +51 -0
  460. package/servers/multi-edit-server.ts +422 -0
  461. package/servers/notebook-edit-server-handlers.ts +34 -0
  462. package/servers/notebook-edit-server.ts +181 -0
  463. package/servers/orient-server-handlers.ts +52 -0
  464. package/servers/orient-server.ts +436 -0
  465. package/servers/pipe-server-handlers.ts +68 -0
  466. package/servers/pipe-server.ts +300 -0
  467. package/servers/read-server-handlers.ts +42 -0
  468. package/servers/read-server.ts +206 -0
  469. package/servers/rename-file-server-handlers.ts +67 -0
  470. package/servers/rename-file-server.ts +699 -0
  471. package/servers/savings-server-handlers.ts +27 -0
  472. package/servers/savings-server.ts +54 -0
  473. package/servers/search-replace-regex-server-handlers.ts +96 -0
  474. package/servers/search-replace-regex-server.ts +751 -0
  475. package/servers/sql-server-handlers.ts +58 -0
  476. package/servers/sql-server.ts +505 -0
  477. package/servers/task-server-handlers.ts +358 -0
  478. package/servers/task-server.ts +24 -0
  479. package/servers/test-server-handlers.ts +351 -0
  480. package/servers/test-server.ts +20 -0
  481. package/servers/tree-server-handlers.ts +503 -0
  482. package/servers/tree-server.ts +20 -0
  483. package/servers/webfetch-server-handlers.ts +166 -0
  484. package/servers/webfetch-server.ts +20 -0
  485. package/servers/websearch-server-handlers.ts +215 -0
  486. package/servers/websearch-server.ts +19 -0
  487. package/servers/write-server-handlers.ts +27 -0
  488. package/servers/write-server.ts +97 -0
  489. package/skills/ashlr-brief/SKILL.md +80 -0
  490. package/skills/ashlr-codex-doctor/SKILL.md +21 -0
  491. package/skills/ashlr-codex-efficient-tools/SKILL.md +17 -0
  492. package/skills/ashlr-codex-genome-refresh/SKILL.md +20 -0
  493. package/skills/ashlr-codex-parallel-explore/SKILL.md +20 -0
  494. package/skills/ashlr-codex-savings/SKILL.md +17 -0
  495. package/skills/ashlr-cost-refactor/SKILL.md +50 -0
  496. package/skills/ashlr-efficient/SKILL.md +64 -0
  497. package/skills/ashlr-genome-author/SKILL.md +43 -0
  498. package/skills/ashlr-lean-tools/SKILL.md +43 -0
  499. package/skills/ashlr-search/SKILL.md +46 -0
@@ -0,0 +1,469 @@
1
+ /**
2
+ * Pure, deterministic status-line renderer — structured output.
3
+ *
4
+ * This module is the single source of truth for every visual decision the
5
+ * animated status line makes. It returns `Cell[]` (a char + optional color +
6
+ * optional bold) instead of ANSI-escaped strings, so two consumers can share
7
+ * exactly the same animation logic:
8
+ *
9
+ * 1. `scripts/ui-animation.ts` — the CLI wrapper. Calls the cell producers
10
+ * here, then serializes Cell[] into ANSI escapes for the terminal.
11
+ * 2. `video/src/compositions/StatusLineStill.tsx` — the Remotion port.
12
+ * Calls the same cell producers, then renders each cell as a `<span>`
13
+ * with inline RGB styles for the hero video.
14
+ *
15
+ * Zero Node deps, zero I/O, zero `Date` calls. Every function is
16
+ * `(values, frame, msSinceActive, capability) -> something deterministic`.
17
+ * Test golden-frames without mocking the clock.
18
+ */
19
+
20
+ // ---------------------------------------------------------------------------
21
+ // Types
22
+ // ---------------------------------------------------------------------------
23
+
24
+ export interface RGB {
25
+ r: number;
26
+ g: number;
27
+ b: number;
28
+ }
29
+
30
+ export interface Cell {
31
+ /** Visible character (single grapheme). */
32
+ char: string;
33
+ /** Foreground RGB. Absent means "use terminal default / inherited color". */
34
+ fg?: RGB;
35
+ /** Bold styling. */
36
+ bold?: boolean;
37
+ /**
38
+ * True when this cell is part of an active pulse/activity effect. Downstream
39
+ * renderers (especially the Remotion port) can use this to emit extra
40
+ * animation hints like a glow filter.
41
+ */
42
+ activity?: boolean;
43
+ }
44
+
45
+ export interface Capability {
46
+ /** Terminal supports 24-bit color escapes. */
47
+ truecolor: boolean;
48
+ /** Terminal supports Unicode. False falls through to ASCII ramps. */
49
+ unicode: boolean;
50
+ /** Master animation switch. False emits a single static frame. */
51
+ animate: boolean;
52
+ }
53
+
54
+ // ---------------------------------------------------------------------------
55
+ // Capability detection
56
+ // ---------------------------------------------------------------------------
57
+
58
+ export function detectCapability(env: NodeJS.ProcessEnv = process.env): Capability {
59
+ const noColor = truthyEnv(env.NO_COLOR);
60
+ const animateOff = env.ASHLR_STATUS_ANIMATE === "0";
61
+ const forceAnimate = env.ASHLR_STATUS_ANIMATE === "1";
62
+ const colorterm = (env.COLORTERM ?? "").toLowerCase();
63
+ const term = (env.TERM ?? "").toLowerCase();
64
+ const truecolor = !noColor && (colorterm === "truecolor" || colorterm === "24bit" || forceAnimate);
65
+ const lang = (env.LANG ?? env.LC_ALL ?? env.LC_CTYPE ?? "").toLowerCase();
66
+ const unicode = lang.includes("utf") || term.includes("xterm") || term.includes("256color") || truecolor;
67
+ const animate = !animateOff && (truecolor || forceAnimate);
68
+ return { truecolor, unicode, animate };
69
+ }
70
+
71
+ function truthyEnv(v: string | undefined): boolean {
72
+ if (v == null) return false;
73
+ const t = v.trim().toLowerCase();
74
+ return t !== "" && t !== "0" && t !== "false" && t !== "no";
75
+ }
76
+
77
+ // ---------------------------------------------------------------------------
78
+ // Frame clock
79
+ // ---------------------------------------------------------------------------
80
+
81
+ export const FRAME_MS = 120;
82
+
83
+ /** Integer frame index for a given wall-clock ms. */
84
+ export function frameAt(nowMs: number, frameMs: number = FRAME_MS): number {
85
+ return Math.floor(nowMs / frameMs);
86
+ }
87
+
88
+ // ---------------------------------------------------------------------------
89
+ // Sparkline glyph ramps
90
+ // ---------------------------------------------------------------------------
91
+
92
+ export const UNICODE_RAMP: readonly string[] = [
93
+ "\u2800",
94
+ "\u2840",
95
+ "\u2844",
96
+ "\u2846",
97
+ "\u2847",
98
+ "\u28E7",
99
+ "\u28F7",
100
+ "\u28FF",
101
+ "\u2581",
102
+ "\u2582",
103
+ "\u2583",
104
+ "\u2584",
105
+ "\u2585",
106
+ "\u2586",
107
+ "\u2587",
108
+ "\u2588",
109
+ ];
110
+
111
+ export const ASCII_RAMP: readonly string[] = [" ", ".", ":", "|", "#"];
112
+
113
+ function pickRamp(cap: Capability): readonly string[] {
114
+ return cap.unicode ? UNICODE_RAMP : ASCII_RAMP;
115
+ }
116
+
117
+ export function valuesToRamp(values: readonly number[], rampLen: number): number[] {
118
+ const max = Math.max(...values, 0);
119
+ if (max <= 0) return values.map(() => 0);
120
+ return values.map((v) => {
121
+ if (v <= 0) return 0;
122
+ const last = rampLen - 1;
123
+ return Math.max(1, Math.min(last, Math.ceil((v / max) * last)));
124
+ });
125
+ }
126
+
127
+ // ---------------------------------------------------------------------------
128
+ // Color helpers
129
+ // ---------------------------------------------------------------------------
130
+
131
+ export const BRAND_DARK: RGB = { r: 0, g: 208, b: 156 }; // #00d09c
132
+ export const BRAND_LIGHT: RGB = { r: 124, g: 255, b: 214 }; // #7cffd6
133
+ export const PULSE_CELL: RGB = { r: 255, g: 255, b: 255 };
134
+
135
+ function lerp(a: number, b: number, t: number): number {
136
+ return Math.round(a + (b - a) * Math.max(0, Math.min(1, t)));
137
+ }
138
+
139
+ export function lerpColor(a: RGB, b: RGB, t: number): RGB {
140
+ return { r: lerp(a.r, b.r, t), g: lerp(a.g, b.g, t), b: lerp(a.b, b.b, t) };
141
+ }
142
+
143
+ // ---------------------------------------------------------------------------
144
+ // Gradient sweep + activity pulse math
145
+ // ---------------------------------------------------------------------------
146
+
147
+ export function gradientTs(width: number, frame: number): number[] {
148
+ if (width <= 0) return [];
149
+ if (width === 1) return [0];
150
+ const ts: number[] = [];
151
+ const phase = ((frame % (width * 2)) + width * 2) % (width * 2);
152
+ for (let i = 0; i < width; i++) {
153
+ const shifted = (i + phase) % (width * 2);
154
+ const raw = shifted < width ? shifted / (width - 1) : (width * 2 - 1 - shifted) / (width - 1);
155
+ ts.push(raw);
156
+ }
157
+ return ts;
158
+ }
159
+
160
+ export interface PulseState {
161
+ /** Linear position 0..1 along the sparkline (wraps). */
162
+ position: number;
163
+ /** Intensity 0..1 used to blend the pulse color over the base cell. */
164
+ intensity: number;
165
+ }
166
+
167
+ export function computePulse(frame: number, msSinceActive: number, width: number): PulseState {
168
+ if (!Number.isFinite(msSinceActive) || msSinceActive < 0 || width <= 0) {
169
+ return { position: 0, intensity: 0 };
170
+ }
171
+ const ACTIVE_MS = 4_000;
172
+ const FADE_MS = 500;
173
+ let intensity: number;
174
+ if (msSinceActive <= ACTIVE_MS) intensity = 1;
175
+ else if (msSinceActive <= ACTIVE_MS + FADE_MS) {
176
+ intensity = 1 - (msSinceActive - ACTIVE_MS) / FADE_MS;
177
+ } else intensity = 0;
178
+ const position = ((frame % (width * 3)) + width * 3) % (width * 3) / (width * 3);
179
+ return { position, intensity };
180
+ }
181
+
182
+ export function sweepFactor(cellIndex: number, headCell: number, width: number): number {
183
+ const w = Math.max(1, width);
184
+ const delta = ((cellIndex - headCell) % w + w) % w;
185
+ if (delta === 0) return 1.0;
186
+ if (delta === w - 1) return 0.45;
187
+ if (delta === w - 2) return 0.15;
188
+ return 0.0;
189
+ }
190
+
191
+ // ---------------------------------------------------------------------------
192
+ // Heartbeat glyph
193
+ // ---------------------------------------------------------------------------
194
+
195
+ export const HEARTBEAT_FRAMES = [
196
+ "\u2840", "\u2844", "\u2846", "\u2847", "\u28C7",
197
+ "\u28E7", "\u28F7", "\u28FF", "\u28F7", "\u28E7",
198
+ "\u28C7", "\u2847", "\u2846", "\u2844", "\u2840",
199
+ ] as const;
200
+ export const HEARTBEAT_IDLE = "\u00B7";
201
+ export const HEARTBEAT_ASCII_IDLE = ".";
202
+ export const HEARTBEAT_ASCII_ACTIVE = ["-", "=", "*", "=", "-"] as const;
203
+
204
+ const HEARTBEAT_IDLE_COLOR: RGB = { r: 100, g: 110, b: 120 };
205
+
206
+ // ---------------------------------------------------------------------------
207
+ // Context pressure widget
208
+ // ---------------------------------------------------------------------------
209
+
210
+ export const CTX_GREEN: RGB = { r: 0, g: 160, b: 120 };
211
+ export const CTX_YELLOW: RGB = { r: 212, g: 167, b: 44 };
212
+ export const CTX_ORANGE: RGB = { r: 217, g: 121, b: 58 };
213
+ export const CTX_RED: RGB = { r: 225, g: 91, b: 91 };
214
+
215
+ // ---------------------------------------------------------------------------
216
+ // Activity indicator
217
+ // ---------------------------------------------------------------------------
218
+
219
+ export const ACTIVITY_ACTIVE_MS = 4_000;
220
+ export const ACTIVITY_GLYPH_UNICODE = "\u2191";
221
+ export const ACTIVITY_GLYPH_ASCII = "+";
222
+
223
+ // ===========================================================================
224
+ // Cell producers — the public API the CLI and Remotion renderers both consume.
225
+ // ===========================================================================
226
+
227
+ export interface SparklineInput {
228
+ values: readonly number[];
229
+ frame: number;
230
+ msSinceActive: number;
231
+ cap: Capability;
232
+ }
233
+
234
+ /**
235
+ * Build the sparkline as structured cells.
236
+ *
237
+ * When `cap.animate` or `cap.truecolor` is false, each cell has no `fg` (plain
238
+ * terminal color) or a single brand-green `fg` depending on caller intent. In
239
+ * the full animated case, each cell gets a gradient-interpolated base color
240
+ * plus optional pulse overlay.
241
+ */
242
+ export function sparklineCells({ values, frame, msSinceActive, cap }: SparklineInput): Cell[] {
243
+ const ramp = pickRamp(cap);
244
+ const idxs = valuesToRamp(values, ramp.length);
245
+ const chars = idxs.map((i) => ramp[i]!);
246
+
247
+ if (!cap.animate || !cap.truecolor) {
248
+ return chars.map((char) => ({ char }));
249
+ }
250
+
251
+ const ts = gradientTs(chars.length, frame);
252
+ const pulse = computePulse(frame, msSinceActive, chars.length);
253
+ const headCell = pulse.intensity > 0
254
+ ? Math.floor(pulse.position * chars.length) % chars.length
255
+ : -1;
256
+
257
+ return chars.map((char, i) => {
258
+ const base = lerpColor(BRAND_DARK, BRAND_LIGHT, ts[i] ?? 0);
259
+ let color = base;
260
+ let activity = false;
261
+ if (headCell >= 0) {
262
+ const factor = sweepFactor(i, headCell, chars.length) * pulse.intensity;
263
+ if (factor > 0) {
264
+ color = lerpColor(base, PULSE_CELL, factor);
265
+ activity = true;
266
+ }
267
+ }
268
+ const cell: Cell = { char, fg: color };
269
+ if (activity) cell.activity = true;
270
+ return cell;
271
+ });
272
+ }
273
+
274
+ /**
275
+ * Single-cell heartbeat glyph (goes between "ashlr" and the sparkline).
276
+ */
277
+ export function heartbeatCell(frame: number, msSinceActive: number, cap: Capability): Cell {
278
+ const active = msSinceActive <= 4_500;
279
+ if (!cap.animate) {
280
+ return { char: cap.unicode ? HEARTBEAT_IDLE : HEARTBEAT_ASCII_IDLE };
281
+ }
282
+ if (!active) {
283
+ const idle = cap.unicode ? HEARTBEAT_IDLE : HEARTBEAT_ASCII_IDLE;
284
+ return cap.truecolor
285
+ ? { char: idle, fg: HEARTBEAT_IDLE_COLOR }
286
+ : { char: idle };
287
+ }
288
+ const frames = cap.unicode ? HEARTBEAT_FRAMES : HEARTBEAT_ASCII_ACTIVE;
289
+ const ch = frames[((frame % frames.length) + frames.length) % frames.length]!;
290
+ if (!cap.truecolor) return { char: ch, activity: true };
291
+
292
+ const ACTIVE_MS = 4_000;
293
+ const FADE_MS = 500;
294
+ const t = msSinceActive <= ACTIVE_MS
295
+ ? 0
296
+ : Math.min(1, (msSinceActive - ACTIVE_MS) / FADE_MS);
297
+ const color = lerpColor(BRAND_LIGHT, BRAND_DARK, t);
298
+ return { char: ch, fg: color, activity: true };
299
+ }
300
+
301
+ /**
302
+ * Activity indicator next to the session counter. Returns `null` when idle so
303
+ * the caller can omit the cell entirely (width stays stable — idle = 0 cells,
304
+ * active = 1 cell).
305
+ */
306
+ export function activityCell(msSinceActive: number, cap: Capability): Cell | null {
307
+ if (!Number.isFinite(msSinceActive) || msSinceActive > ACTIVITY_ACTIVE_MS) return null;
308
+ const glyph = cap.unicode ? ACTIVITY_GLYPH_UNICODE : ACTIVITY_GLYPH_ASCII;
309
+ if (!cap.truecolor) return { char: glyph, activity: true };
310
+ const t = msSinceActive / ACTIVITY_ACTIVE_MS;
311
+ const color = lerpColor(BRAND_LIGHT, BRAND_DARK, t);
312
+ return { char: glyph, fg: color, activity: true };
313
+ }
314
+
315
+ /**
316
+ * Context-pressure micro-widget, rendered as a sequence of cells spelling out
317
+ * e.g. `ctx: 72%`. Width is 8–9 visible cells depending on the percentage.
318
+ *
319
+ * When `cap.truecolor` is false every cell is returned without `fg` so the
320
+ * caller renders in the terminal's default color.
321
+ */
322
+ export function contextPressureCells(pct: number, cap: Capability): Cell[] {
323
+ const label = `ctx: ${Math.round(pct)}%`;
324
+ if (!cap.truecolor) return [...label].map((char) => ({ char }));
325
+
326
+ let color: RGB;
327
+ let bold = false;
328
+ if (pct >= 95) {
329
+ color = CTX_RED;
330
+ bold = true;
331
+ } else if (pct >= 80) {
332
+ color = CTX_ORANGE;
333
+ } else if (pct >= 60) {
334
+ color = CTX_YELLOW;
335
+ } else {
336
+ color = CTX_GREEN;
337
+ }
338
+
339
+ return [...label].map((char) => {
340
+ const cell: Cell = { char, fg: color };
341
+ if (bold) cell.bold = true;
342
+ return cell;
343
+ });
344
+ }
345
+
346
+ // ---------------------------------------------------------------------------
347
+ // Budget segment — shows "$X / $Y · Z%" or "Nt / Nmax · Z%"
348
+ // ---------------------------------------------------------------------------
349
+
350
+ export const BUDGET_OK: RGB = { r: 0, g: 160, b: 120 }; // green
351
+ export const BUDGET_WARN: RGB = { r: 212, g: 167, b: 44 }; // yellow
352
+ export const BUDGET_CRIT: RGB = { r: 225, g: 91, b: 91 }; // red
353
+
354
+ /**
355
+ * Return cells for the budget segment, or an empty array when no budget is set.
356
+ *
357
+ * When `ASHLR_SESSION_BUDGET_USD` is set renders: `$used / $cap · Z%`
358
+ * When `ASHLR_SESSION_BUDGET_TOKENS` is set renders: `Nt / Nmax · Z%`
359
+ */
360
+ export function budgetCells(
361
+ usedUsd: number,
362
+ budgetUsd: number,
363
+ usedTokens: number,
364
+ budgetTokens: number,
365
+ cap: Capability,
366
+ ): Cell[] {
367
+ if (budgetUsd <= 0 && budgetTokens <= 0) return [];
368
+
369
+ let label: string;
370
+ let pct: number;
371
+
372
+ if (budgetUsd > 0) {
373
+ pct = budgetUsd > 0 ? usedUsd / budgetUsd : 0;
374
+ label = `$${usedUsd.toFixed(2)} / $${budgetUsd.toFixed(2)} · ${Math.round(pct * 100)}%`;
375
+ } else {
376
+ pct = budgetTokens > 0 ? usedTokens / budgetTokens : 0;
377
+ const usedK = usedTokens >= 1000 ? `${Math.round(usedTokens / 1000)}k` : String(usedTokens);
378
+ const maxK = budgetTokens >= 1000 ? `${Math.round(budgetTokens / 1000)}k` : String(budgetTokens);
379
+ label = `${usedK} / ${maxK} · ${Math.round(pct * 100)}%`;
380
+ }
381
+
382
+ if (!cap.truecolor) return [...label].map((char) => ({ char }));
383
+
384
+ let color: RGB;
385
+ let bold = false;
386
+ if (pct >= 1.0) {
387
+ color = BUDGET_CRIT;
388
+ bold = true;
389
+ } else if (pct >= 0.95) {
390
+ color = BUDGET_CRIT;
391
+ } else if (pct >= 0.80) {
392
+ color = BUDGET_WARN;
393
+ } else {
394
+ color = BUDGET_OK;
395
+ }
396
+
397
+ return [...label].map((char) => {
398
+ const cell: Cell = { char, fg: color };
399
+ if (bold) cell.bold = true;
400
+ return cell;
401
+ });
402
+ }
403
+
404
+ // ---------------------------------------------------------------------------
405
+ // Eco badge — shows "eco" when ASHLR_ECO=1
406
+ // ---------------------------------------------------------------------------
407
+
408
+ export const ECO_COLOR: RGB = { r: 80, g: 200, b: 100 }; // leaf green
409
+
410
+ /**
411
+ * Return cells for the eco badge, or empty array when eco mode is off.
412
+ * Reads ASHLR_ECO from env unless overridden.
413
+ */
414
+ export function ecoBadgeCells(
415
+ cap: Capability,
416
+ env: NodeJS.ProcessEnv = process.env,
417
+ ): Cell[] {
418
+ if (!env.ASHLR_ECO || env.ASHLR_ECO === "0") return [];
419
+ const label = "eco";
420
+ if (!cap.truecolor) return [...label].map((char) => ({ char }));
421
+ return [...label].map((char) => ({ char, fg: ECO_COLOR }));
422
+ }
423
+
424
+ // ===========================================================================
425
+ // Cell → ANSI serialization (used by the CLI wrapper)
426
+ // ===========================================================================
427
+
428
+ const ANSI_RESET = "\x1b[0m";
429
+
430
+ function ansiFg(c: RGB): string {
431
+ return `\x1b[38;2;${c.r};${c.g};${c.b}m`;
432
+ }
433
+
434
+ /**
435
+ * Collapse a `Cell[]` into an ANSI-escaped string suitable for TTY output.
436
+ * Runs of cells that share `(fg, bold)` share a single escape prefix so the
437
+ * output stays compact and readable in a `cat`'d log.
438
+ *
439
+ * Callers who want no ANSI (raw character dump for width measurement) can just
440
+ * join `cells.map(c => c.char)` directly.
441
+ */
442
+ export function cellsToAnsi(cells: readonly Cell[]): string {
443
+ if (cells.length === 0) return "";
444
+ const parts: string[] = [];
445
+ let last: { fg?: RGB; bold?: boolean } = {};
446
+ let hadStyle = false;
447
+
448
+ for (const cell of cells) {
449
+ const sameFg =
450
+ (cell.fg == null && last.fg == null) ||
451
+ (cell.fg != null && last.fg != null &&
452
+ cell.fg.r === last.fg.r && cell.fg.g === last.fg.g && cell.fg.b === last.fg.b);
453
+ const sameBold = !!cell.bold === !!last.bold;
454
+ if (!(sameFg && sameBold)) {
455
+ if (hadStyle) parts.push(ANSI_RESET);
456
+ if (cell.bold) parts.push("\x1b[1m");
457
+ if (cell.fg) {
458
+ parts.push(ansiFg(cell.fg));
459
+ hadStyle = true;
460
+ } else {
461
+ hadStyle = !!cell.bold;
462
+ }
463
+ last = { fg: cell.fg, bold: cell.bold };
464
+ }
465
+ parts.push(cell.char);
466
+ }
467
+ if (hadStyle) parts.push(ANSI_RESET);
468
+ return parts.join("");
469
+ }
@@ -0,0 +1,162 @@
1
+ /**
2
+ * _streaks.ts — Saving-streak tracking for the ashlr-plugin.
3
+ *
4
+ * Persists to ~/.ashlr/streaks.json:
5
+ * { currentStreak: N, longestStreak: M, lastActiveDay: "YYYY-MM-DD" }
6
+ *
7
+ * Called by recordSaving (gated to once-per-day to avoid hot-path overhead).
8
+ * Read by the status-line, dashboard, and savings-report surfaces.
9
+ *
10
+ * Grace rule: a 1-day gap does NOT break the streak (life happens).
11
+ * A 2+ day gap resets currentStreak to 1.
12
+ */
13
+
14
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "fs";
15
+ import { homedir } from "os";
16
+ import { join } from "path";
17
+
18
+ // ---------------------------------------------------------------------------
19
+ // Types
20
+ // ---------------------------------------------------------------------------
21
+
22
+ export interface StreakData {
23
+ currentStreak: number;
24
+ longestStreak: number;
25
+ lastActiveDay: string; // YYYY-MM-DD UTC
26
+ }
27
+
28
+ // ---------------------------------------------------------------------------
29
+ // Paths
30
+ // ---------------------------------------------------------------------------
31
+
32
+ export function streakPath(home: string = homedir()): string {
33
+ return join(home, ".ashlr", "streaks.json");
34
+ }
35
+
36
+ // ---------------------------------------------------------------------------
37
+ // IO helpers
38
+ // ---------------------------------------------------------------------------
39
+
40
+ export function readStreaks(home: string = homedir()): StreakData {
41
+ try {
42
+ const p = streakPath(home);
43
+ if (!existsSync(p)) return { currentStreak: 0, longestStreak: 0, lastActiveDay: "" };
44
+ const raw = JSON.parse(readFileSync(p, "utf-8")) as Partial<StreakData>;
45
+ return {
46
+ currentStreak: typeof raw.currentStreak === "number" ? raw.currentStreak : 0,
47
+ longestStreak: typeof raw.longestStreak === "number" ? raw.longestStreak : 0,
48
+ lastActiveDay: typeof raw.lastActiveDay === "string" ? raw.lastActiveDay : "",
49
+ };
50
+ } catch {
51
+ return { currentStreak: 0, longestStreak: 0, lastActiveDay: "" };
52
+ }
53
+ }
54
+
55
+ export function writeStreaks(data: StreakData, home: string = homedir()): void {
56
+ try {
57
+ mkdirSync(join(home, ".ashlr"), { recursive: true });
58
+ writeFileSync(streakPath(home), JSON.stringify(data, null, 2));
59
+ } catch {
60
+ /* best-effort */
61
+ }
62
+ }
63
+
64
+ // ---------------------------------------------------------------------------
65
+ // Core logic
66
+ // ---------------------------------------------------------------------------
67
+
68
+ /** UTC YYYY-MM-DD for the given timestamp (or now). */
69
+ export function todayKey(nowMs: number = Date.now()): string {
70
+ return new Date(nowMs).toISOString().slice(0, 10);
71
+ }
72
+
73
+ /** Number of calendar days between two YYYY-MM-DD strings. */
74
+ export function daysBetween(a: string, b: string): number {
75
+ const msA = Date.parse(a);
76
+ const msB = Date.parse(b);
77
+ if (!Number.isFinite(msA) || !Number.isFinite(msB)) return Infinity;
78
+ return Math.round(Math.abs(msA - msB) / 86_400_000);
79
+ }
80
+
81
+ /**
82
+ * Compute updated streak given existing data and today's key.
83
+ *
84
+ * Rules:
85
+ * - same day as lastActiveDay → no change (idempotent)
86
+ * - gap of 1 day (yesterday was the last active day) → streak extends
87
+ * - gap of 2+ days BUT ≤ grace (1 missed day) → still extends
88
+ * - gap > 1-day grace → reset to 1
89
+ *
90
+ * Grace rule: a SINGLE missed day (gap = 2: last active 2 days ago) is
91
+ * forgiven. Gap of 3+ days breaks the streak.
92
+ */
93
+ export function computeNewStreak(data: StreakData, today: string): StreakData {
94
+ if (!today) return data;
95
+
96
+ // No prior active day → start streak.
97
+ if (!data.lastActiveDay) {
98
+ return {
99
+ currentStreak: 1,
100
+ longestStreak: Math.max(1, data.longestStreak),
101
+ lastActiveDay: today,
102
+ };
103
+ }
104
+
105
+ const gap = daysBetween(data.lastActiveDay, today);
106
+
107
+ // Same day — idempotent.
108
+ if (gap === 0) return data;
109
+
110
+ let newStreak: number;
111
+ if (gap <= 2) {
112
+ // 1-day gap = consecutive; 2-day gap = one missed day (grace).
113
+ newStreak = data.currentStreak + 1;
114
+ } else {
115
+ // Streak broken.
116
+ newStreak = 1;
117
+ }
118
+
119
+ return {
120
+ currentStreak: newStreak,
121
+ longestStreak: Math.max(newStreak, data.longestStreak),
122
+ lastActiveDay: today,
123
+ };
124
+ }
125
+
126
+ /**
127
+ * Public API: bump the streak for today.
128
+ * Gated by lastActiveDay so it's a no-op if already called today.
129
+ * Never throws.
130
+ */
131
+ export function bumpStreak(home: string = homedir(), nowMs: number = Date.now()): void {
132
+ try {
133
+ const data = readStreaks(home);
134
+ const today = todayKey(nowMs);
135
+ // If we already recorded today, skip the write entirely.
136
+ if (data.lastActiveDay === today) return;
137
+ const updated = computeNewStreak(data, today);
138
+ writeStreaks(updated, home);
139
+ } catch {
140
+ /* best-effort */
141
+ }
142
+ }
143
+
144
+ // ---------------------------------------------------------------------------
145
+ // Rendering helpers (used by status-line, dashboard, savings)
146
+ // ---------------------------------------------------------------------------
147
+
148
+ /** Returns a streak label like "5d streak" or "" when streak < 3. */
149
+ export function renderStreakLabel(data: StreakData): string {
150
+ if (data.currentStreak < 3) return "";
151
+ return `${data.currentStreak}d streak`;
152
+ }
153
+
154
+ /**
155
+ * Returns a streak badge for the status line: "* Nd streak" (ASCII star,
156
+ * no emoji — matches the no-emoji convention of the rest of the status line).
157
+ * Returns "" when streak < 3.
158
+ */
159
+ export function renderStreakBadge(data: StreakData): string {
160
+ if (data.currentStreak < 3) return "";
161
+ return `* ${data.currentStreak}d streak`;
162
+ }