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,873 @@
1
+ #!/usr/bin/env bun
2
+ /**
3
+ * ashlr status line script.
4
+ *
5
+ * Claude Code invokes this command periodically and renders the first line of
6
+ * stdout in its status bar. We surface lifetime + session token-savings from
7
+ * ~/.ashlr/stats.json, gated by toggles in ~/.claude/settings.json under the
8
+ * "ashlr" key:
9
+ *
10
+ * {
11
+ * "ashlr": {
12
+ * "statusLine": true, // master switch (default: true)
13
+ * "statusLineSession": true, // show "session +N" segment
14
+ * "statusLineLifetime": true, // show "lifetime +N" segment
15
+ * "statusLineTips": true // rotate a helpful tip at the end
16
+ * }
17
+ * }
18
+ *
19
+ * Contract:
20
+ * - Always exits 0.
21
+ * - On any error, emits an empty line.
22
+ * - Output is a single line, target ≤ 80 chars (Claude Code truncates
23
+ * overflow rather than scrolling, so we self-trim).
24
+ */
25
+
26
+ import { existsSync, readFileSync, statSync } from "fs";
27
+ import { homedir } from "os";
28
+ import { join } from "path";
29
+ import { c } from "./ui.ts";
30
+ import { maybeSyncToCloud, recordNudgeShown } from "../servers/_nudge-events.ts";
31
+ import { readSessionHint } from "../servers/_stats.ts";
32
+ import { costFor, costForSummarizer } from "../servers/_pricing.ts";
33
+ import { readStreaks, renderStreakBadge } from "../servers/_streaks.ts";
34
+ import { readAggregateCache } from "./stats-cloud-pull.ts";
35
+ import {
36
+ activityIndicator,
37
+ detectCapability,
38
+ frameAt,
39
+ renderContextPressure,
40
+ renderHeartbeat,
41
+ renderSparkline as renderAnimatedSparkline,
42
+ visibleWidth,
43
+ type Capability,
44
+ } from "./ui-animation.ts";
45
+
46
+ // ---------------------------------------------------------------------------
47
+ // Stats shape (v2 schema — keyed by session id)
48
+ // ---------------------------------------------------------------------------
49
+ //
50
+ // We read the file defensively and support both the v1 shape (`session`
51
+ // singular) and the v2 shape (`sessions` map + `schemaVersion: 2`). The
52
+ // status line should never crash a terminal because stats.json is on an
53
+ // old version — we just fall back to zero.
54
+
55
+ interface ByDay { [date: string]: { calls?: number; tokensSaved?: number } }
56
+ interface SessionBucket {
57
+ calls?: number;
58
+ tokensSaved?: number;
59
+ lastSavingAt?: string | null;
60
+ }
61
+ interface Stats {
62
+ schemaVersion?: number;
63
+ /** v2: per-session buckets keyed by CLAUDE_SESSION_ID. */
64
+ sessions?: Record<string, SessionBucket>;
65
+ /** v1 legacy shape — kept for readback compatibility during migration. */
66
+ session?: SessionBucket;
67
+ lifetime?: {
68
+ calls?: number;
69
+ tokensSaved?: number;
70
+ tokensSavedMeasured?: number;
71
+ measuredCalls?: number;
72
+ byDay?: ByDay;
73
+ };
74
+ }
75
+
76
+ /**
77
+ * Candidate session ids for this process. Claude Code forwards
78
+ * CLAUDE_SESSION_ID to status-line and hook invocations but NOT to MCP
79
+ * server subprocesses — so the status line sees one id and the MCP servers
80
+ * write under a different (PPID-derived) id. Returning every candidate
81
+ * lets the status line aggregate across whichever bucket actually got
82
+ * written.
83
+ *
84
+ * v1.20.2: also include the session-start hint id (~/.ashlr/last-project.json)
85
+ * so the status line converges on the same bucket the MCP writers use when
86
+ * neither side has CLAUDE_SESSION_ID. Without this, every Claude Code
87
+ * session shows session=0 because writers land in the hint-id bucket while
88
+ * the status line was looking at the ppid-id bucket.
89
+ */
90
+ function candidateSessionIds(env: NodeJS.ProcessEnv = process.env): string[] {
91
+ const ids: string[] = [];
92
+ const explicit = env.CLAUDE_SESSION_ID ?? env.ASHLR_SESSION_ID;
93
+ if (explicit && explicit.trim().length > 0) ids.push(explicit.trim());
94
+ const hint = readSessionHint(env.HOME ?? "");
95
+ if (hint && !ids.includes(hint)) ids.push(hint);
96
+ // PPID-hash fallback — matches the same shape used by servers/_stats.ts.
97
+ const seed = `ppid:${typeof process.ppid === "number" ? process.ppid : "?"}:${env.HOME ?? ""}`;
98
+ let h = 0;
99
+ for (let i = 0; i < seed.length; i++) h = ((h << 5) - h + seed.charCodeAt(i)) | 0;
100
+ const ppidId = `p${(h >>> 0).toString(16)}`;
101
+ if (!ids.includes(ppidId)) ids.push(ppidId);
102
+ return ids;
103
+ }
104
+
105
+ /** Pick the first candidate id that has a bucket — for display/startedAt fields. */
106
+ function currentSessionId(env: NodeJS.ProcessEnv = process.env): string {
107
+ return candidateSessionIds(env)[0]!;
108
+ }
109
+
110
+ /**
111
+ * Return aggregated session counters across every candidate id the current
112
+ * process could own, or null if no candidate bucket exists. Summing across
113
+ * candidates ensures the status line reflects the full session even when
114
+ * CLAUDE_SESSION_ID reached the status line but not the MCP servers (or
115
+ * vice versa).
116
+ */
117
+ function pickSession(stats: Stats | null, ids: string[]): SessionBucket | null {
118
+ if (!stats) return null;
119
+ // Primary path: sum across the v2 `sessions` map candidates.
120
+ if (stats.sessions) {
121
+ let calls = 0;
122
+ let tokensSaved = 0;
123
+ let latest: string | null = null;
124
+ let hit = false;
125
+ for (const id of ids) {
126
+ const b = stats.sessions[id];
127
+ if (!b) continue;
128
+ hit = true;
129
+ calls += b.calls ?? 0;
130
+ tokensSaved += b.tokensSaved ?? 0;
131
+ if (b.lastSavingAt && (!latest || b.lastSavingAt > latest)) latest = b.lastSavingAt;
132
+ }
133
+ if (hit) return { calls, tokensSaved, lastSavingAt: latest };
134
+ }
135
+ // Fallback: if the file is in v1 shape (zombie pre-v0.8.0 process still
136
+ // writing), surface the v1 `session` field rather than showing 0. The v1
137
+ // counter technically lies across concurrent terminals, but "lying
138
+ // slightly" beats "stuck at 0" for UX. The zombie-writer scenario
139
+ // resolves once the user fully restarts Claude Code.
140
+ if (stats.session && typeof stats.session.tokensSaved === "number") {
141
+ return {
142
+ calls: stats.session.calls ?? 0,
143
+ tokensSaved: stats.session.tokensSaved,
144
+ lastSavingAt: null,
145
+ };
146
+ }
147
+ return null;
148
+ }
149
+
150
+ interface AshlrSettings {
151
+ statusLine?: boolean;
152
+ statusLineSession?: boolean;
153
+ statusLineLifetime?: boolean;
154
+ statusLineTips?: boolean;
155
+ statusLineSparkline?: boolean;
156
+ /**
157
+ * When the current session crosses ~50k tokens saved and the user is on
158
+ * the free tier, swap the random tip for a single upgrade nudge. Default
159
+ * true. Set false to silence the nudge entirely.
160
+ */
161
+ statusLineUpgradeNudge?: boolean;
162
+ }
163
+
164
+ /** Session-token threshold above which free users see the Pro upgrade nudge. */
165
+ const UPGRADE_NUDGE_THRESHOLD = 50_000;
166
+
167
+ /** Upgrade nudge copy — kept short so it fits under typical 120-col budgets. */
168
+ const UPGRADE_NUDGE_TEXT = "50k+ saved — try Pro (7d trial /ashlr-upgrade)";
169
+
170
+ /** Copy-version string. Bump when we reword UPGRADE_NUDGE_TEXT so A/B math stays honest. */
171
+ const UPGRADE_NUDGE_VARIANT = "v1";
172
+
173
+ /**
174
+ * Lifetime-savings threshold that unlocks a one-shot celebration. We keep
175
+ * this low on purpose — first real "ok this plugin is paying for itself"
176
+ * moment fires around 10k tokens on most dev workflows. The celebration
177
+ * only runs once; subsequent renders see `milestones.ten_k_reached: true`
178
+ * in ~/.ashlr/milestones.json and stay silent.
179
+ */
180
+ const MILESTONE_10K_THRESHOLD = 10_000;
181
+
182
+ /**
183
+ * Format a token count into "≈$0.04" / "≈$0.00" using the active summarizer
184
+ * model's input price. This corrects the prior behaviour of always using
185
+ * Sonnet pricing (~$3/MTok) even when the actual summarizer was Haiku
186
+ * (~$0.80/MTok) — which inflated displayed savings by up to 3-4x.
187
+ *
188
+ * When ASHLR_PRICING_MODEL is explicitly set by the user, costForSummarizer
189
+ * honours that override for backward compat.
190
+ */
191
+ function formatCost(tokens: number): string {
192
+ if (!Number.isFinite(tokens) || tokens <= 0) return "≈$0.00";
193
+ const dollars = costForSummarizer(tokens);
194
+ if (dollars < 0.01) return "≈$0.00";
195
+ if (dollars < 10) return `≈$${dollars.toFixed(2)}`;
196
+ if (dollars < 1000) return `≈$${dollars.toFixed(1)}`;
197
+ return `≈$${Math.round(dollars)}`;
198
+ }
199
+
200
+ /**
201
+ * True when the user appears to be on Pro / Team (a local `~/.ashlr/pro-token`
202
+ * file is written by the upgrade flow once activation succeeds). Best-effort —
203
+ * a missing or empty file means "assume free". Never throws.
204
+ */
205
+ function hasProToken(home: string): boolean {
206
+ try {
207
+ const path = join(home, ".ashlr", "pro-token");
208
+ const st = statSync(path);
209
+ return st.isFile() && st.size > 0;
210
+ } catch {
211
+ return false;
212
+ }
213
+ }
214
+
215
+ /**
216
+ * Milestone tracking. Persisted in a separate JSON file from stats.json so
217
+ * this agent can stay out of Agent 1's stats-schema lane. Flags are flipped
218
+ * exactly once — subsequent renders read the flag and skip the celebration.
219
+ *
220
+ * Shape: `{ "ten_k_reached": true }` once fired.
221
+ */
222
+ interface Milestones {
223
+ ten_k_reached?: boolean;
224
+ /**
225
+ * One-shot flag set after the first-call savings projection has been
226
+ * shown to the user. Subsequent renders read this flag and stay silent
227
+ * even when lifetime.calls happens to be 1 again (e.g. stats reset).
228
+ */
229
+ first_call_celebrated?: boolean;
230
+ }
231
+
232
+ function readMilestones(home: string): Milestones {
233
+ try {
234
+ const path = join(home, ".ashlr", "milestones.json");
235
+ if (!existsSync(path)) return {};
236
+ const raw = readFileSync(path, "utf-8");
237
+ const parsed = JSON.parse(raw) as Milestones;
238
+ return parsed && typeof parsed === "object" ? parsed : {};
239
+ } catch {
240
+ return {};
241
+ }
242
+ }
243
+
244
+ /**
245
+ * Persist a milestone flag as a plain JSON blob. Best-effort, synchronous,
246
+ * and wrapped in try/catch so it never blocks the status-line render path.
247
+ * A failed write means the celebration might fire twice — acceptable
248
+ * tradeoff vs. crashing the terminal.
249
+ */
250
+ function writeMilestones(home: string, m: Milestones): void {
251
+ try {
252
+ const { mkdirSync, writeFileSync } = require("fs") as typeof import("fs");
253
+ const dir = join(home, ".ashlr");
254
+ mkdirSync(dir, { recursive: true });
255
+ writeFileSync(join(dir, "milestones.json"), JSON.stringify(m));
256
+ } catch {
257
+ /* best-effort */
258
+ }
259
+ }
260
+
261
+ /** Render a one-shot 10k celebration to stderr (stdout is the status line). */
262
+ function celebrate10k(home: string, stderr: NodeJS.WritableStream = process.stderr): void {
263
+ try {
264
+ stderr.write("🎉 10,000 tokens saved! ashlr just paid for your next coffee.\n");
265
+ } catch {
266
+ /* ignore — diag output is best-effort */
267
+ }
268
+ const current = readMilestones(home);
269
+ writeMilestones(home, { ...current, ten_k_reached: true });
270
+ }
271
+
272
+ /**
273
+ * Render the one-shot "first ashlr tool call" celebration to stderr.
274
+ *
275
+ * This closes the audit-identified "$0.001 saved → user closes terminal"
276
+ * drop-off cliff: on the very first successful ashlr call we surface a
277
+ * lifetime-value projection so the user immediately sees the annualized
278
+ * upside, not just a tiny per-call delta.
279
+ *
280
+ * Math (intentionally simple, intentionally explained inline so reviewers
281
+ * don't have to chase it):
282
+ * - dailyEstimate = perCallTokens (one call's worth — the call that
283
+ * just landed)
284
+ * - annualEstimate = dailyEstimate × 50 calls/day × 250 work days/year
285
+ *
286
+ * Cost uses `costFor()` (NOT `costForSummarizer`) on purpose: the user
287
+ * cares about the counterfactual — "what would this have cost on my main
288
+ * model if ashlr weren't here" — which is main-model pricing. This is the
289
+ * same reasoning behind the v1.31 $0-fix where the summarizer fallback
290
+ * lands on costFor when no summarizer is configured.
291
+ */
292
+ function celebrateFirstCall(
293
+ home: string,
294
+ perCallTokens: number,
295
+ stderr: NodeJS.WritableStream = process.stderr,
296
+ ): void {
297
+ const dailyEstimate = perCallTokens;
298
+ const annualEstimate = dailyEstimate * 50 * 250;
299
+ const perCallCost = costFor(perCallTokens);
300
+ const annualCost = costFor(annualEstimate);
301
+ const perCallDollars = perCallCost < 0.01 ? "≈$0.00" : `≈$${perCallCost.toFixed(2)}`;
302
+ const annualDollars =
303
+ annualCost < 0.01
304
+ ? "≈$0.00"
305
+ : annualCost < 10
306
+ ? `≈$${annualCost.toFixed(2)}`
307
+ : annualCost < 1000
308
+ ? `≈$${annualCost.toFixed(0)}`
309
+ : `≈$${Math.round(annualCost).toLocaleString("en-US")}`;
310
+ try {
311
+ stderr.write(
312
+ `🎉 ashlr saved you ${formatTokens(perCallTokens)} tokens on your first call (${perCallDollars}).\n` +
313
+ ` At this rate (~50 calls/day × 250 work days/year) → ${annualDollars}/year saved.\n`,
314
+ );
315
+ } catch {
316
+ /* ignore — diag output is best-effort */
317
+ }
318
+ const current = readMilestones(home);
319
+ writeMilestones(home, { ...current, first_call_celebrated: true });
320
+ }
321
+
322
+ // 9-rung Braille ladder: empty → full. Each char represents one day's
323
+ // tokens-saved, bucketed against the busiest day in the window.
324
+ // 0% → ⠀ (U+2800, blank-but-present braille), 100% → ⣿
325
+ const SPARK_LADDER = ["\u2800", "\u2840", "\u2844", "\u2846", "\u2847", "\u28E7", "\u28F7", "\u28FF", "\u28FF"];
326
+ // Note: we use a 9-slot ladder so that ratio 0 maps to blank, anything >0 maps
327
+ // to at least the first rung (so an active-but-quiet day is still visible).
328
+
329
+ function lastNDayKeys(n: number): string[] {
330
+ const out: string[] = [];
331
+ const now = new Date();
332
+ for (let i = n - 1; i >= 0; i--) {
333
+ const d = new Date(now);
334
+ d.setUTCDate(d.getUTCDate() - i);
335
+ out.push(d.toISOString().slice(0, 10));
336
+ }
337
+ return out;
338
+ }
339
+
340
+ export function renderSparkline(
341
+ byDay: Record<string, { tokensSaved?: number }> | undefined,
342
+ days = 7,
343
+ ): string {
344
+ const keys = lastNDayKeys(days);
345
+ const vals = keys.map((k) => byDay?.[k]?.tokensSaved ?? 0);
346
+ const max = Math.max(...vals, 0);
347
+ if (max <= 0) return SPARK_LADDER[0]!.repeat(days);
348
+ return vals
349
+ .map((v) => {
350
+ if (v <= 0) return SPARK_LADDER[0]!;
351
+ // Scale 1..max → rungs 1..8 (skip rung 0 so any activity is visible).
352
+ const idx = Math.max(1, Math.min(8, Math.ceil((v / max) * 8)));
353
+ return SPARK_LADDER[idx]!;
354
+ })
355
+ .join("");
356
+ }
357
+
358
+ const MAX_LEN = 80;
359
+
360
+ // Editable list — keep small, keep useful.
361
+ const TIPS: readonly string[] = [
362
+ "use /ashlr-savings to see totals",
363
+ "ashlr__read auto-snips files >2KB",
364
+ "ashlr__edit ships diffs, not full files",
365
+ "ashlr__grep is genome-aware in mapped repos",
366
+ "toggle status line via /ashlr-settings",
367
+ "run `ashlr map` to build a code genome",
368
+ "savings persist in ~/.ashlr/stats.json",
369
+ ];
370
+
371
+ /** Resolve the terminal-width budget for the status line.
372
+ * - Reads $COLUMNS from the environment (terminals typically set this).
373
+ * - Clamped to [1, 120] to avoid absurdly wide rendering.
374
+ * - Falls back to 80 when unset/invalid.
375
+ */
376
+ export function resolveBudget(env: NodeJS.ProcessEnv = process.env): number {
377
+ const raw = env.COLUMNS;
378
+ const parsed = raw ? parseInt(raw, 10) : NaN;
379
+ if (!Number.isFinite(parsed) || parsed <= 0) return 80;
380
+ return Math.min(parsed, 120);
381
+ }
382
+
383
+ export function formatTokens(n: number): string {
384
+ if (!Number.isFinite(n) || n < 0) return "0";
385
+ if (n < 1000) return String(Math.floor(n));
386
+ if (n < 1_000_000) return (n / 1000).toFixed(1) + "K";
387
+ return (n / 1_000_000).toFixed(1) + "M";
388
+ }
389
+
390
+ function readJson<T>(path: string): T | null {
391
+ try {
392
+ if (!existsSync(path)) return null;
393
+ return JSON.parse(readFileSync(path, "utf-8")) as T;
394
+ } catch {
395
+ return null;
396
+ }
397
+ }
398
+
399
+ // ---------------------------------------------------------------------------
400
+ // Read cache (mtime-aware, 300ms TTL)
401
+ // ---------------------------------------------------------------------------
402
+ // Claude Code invokes the status line frequently (on every prompt tick).
403
+ // Re-reading + re-parsing stats.json on every call is wasteful — the status
404
+ // bar does not need sub-millisecond freshness. We cache JSON reads for 300ms
405
+ // keyed by absolute path, but a cache entry is invalidated immediately whenever
406
+ // the file's mtime changes (or its existence flips). That makes the cache
407
+ // effective under steady load (caps at ~3 reads/second) while staying correct
408
+ // across terminals: as soon as terminal A's debounce flush lands on disk (≤250ms),
409
+ // terminal B's next status-line poll sees the new mtime and re-reads.
410
+ //
411
+ // Net worst-case latency after recordSaving: 250ms debounce + 300ms TTL = 550ms.
412
+ // Typical case when mtime changes mid-window: 250ms debounce + 0ms (mtime
413
+ // invalidation fires immediately) = ~250ms.
414
+ //
415
+ // Cache is process-local — each fresh invocation of this script as a Claude
416
+ // Code subprocess starts empty; the cache only helps long-running hosts and
417
+ // within-test batch calls.
418
+ const READ_CACHE_TTL_MS = 300;
419
+ interface CacheEntry {
420
+ at: number;
421
+ mtimeMs: number;
422
+ value: unknown;
423
+ }
424
+ const _readCache = new Map<string, CacheEntry>();
425
+
426
+ function fileMtime(path: string): number {
427
+ try {
428
+ return statSync(path).mtimeMs;
429
+ } catch {
430
+ // Missing → sentinel -1 so "absent → present" also invalidates.
431
+ return -1;
432
+ }
433
+ }
434
+
435
+ function readJsonCached<T>(path: string): T | null {
436
+ const now = Date.now();
437
+ const mtime = fileMtime(path);
438
+ const hit = _readCache.get(path);
439
+ if (hit && now - hit.at < READ_CACHE_TTL_MS && hit.mtimeMs === mtime) {
440
+ return hit.value as T | null;
441
+ }
442
+ const fresh = readJson<T>(path);
443
+ _readCache.set(path, { at: now, mtimeMs: mtime, value: fresh });
444
+ return fresh;
445
+ }
446
+
447
+ /** Test hook — flush the 2s read cache. */
448
+ export function _resetReadCache(): void {
449
+ _readCache.clear();
450
+ }
451
+
452
+ function pickTip(tips: readonly string[], seed?: number): string {
453
+ if (tips.length === 0) return "";
454
+ const idx = (seed ?? Math.floor(Date.now() / 86_400_000)) % tips.length;
455
+ return tips[idx]!;
456
+ }
457
+
458
+ /**
459
+ * Subset of the JSON payload Claude Code may send on stdin to the status-line
460
+ * command. We try multiple field names defensively; if none resolve to a
461
+ * positive number the context-pressure widget is hidden entirely.
462
+ *
463
+ * Known / candidate fields (any may be present depending on CC version):
464
+ * input_tokens — tokens consumed in the current context window
465
+ * context_tokens — alias used in some builds
466
+ * total_tokens — alias used in some builds
467
+ * total_tokens_used — alias
468
+ * sessionTokens — alias
469
+ * context_used_tokens — tokens used (paired with context_limit_tokens)
470
+ * context_limit_tokens — total window size
471
+ */
472
+ export interface StatusLineInput {
473
+ input_tokens?: number;
474
+ context_tokens?: number;
475
+ total_tokens?: number;
476
+ total_tokens_used?: number;
477
+ sessionTokens?: number;
478
+ context_used_tokens?: number;
479
+ context_limit_tokens?: number;
480
+ [key: string]: unknown;
481
+ }
482
+
483
+ /** Extract a 0–100 context-pressure percentage from a CC stdin payload.
484
+ * Returns null when no usable field is found. */
485
+ export function extractContextPct(input: StatusLineInput | null | undefined): number | null {
486
+ if (!input) return null;
487
+ // Prefer the explicit used+limit pair — most precise.
488
+ if (
489
+ typeof input.context_used_tokens === "number" &&
490
+ typeof input.context_limit_tokens === "number" &&
491
+ input.context_limit_tokens > 0
492
+ ) {
493
+ return Math.min(100, (input.context_used_tokens / input.context_limit_tokens) * 100);
494
+ }
495
+ // Fall through to token-count fields — these need a known window size.
496
+ // Claude claude-sonnet-4-6 / opus context windows are large; without a limit
497
+ // field we cannot compute a meaningful %. Hide the widget.
498
+ return null;
499
+ }
500
+
501
+ export interface BuildOptions {
502
+ home?: string;
503
+ /** Deterministic tip index, used by tests. */
504
+ tipSeed?: number;
505
+ /** Explicit budget override (bypasses $COLUMNS detection). */
506
+ budget?: number;
507
+ /** Environment to read $COLUMNS from — used by tests. */
508
+ env?: NodeJS.ProcessEnv;
509
+ /** Clock injection — tests pin this; production uses Date.now. */
510
+ now?: number;
511
+ /**
512
+ * Parsed JSON payload from Claude Code's stdin. When present, we extract a
513
+ * context-pressure percentage and render the ctx widget. When absent/null
514
+ * the widget is hidden entirely (never lies).
515
+ */
516
+ statusLineInput?: StatusLineInput | null;
517
+ /**
518
+ * Test hook. When true, the status line still renders the upgrade nudge
519
+ * but does not emit a nudge_shown telemetry event. Production callers
520
+ * never set this; existing tests that don't care about telemetry leave
521
+ * it undefined and rely on the HOME sandbox to isolate the log file.
522
+ */
523
+ suppressNudgeTelemetry?: boolean;
524
+ /**
525
+ * Test hook. When true, the 10k celebration is evaluated for rendering
526
+ * but does not write the milestones.json flag and does not print to
527
+ * stderr. Lets tests assert on render output without polluting the tmp
528
+ * HOME or stderr.
529
+ */
530
+ suppressMilestoneSideEffects?: boolean;
531
+ /**
532
+ * "Last save" window in milliseconds. The "last save" segment is shown only
533
+ * when msSinceActive is within this window. Defaults to 60 seconds.
534
+ */
535
+ lastSaveWindowMs?: number;
536
+ }
537
+
538
+ export function buildStatusLine(opts: BuildOptions = {}): string {
539
+ try {
540
+ const home = opts.home ?? homedir();
541
+ const env = opts.env ?? process.env;
542
+ const budget = opts.budget ?? resolveBudget(env);
543
+ const now = opts.now ?? Date.now();
544
+ const settings = readJsonCached<{ ashlr?: AshlrSettings }>(
545
+ join(home, ".claude", "settings.json"),
546
+ );
547
+ const cfg: AshlrSettings = settings?.ashlr ?? {};
548
+
549
+ // Defaults: master on, session on, lifetime on, tips on.
550
+ const master = cfg.statusLine ?? true;
551
+ if (!master) return "";
552
+
553
+ const showSession = cfg.statusLineSession ?? true;
554
+ const showLifetime = cfg.statusLineLifetime ?? true;
555
+ const showTips = cfg.statusLineTips ?? true;
556
+ const showSpark = cfg.statusLineSparkline ?? true;
557
+
558
+ const statsPath = join(home, ".ashlr", "stats.json");
559
+ const statsExists = existsSync(statsPath);
560
+ const stats = readJsonCached<Stats>(statsPath);
561
+ const sessionIds = candidateSessionIds(env);
562
+ const sess = pickSession(stats, sessionIds);
563
+ // v1.34: use API-measured figure when >= 10 measured calls exist
564
+ const lifetimeMeasuredCalls = stats?.lifetime?.measuredCalls ?? 0;
565
+ const showMeasured = lifetimeMeasuredCalls >= 10;
566
+ const session = sess?.tokensSaved ?? 0;
567
+ const lifetime = showMeasured
568
+ ? (stats?.lifetime?.tokensSavedMeasured ?? 0)
569
+ : (stats?.lifetime?.tokensSaved ?? 0);
570
+ const methodSuffix = showMeasured ? ' (API-measured)' : ' (est.)';
571
+ const lifetimeCalls = stats?.lifetime?.calls ?? 0;
572
+ const lastSavingAt = sess?.lastSavingAt ?? null;
573
+ const msSinceActive = lastSavingAt ? Math.max(0, now - Date.parse(lastSavingAt)) : Number.POSITIVE_INFINITY;
574
+
575
+ const cap = detectCapability(env);
576
+ const frame = frameAt(now);
577
+
578
+ // -----------------------------------------------------------------------
579
+ // Left edge: "ashlr" brand + heartbeat + animated sparkline
580
+ // -----------------------------------------------------------------------
581
+ const brandParts: string[] = ["ashlr"];
582
+ if (showSpark) {
583
+ // Heartbeat glyph: dim middle-dot when idle, braille-wave when active.
584
+ brandParts.push(renderHeartbeat(frame, msSinceActive, cap));
585
+ // 7-day sparkline. Values come from the existing lifetime.byDay map
586
+ // so the 7-day shape stays stable across the new per-session stats.
587
+ const keys = lastNDayKeys(7);
588
+ const values = keys.map((k) => stats?.lifetime?.byDay?.[k]?.tokensSaved ?? 0);
589
+ const spark = renderAnimatedSparkline({ values, frame, msSinceActive, cap });
590
+ // Wide terminals (>100 cols) get a "7d " label before the sparkline so
591
+ // users know at a glance what the cells represent. Under 100 cols the
592
+ // prefix is dropped to keep things compact. Width-stable in both modes.
593
+ const sparkPrefix = budget > 100 ? "7d " : "";
594
+ brandParts.push(sparkPrefix + spark);
595
+ }
596
+ const brand = brandParts.join(" ");
597
+
598
+ const parts: string[] = [brand];
599
+
600
+ // Context-pressure widget — inserted between sparkline and "session +N".
601
+ // Hidden entirely when no usable payload field is present.
602
+ const ctxPct = extractContextPct(opts.statusLineInput);
603
+ const ctxWidget = ctxPct !== null ? renderContextPressure(ctxPct, cap) : null;
604
+ if (ctxWidget !== null) parts.push(ctxWidget);
605
+
606
+ const actIndicator = activityIndicator(msSinceActive, cap);
607
+ // Session segment gains an inline `$` cost estimate (≈$0.04) right after
608
+ // the token counter. The cost is built from a shared per-MTok price so a
609
+ // future module swap only changes SESSION_PRICE_PER_MTOK / the helper.
610
+ const sessionCost = formatCost(session);
611
+ if (showSession) {
612
+ if (!statsExists) {
613
+ // Stats file doesn't exist yet — waiting for the first tool call.
614
+ // Render a distinct waiting message instead of the regular counters.
615
+ parts.push("(waiting for first tool call)");
616
+ } else if (stats !== null && lifetimeCalls < 5 && session === 0) {
617
+ // Stats file present but very early (< 5 lifetime calls) AND the
618
+ // current session has no tokens yet — not enough data to show
619
+ // meaningful counters. Render a dim hint so users know the plugin
620
+ // is warming up (not broken). If the session HAS tokens, fall
621
+ // through to the normal path even if lifetime is low — the user
622
+ // is actively saving and deserves to see the counter tick.
623
+ parts.push(`session +0 (collecting…)`);
624
+ } else {
625
+ // Normal path: stats file present and established (≥ 5 calls) or
626
+ // stats is null/corrupt (falls back gracefully to +0 counters).
627
+ parts.push(`session ${actIndicator}+${formatTokens(session)} ${sessionCost}`);
628
+ }
629
+ }
630
+ if (showLifetime && statsExists && lifetime > 0)
631
+ parts.push(`lifetime +${formatTokens(lifetime)}${methodSuffix}`);
632
+
633
+ // -----------------------------------------------------------------------
634
+ // Streak badge — shown when currentStreak >= 3.
635
+ // Read from ~/.ashlr/streaks.json; no-op on any error.
636
+ // -----------------------------------------------------------------------
637
+ const streakData = readStreaks(home);
638
+ const streakBadge = renderStreakBadge(streakData);
639
+ if (streakBadge) parts.push(streakBadge);
640
+
641
+ // -----------------------------------------------------------------------
642
+ // Cross-machine badge (Pro only) — "☁ N machines" suffix.
643
+ // Reads from the 1h aggregate cache; silent for free-tier users.
644
+ // Only shown when machine_count > 1 (trivial for single-machine users).
645
+ // -----------------------------------------------------------------------
646
+ try {
647
+ const aggData = readAggregateCache();
648
+ if (aggData?.machine_count != null && aggData.machine_count > 1) {
649
+ parts.push(`☁ ${aggData.machine_count} machines`);
650
+ }
651
+ } catch {
652
+ /* aggregate cache is decoration — never break status line */
653
+ }
654
+
655
+ // -----------------------------------------------------------------------
656
+ // Brief badge — `[brief: <level>]` when /ashlr-brief is active.
657
+ // Reads ~/.ashlr/brief.json (project file wins via the SessionStart hook,
658
+ // but for status-line purposes we just surface the user-level state +
659
+ // an indicator if a project file exists). Hidden when level is "off".
660
+ // -----------------------------------------------------------------------
661
+ try {
662
+ const userBriefPath = join(home, ".ashlr", "brief.json");
663
+ if (existsSync(userBriefPath)) {
664
+ const cfg = JSON.parse(readFileSync(userBriefPath, "utf-8")) as { level?: string };
665
+ if (cfg.level && cfg.level !== "off") {
666
+ parts.push(`brief: ${cfg.level}`);
667
+ }
668
+ }
669
+ } catch {
670
+ /* brief badge is decoration — never break status line */
671
+ }
672
+
673
+ // -----------------------------------------------------------------------
674
+ // "Last save" segment — shows the most recent ashlr tool call within the
675
+ // last N seconds (default: 60s). Format: "last: ashlr__read +5K [2s ago]"
676
+ // Uses lastSavingAt + byTool from the session bucket.
677
+ // -----------------------------------------------------------------------
678
+ const lastSaveWindowMs = opts.lastSaveWindowMs ?? 60_000;
679
+ if (msSinceActive <= lastSaveWindowMs && lastSavingAt !== null) {
680
+ const secsAgo = Math.floor(msSinceActive / 1000);
681
+ const agoStr = secsAgo <= 1 ? "1s ago" : `${secsAgo}s ago`;
682
+ // Best-effort: find the tool with the most recent save across candidates.
683
+ // We show the tool that has the highest tokensSaved as a proxy for the
684
+ // "just fired" tool (no per-call timestamp is stored, only per-session).
685
+ const sessionBuckets = stats?.sessions;
686
+ let lastToolName = "";
687
+ let lastToolTok = 0;
688
+ if (sessionBuckets) {
689
+ for (const id of sessionIds) {
690
+ const b = sessionBuckets[id];
691
+ if (!b) continue;
692
+ const bt = (b as unknown as { byTool?: Record<string, { tokensSaved?: number }> }).byTool;
693
+ if (!bt) continue;
694
+ for (const [name, info] of Object.entries(bt)) {
695
+ const tok = info?.tokensSaved ?? 0;
696
+ if (tok > lastToolTok) { lastToolTok = tok; lastToolName = name; }
697
+ }
698
+ }
699
+ }
700
+ if (lastToolName && lastToolTok > 0) {
701
+ parts.push(`last: ${lastToolName} +${formatTokens(lastToolTok)} [${agoStr}]`);
702
+ }
703
+ }
704
+
705
+ // One-shot 10k lifetime celebration. Fires the first time lifetime
706
+ // crosses MILESTONE_10K_THRESHOLD; persisted in milestones.json so
707
+ // subsequent renders stay silent. Stderr output so the status line
708
+ // (stdout) remains a single unbroken line for Claude Code.
709
+ //
710
+ // Disabled by opts.suppressMilestoneSideEffects (test hook) or by the
711
+ // ASHLR_DISABLE_MILESTONES env var (CI / debugging).
712
+ const milestonesDisabled =
713
+ opts.suppressMilestoneSideEffects === true ||
714
+ (env.ASHLR_DISABLE_MILESTONES && env.ASHLR_DISABLE_MILESTONES !== "0");
715
+ if (!milestonesDisabled) {
716
+ const ms = readMilestones(home);
717
+ // First-call projection — fires exactly once on the lifetime.calls === 1
718
+ // transition. Closes the audit's drop-off cliff by surfacing the
719
+ // annualized upside the moment a new user sees their first save.
720
+ if (!ms.first_call_celebrated && lifetimeCalls === 1 && lifetime > 0) {
721
+ celebrateFirstCall(home, lifetime);
722
+ }
723
+ if (!ms.ten_k_reached && lifetime >= MILESTONE_10K_THRESHOLD) {
724
+ celebrate10k(home);
725
+ }
726
+ }
727
+
728
+ let line = parts.join(" · ");
729
+
730
+ // Drop-order under tight budget:
731
+ // 1. Try to add the tip — drop tip first if it doesn't fit.
732
+ // 2. If still over budget, drop the ctx widget.
733
+ // 3. Hard-truncate as last resort.
734
+ if (showTips) {
735
+ // When a free-tier user crosses the session savings threshold, swap the
736
+ // rotating tip for a single targeted upgrade nudge. `Pro token` absence
737
+ // is the cheap tier check — good enough for a status-line hint and
738
+ // avoids any backend round-trip on every render.
739
+ const nudgeEnabled = cfg.statusLineUpgradeNudge ?? true;
740
+ const showNudge =
741
+ nudgeEnabled && session >= UPGRADE_NUDGE_THRESHOLD && !hasProToken(home);
742
+ const tipText = showNudge ? UPGRADE_NUDGE_TEXT : pickTip(TIPS, opts.tipSeed);
743
+ const tipLabel = showNudge ? "↑" : "tip";
744
+ const candidate = `${line} · ${tipLabel}: ${tipText}`;
745
+ if (visibleWidth(candidate) <= budget) {
746
+ line = candidate;
747
+ // Only emit telemetry when the nudge actually fits on the rendered
748
+ // line — a truncated nudge the user never sees shouldn't count.
749
+ if (showNudge && !opts.suppressNudgeTelemetry) {
750
+ // Fire-and-forget: status line must never block on IO.
751
+ // Pass `home` through so tests using a tmp $HOME stay sandboxed.
752
+ void recordNudgeShown({
753
+ tokenCount: session,
754
+ variant: UPGRADE_NUDGE_VARIANT,
755
+ home,
756
+ });
757
+ maybeSyncToCloud();
758
+ }
759
+ }
760
+ // Otherwise drop the tip/nudge entirely (no partial rendering).
761
+ }
762
+
763
+ // Drop order when still over budget after tip was dropped:
764
+ // (a) drop last-save + streak badge first (lowest signal under pressure)
765
+ // (b) drop cost suffix (saves ~7 chars)
766
+ // (c) then drop ctx widget if still overflowing
767
+ // Cost is cheap bookkeeping, ctx widget is higher-signal for heavy users,
768
+ // so cost goes before ctx.
769
+ if (visibleWidth(line) > budget) {
770
+ // Drop streak badge + last-save first — they're bonus delight, not core data.
771
+ const partsNoExtras: string[] = [brand];
772
+ if (ctxWidget !== null) partsNoExtras.push(ctxWidget);
773
+ if (showSession) partsNoExtras.push(`session ${actIndicator}+${formatTokens(session)} ${sessionCost}`);
774
+ if (showLifetime) partsNoExtras.push(`lifetime +${formatTokens(lifetime)}`);
775
+ line = partsNoExtras.join(" · ");
776
+ }
777
+
778
+ if (visibleWidth(line) > budget) {
779
+ const partsNoCost: string[] = [brand];
780
+ if (ctxWidget !== null) partsNoCost.push(ctxWidget);
781
+ if (showSession) partsNoCost.push(`session ${actIndicator}+${formatTokens(session)}`);
782
+ if (showLifetime) partsNoCost.push(`lifetime +${formatTokens(lifetime)}`);
783
+ line = partsNoCost.join(" · ");
784
+ }
785
+
786
+ // If ctx widget caused overflow (narrow terminal), rebuild without it.
787
+ if (ctxWidget !== null && visibleWidth(line) > budget) {
788
+ const partsNoCtx: string[] = [brand];
789
+ if (showSession) partsNoCtx.push(`session ${actIndicator}+${formatTokens(session)}`);
790
+ if (showLifetime) partsNoCtx.push(`lifetime +${formatTokens(lifetime)}`);
791
+ line = partsNoCtx.join(" · ");
792
+ // Tip + cost + streak were already dropped above (we only reach here when over budget).
793
+ }
794
+
795
+ // Budget enforcement operates on VISIBLE width — ANSI escapes don't count.
796
+ if (visibleWidth(line) > budget) {
797
+ // Last-resort safety. A naive `line.slice()` could cut mid-ANSI and
798
+ // leak a dangling escape that corrupts the terminal. Strip ANSI first
799
+ // so the slice operates on visible characters only.
800
+ const stripped = line.replace(/\x1b\[[0-9;]*m/g, "");
801
+ line = stripped.slice(0, Math.max(0, budget - 1)) + "…";
802
+ }
803
+
804
+ return colorize(line);
805
+ } catch {
806
+ return "";
807
+ }
808
+ }
809
+
810
+ /** Overlay color on a finished, length-bounded status line. */
811
+ function colorize(line: string): string {
812
+ // Brand (green, bold) — the very first "ashlr" token.
813
+ let out = line.replace(/^ashlr\b/, c.bold(c.brightGreen("ashlr")));
814
+ // Savings numbers — green for positive, dim grey for the zero case.
815
+ // The session segment may have an activity indicator glyph (possibly ANSI-
816
+ // wrapped) between the label and the "+N" — capture it with a lazy wildcard
817
+ // that matches ANSI escape sequences and/or a single Unicode/ASCII glyph.
818
+ out = out.replace(/(session |lifetime )((?:\x1b\[[0-9;]*m)*[↑+]?(?:\x1b\[[0-9;]*m)*)\+([\d.]+[KM]?)/g, (_m, lbl, indicator, num) => {
819
+ const isZero = num === "0";
820
+ const coloredLabel = c.dim(lbl);
821
+ const coloredNum = isZero ? c.dim(`+${num}`) : c.green(`+${num}`);
822
+ // indicator may be empty (idle) or already ANSI-wrapped by activityIndicator.
823
+ return `${coloredLabel}${indicator}${coloredNum}`;
824
+ });
825
+ // Cost suffix after the session counter — dim grey so it doesn't compete
826
+ // with the primary token count. Matches "≈$0.04" / "≈$12" / "≈$1.2K".
827
+ out = out.replace(/(\s)(≈\$[0-9]+(?:\.[0-9]+)?[KM]?)/g, (_m, sp, money) => `${sp}${c.dim(money)}`);
828
+ // Tip prefix — dim cyan label, dim body.
829
+ out = out.replace(/tip: (.+)$/, (_m, body) => `${c.cyan("tip:")} ${c.dim(body)}`);
830
+ // Early-state hints — dim italic for collecting… and waiting messages.
831
+ out = out.replace(/(\(collecting…\))/, (_m, s) => c.italic(c.dim(s)));
832
+ out = out.replace(/(\(waiting for first tool call\))/, (_m, s) => c.dim(s));
833
+ // Mid-dot separators — dim.
834
+ out = out.replaceAll(" · ", ` ${c.dim("\u00B7")} `);
835
+ return out;
836
+ }
837
+
838
+ // ---------------------------------------------------------------------------
839
+ // Stdin reader — non-blocking, 50ms deadline
840
+ // ---------------------------------------------------------------------------
841
+ // Claude Code may pipe a JSON payload describing the current session state.
842
+ // We read it with a hard 50ms deadline so we never stall the terminal on a
843
+ // slow or empty stdin. Returns null on timeout, parse error, or empty input.
844
+
845
+ async function readStdinPayload(): Promise<StatusLineInput | null> {
846
+ return new Promise<StatusLineInput | null>((resolve) => {
847
+ // Resolve null if stdin is a TTY (nothing to read) or non-readable.
848
+ if (process.stdin.isTTY) { resolve(null); return; }
849
+
850
+ let raw = "";
851
+ const timer = setTimeout(() => {
852
+ process.stdin.destroy();
853
+ resolve(null);
854
+ }, 50);
855
+
856
+ process.stdin.setEncoding("utf-8");
857
+ process.stdin.on("data", (chunk: string) => { raw += chunk; });
858
+ process.stdin.on("end", () => {
859
+ clearTimeout(timer);
860
+ if (!raw.trim()) { resolve(null); return; }
861
+ try { resolve(JSON.parse(raw) as StatusLineInput); }
862
+ catch { resolve(null); }
863
+ });
864
+ process.stdin.on("error", () => { clearTimeout(timer); resolve(null); });
865
+ });
866
+ }
867
+
868
+ // Run as script (skip when imported by tests).
869
+ if (import.meta.main) {
870
+ const statusLineInput = await readStdinPayload();
871
+ process.stdout.write(buildStatusLine({ statusLineInput }) + "\n");
872
+ process.exit(0);
873
+ }