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,719 @@
1
+ /**
2
+ * _telemetry.ts — Opt-in anonymized telemetry buffer + writer.
3
+ *
4
+ * STRICTLY OPT-IN. Default OFF. Users enable via:
5
+ * - ~/.ashlr/config.json :: { "telemetry": "opt-in" }
6
+ * - env: ASHLR_TELEMETRY=on
7
+ *
8
+ * Kill switch (always respected, highest priority):
9
+ * - ~/.ashlr/config.json :: { "telemetry": "off" }
10
+ * - env: ASHLR_TELEMETRY=off
11
+ *
12
+ * What is collected:
13
+ * - Tool call shapes: tool name, raw/compact byte counts, fell-back flag,
14
+ * provider used, duration. No file paths, no patterns, no content.
15
+ * - Pre-tool-use block metadata: tool name, blocked-to destination, size bucket.
16
+ * - Pre-tool-use passthrough reason: tool name, bypass reason string.
17
+ * - Session header: plugin version, bun version, platform, arch.
18
+ *
19
+ * What is NEVER collected:
20
+ * - File paths
21
+ * - Grep patterns
22
+ * - File content or command arguments
23
+ * - User identifiers or repo names
24
+ *
25
+ * Design:
26
+ * - Synchronous append to ~/.ashlr/telemetry-buffer.jsonl (same pattern as
27
+ * _recent-blocks.ts — atomic for lines < PIPE_BUF).
28
+ * - Capped at MAX_BUFFER_LINES (5000). Oldest entries evicted when exceeded.
29
+ * - Eviction is lazy (checked on write) using atomic rename to avoid blocking
30
+ * the hot path most of the time.
31
+ * - Never throws. Never blocks tool functionality.
32
+ */
33
+
34
+ import { appendFileSync, existsSync, mkdirSync, readFileSync, renameSync, writeFileSync } from "fs";
35
+ import { homedir } from "os";
36
+ import { dirname, join } from "path";
37
+ import { existsSync as fsExistsSync, readFileSync as fsReadFileSync } from "fs";
38
+
39
+ // ---------------------------------------------------------------------------
40
+ // Constants
41
+ // ---------------------------------------------------------------------------
42
+
43
+ export const MAX_BUFFER_LINES = 5000;
44
+
45
+ // Eviction is triggered when the file grows past this multiple of MAX_BUFFER_LINES.
46
+ // Using 1.1 (10% overage) keeps the file tight without frequent rewrites.
47
+ const EVICTION_FACTOR = 1.1;
48
+
49
+ // ---------------------------------------------------------------------------
50
+ // Types
51
+ // ---------------------------------------------------------------------------
52
+
53
+ /** Size bucket for pre-tool-use block events. Never raw bytes. */
54
+ export type SizeRange = "small" | "medium" | "large";
55
+
56
+ /** Canonical telemetry event kinds. */
57
+ export type TelemetryEventKind =
58
+ | "tool_call"
59
+ | "pretooluse_block"
60
+ | "pretooluse_passthrough"
61
+ | "version"
62
+ // "multi_turn_stale_estimate" removed in v1.30 (1.5) — wired since v1.24, never populated.
63
+ | "hook_perf"
64
+ | "genome_compression_ratio"
65
+ | "wizard_step"
66
+ | "pre_compaction_nudge_emitted";
67
+
68
+ /** tool_call payload — what the maintainer needs to tune heuristics. */
69
+ export interface ToolCallPayload {
70
+ tool: string;
71
+ /** The MCP tool name (e.g. "ashlr__grep"). Added in v1.30 (1.1). */
72
+ tool_name?: string;
73
+ rawBytes: number;
74
+ compactBytes: number;
75
+ fellBack: boolean;
76
+ providerUsed: string;
77
+ durationMs: number;
78
+ }
79
+
80
+ /** pretooluse_block payload. */
81
+ export interface PreToolUseBlockPayload {
82
+ tool: string;
83
+ blockedTo: string;
84
+ sizeRange: SizeRange;
85
+ }
86
+
87
+ /** pretooluse_passthrough payload. */
88
+ export interface PreToolUsePassthroughPayload {
89
+ tool: string;
90
+ reason: "below-threshold" | "out-of-cwd" | "plugin-tree" | "micro-edit" | "bypass";
91
+ }
92
+
93
+ /** version / session-header payload. Sent once per session. */
94
+ export interface VersionPayload {
95
+ pluginVersion: string;
96
+ bunVersion: string;
97
+ platform: string;
98
+ arch: string;
99
+ }
100
+
101
+ /**
102
+ * multi_turn_stale_estimate payload — removed in v1.30 (1.5).
103
+ * Kept as a type alias for backward-compat with any call sites that may still
104
+ * reference it; the event kind is no longer accepted by the server.
105
+ * @deprecated
106
+ */
107
+ export interface MultiTurnStaleEstimatePayload {
108
+ sessionTurnCount: number;
109
+ staleBytes: number;
110
+ staleResults: number;
111
+ }
112
+
113
+ /**
114
+ * pre_compaction_nudge_emitted payload — fired once per session when cumulative
115
+ * context bytes approach the auto-compact threshold (~80K tokens).
116
+ */
117
+ export interface PreCompactionNudgePayload {
118
+ cumulativeBytes: number;
119
+ estimatedTokens: number;
120
+ pct: number;
121
+ }
122
+
123
+ /**
124
+ * hook_perf payload — one rollup per hook name per session, emitted at SessionEnd.
125
+ * p50_ms / p99_ms derived from hook-timings.jsonl. Added in v1.30 (1.2).
126
+ */
127
+ export interface HookPerfPayload {
128
+ hook_name: string;
129
+ p50_ms: number;
130
+ p99_ms: number;
131
+ count: number;
132
+ }
133
+
134
+ /**
135
+ * genome_compression_ratio payload — emitted on every ashlr__grep call that
136
+ * routes through the genome path. Added in v1.30 (1.3).
137
+ */
138
+ export interface GenomeCompressionRatioPayload {
139
+ tool: string;
140
+ raw_bytes: number;
141
+ compressed_bytes: number;
142
+ }
143
+
144
+ /**
145
+ * wizard_step payload — emitted by Track 3 (Wizard Repair) for each wizard step.
146
+ * Schema added here so Track 3 emit sites just work. Added in v1.30 (Track 3 coord).
147
+ */
148
+ export interface WizardStepPayload {
149
+ step_name: string;
150
+ outcome: "completed" | "skipped" | "error";
151
+ }
152
+
153
+ /** Union of all typed payloads. */
154
+ export type TelemetryPayload =
155
+ | ({ kind: "tool_call" } & ToolCallPayload)
156
+ | ({ kind: "pretooluse_block" } & PreToolUseBlockPayload)
157
+ | ({ kind: "pretooluse_passthrough" } & PreToolUsePassthroughPayload)
158
+ | ({ kind: "version" } & VersionPayload)
159
+ | ({ kind: "hook_perf" } & HookPerfPayload)
160
+ | ({ kind: "genome_compression_ratio" } & GenomeCompressionRatioPayload)
161
+ | ({ kind: "wizard_step" } & WizardStepPayload)
162
+ | ({ kind: "pre_compaction_nudge_emitted" } & PreCompactionNudgePayload);
163
+
164
+ /** A single JSONL record in the buffer. */
165
+ export interface TelemetryRecord {
166
+ ts: number;
167
+ kind: TelemetryEventKind;
168
+ sessionId: string;
169
+ [key: string]: unknown;
170
+ }
171
+
172
+ // ---------------------------------------------------------------------------
173
+ // Path helpers (resolved at call-time so tests overriding $HOME work)
174
+ // ---------------------------------------------------------------------------
175
+
176
+ function home(): string {
177
+ return process.env.HOME ?? homedir();
178
+ }
179
+
180
+ export function telemetryBufferPath(homeDir: string = home()): string {
181
+ return join(homeDir, ".ashlr", "telemetry-buffer.jsonl");
182
+ }
183
+
184
+ export function telemetrySessionPath(homeDir: string = home()): string {
185
+ return join(homeDir, ".ashlr", "telemetry-session.json");
186
+ }
187
+
188
+ export function ashlrConfigPath(homeDir: string = home()): string {
189
+ return join(homeDir, ".ashlr", "config.json");
190
+ }
191
+
192
+ // ---------------------------------------------------------------------------
193
+ // Config reading (shared with session-start.ts — local copy to avoid
194
+ // circular imports across the servers/ and hooks/ boundary)
195
+ // ---------------------------------------------------------------------------
196
+
197
+ function readConfig(homeDir: string = home()): Record<string, unknown> {
198
+ try {
199
+ const p = ashlrConfigPath(homeDir);
200
+ if (!fsExistsSync(p)) return {};
201
+ const raw = fsReadFileSync(p, "utf-8");
202
+ const parsed: unknown = JSON.parse(raw);
203
+ if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
204
+ return parsed as Record<string, unknown>;
205
+ }
206
+ } catch {
207
+ /* treat as empty */
208
+ }
209
+ return {};
210
+ }
211
+
212
+ // ---------------------------------------------------------------------------
213
+ // Opt-in check
214
+ // ---------------------------------------------------------------------------
215
+
216
+ /**
217
+ * Returns true only when the user has explicitly opted in via:
218
+ * ASHLR_TELEMETRY=on OR config.telemetry === "opt-in"
219
+ *
220
+ * Any other value (missing, "off", anything else) → false (opt-out / default).
221
+ */
222
+ export function isTelemetryEnabled(homeDir: string = home()): boolean {
223
+ // Env kill switch takes highest priority.
224
+ const envVal = (process.env.ASHLR_TELEMETRY ?? "").toLowerCase().trim();
225
+ if (envVal === "off" || envVal === "0") return false;
226
+ if (envVal === "on" || envVal === "1") {
227
+ // Env opt-in: verify config doesn't explicitly say "off".
228
+ const config = readConfig(homeDir);
229
+ if (config.telemetry === "off") return false;
230
+ return true;
231
+ }
232
+
233
+ // No env override: check config file.
234
+ const config = readConfig(homeDir);
235
+ return config.telemetry === "opt-in";
236
+ }
237
+
238
+ // ---------------------------------------------------------------------------
239
+ // Session ID
240
+ // ---------------------------------------------------------------------------
241
+
242
+ /**
243
+ * Returns the per-session hashed ID. Creates one if it doesn't exist.
244
+ * The session file is written on first call and deleted by the session-end hook.
245
+ *
246
+ * We use a UUID-like random string hashed via a simple XOR fold so the raw
247
+ * random seed never persists — only the folded hex is stored.
248
+ */
249
+ export function getOrCreateTelemetrySessionId(homeDir: string = home()): string {
250
+ const sessionPath = telemetrySessionPath(homeDir);
251
+ try {
252
+ if (fsExistsSync(sessionPath)) {
253
+ const raw = JSON.parse(fsReadFileSync(sessionPath, "utf-8")) as { id?: unknown };
254
+ if (typeof raw.id === "string" && raw.id.length > 0) return raw.id;
255
+ }
256
+ } catch {
257
+ /* fall through to create */
258
+ }
259
+
260
+ // Create a new session ID: random seed → simple hash → hex string.
261
+ const seed = `${Date.now()}:${Math.random().toString(36)}:${Math.random().toString(36)}`;
262
+ const id = hashString(seed);
263
+
264
+ try {
265
+ mkdirSync(dirname(sessionPath), { recursive: true });
266
+ writeFileSync(sessionPath, JSON.stringify({ id, createdAt: Date.now() }), "utf-8");
267
+ } catch {
268
+ /* best-effort */
269
+ }
270
+
271
+ return id;
272
+ }
273
+
274
+ /**
275
+ * Delete the session file on session end.
276
+ */
277
+ export function deleteTelemetrySession(homeDir: string = home()): void {
278
+ try {
279
+ const sessionPath = telemetrySessionPath(homeDir);
280
+ if (fsExistsSync(sessionPath)) {
281
+ // Use fs.unlinkSync — keep the import footprint minimal.
282
+ // eslint-disable-next-line @typescript-eslint/no-var-requires
283
+ const { unlinkSync } = require("fs") as typeof import("fs");
284
+ unlinkSync(sessionPath);
285
+ }
286
+ } catch {
287
+ /* best-effort */
288
+ }
289
+ }
290
+
291
+ // ---------------------------------------------------------------------------
292
+ // Simple string hash (non-crypto — for session-id generation only)
293
+ // ---------------------------------------------------------------------------
294
+
295
+ function hashString(s: string): string {
296
+ // djb2 XOR variant — good enough for a non-security opaque identifier.
297
+ let h1 = 5381;
298
+ let h2 = 52711;
299
+ for (let i = 0; i < s.length; i++) {
300
+ const c = s.charCodeAt(i);
301
+ h1 = ((h1 << 5) + h1) ^ c;
302
+ h2 = ((h2 << 5) + h2) ^ c;
303
+ }
304
+ return ((h1 >>> 0).toString(16).padStart(8, "0") +
305
+ (h2 >>> 0).toString(16).padStart(8, "0"));
306
+ }
307
+
308
+ // ---------------------------------------------------------------------------
309
+ // Buffer writer
310
+ // ---------------------------------------------------------------------------
311
+
312
+ /**
313
+ * Record one telemetry event to the local buffer.
314
+ *
315
+ * Called from event-emission sites (alongside logEvent) when telemetry is
316
+ * enabled. Must be cheap: synchronous append-only.
317
+ *
318
+ * Race safety: appendFileSync is atomic for writes < PIPE_BUF (~512B–4KB on
319
+ * POSIX, ~4KB on Windows). Our JSONL lines are ≤400B, well inside the
320
+ * threshold. Concurrent callers cannot interleave bytes.
321
+ *
322
+ * Eviction: when the buffer exceeds MAX_BUFFER_LINES * EVICTION_FACTOR, we
323
+ * do a one-shot atomic rewrite dropping oldest entries. This only runs on
324
+ * the write path that pushes us over the limit — typically rare.
325
+ */
326
+ export function recordTelemetryEvent(
327
+ payload: TelemetryPayload,
328
+ homeDir: string = home(),
329
+ ): void {
330
+ if (!isTelemetryEnabled(homeDir)) return;
331
+
332
+ try {
333
+ const bufPath = telemetryBufferPath(homeDir);
334
+ mkdirSync(dirname(bufPath), { recursive: true });
335
+
336
+ const sessionId = getOrCreateTelemetrySessionId(homeDir);
337
+
338
+ // Build the anonymized record. Destructure `kind` out; spread remaining.
339
+ const { kind, ...rest } = payload;
340
+
341
+ // Safety guard: reject any record that sneaks in a path-like string value.
342
+ // This is a regression guardrail — callers should never pass paths, but
343
+ // we double-check here as defense in depth.
344
+ for (const v of Object.values(rest)) {
345
+ if (typeof v === "string" && looksLikePath(v)) {
346
+ // Drop the event silently rather than risk exfiltrating a path.
347
+ return;
348
+ }
349
+ }
350
+
351
+ const record: TelemetryRecord = {
352
+ ts: Math.floor(Date.now() / 1000), // epoch seconds (not ms) to save bytes
353
+ kind,
354
+ sessionId,
355
+ ...rest,
356
+ };
357
+
358
+ const line = JSON.stringify(record) + "\n";
359
+ appendFileSync(bufPath, line, "utf-8");
360
+
361
+ // Lazy eviction: check line count and prune if needed.
362
+ // We do this AFTER the append so the new event isn't lost.
363
+ _maybeEvict(bufPath);
364
+ } catch {
365
+ // Never propagate — telemetry must never break tool functionality.
366
+ }
367
+ }
368
+
369
+ /**
370
+ * Convenience wrapper for posttooluse-stale-result — records a
371
+ * pre_compaction_nudge_emitted event when context is approaching auto-compact.
372
+ * No-op when telemetry is off.
373
+ */
374
+ export function logPreCompactionNudgeEvent(
375
+ payload: PreCompactionNudgePayload,
376
+ homeDir: string = home(),
377
+ ): void {
378
+ recordTelemetryEvent({ kind: "pre_compaction_nudge_emitted", ...payload }, homeDir);
379
+ }
380
+
381
+ /**
382
+ * Record a hook_perf rollup event. Called from the session-end hook after
383
+ * reading hook-timings.jsonl and computing per-hook percentiles.
384
+ * No-op when telemetry is off.
385
+ */
386
+ export function logHookPerfEvent(
387
+ payload: HookPerfPayload,
388
+ homeDir: string = home(),
389
+ ): void {
390
+ recordTelemetryEvent({ kind: "hook_perf", ...payload }, homeDir);
391
+ }
392
+
393
+ /**
394
+ * Record a genome_compression_ratio event. Called from ashlr__grep whenever
395
+ * the genome path returns content.
396
+ * No-op when telemetry is off.
397
+ */
398
+ export function logGenomeCompressionRatioEvent(
399
+ payload: GenomeCompressionRatioPayload,
400
+ homeDir: string = home(),
401
+ ): void {
402
+ recordTelemetryEvent({ kind: "genome_compression_ratio", ...payload }, homeDir);
403
+ }
404
+
405
+ /**
406
+ * Returns true if `s` looks like an absolute filesystem path.
407
+ * Used as a safety guard to prevent accidental path exfiltration.
408
+ */
409
+ export function looksLikePath(s: string): boolean {
410
+ if (s.length < 3) return false;
411
+ // POSIX absolute: starts with /
412
+ if (s.startsWith("/")) return true;
413
+ // Windows absolute: C:\ or \\server\share
414
+ if (/^[A-Za-z]:[/\\]/.test(s)) return true;
415
+ if (s.startsWith("\\\\")) return true;
416
+ return false;
417
+ }
418
+
419
+ /**
420
+ * Evict oldest entries when the buffer exceeds MAX_BUFFER_LINES * EVICTION_FACTOR.
421
+ * Uses atomic rename (write to .tmp, then rename) so concurrent readers see
422
+ * either the old or new file, never a partial write.
423
+ *
424
+ * Only called from recordTelemetryEvent — never on the hot read path.
425
+ */
426
+ function _maybeEvict(bufPath: string): void {
427
+ try {
428
+ if (!existsSync(bufPath)) return;
429
+ const raw = readFileSync(bufPath, "utf-8");
430
+ const lines = raw.split("\n").filter((l) => l.trim());
431
+ if (lines.length <= Math.floor(MAX_BUFFER_LINES * EVICTION_FACTOR)) return;
432
+
433
+ // Keep only the most recent MAX_BUFFER_LINES entries.
434
+ const pruned = lines.slice(-MAX_BUFFER_LINES);
435
+ _atomicRewrite(bufPath, pruned);
436
+ } catch {
437
+ /* best-effort */
438
+ }
439
+ }
440
+
441
+ function _atomicRewrite(path: string, lines: string[]): void {
442
+ const tmp = path + ".tmp";
443
+ const content = lines.length === 0 ? "" : lines.join("\n") + "\n";
444
+ writeFileSync(tmp, content, "utf-8");
445
+ renameSync(tmp, path);
446
+ }
447
+
448
+ // ---------------------------------------------------------------------------
449
+ // Consent notice stamp (testable without importing the full session-start hook)
450
+ // ---------------------------------------------------------------------------
451
+
452
+ /** Path to the one-time consent notice stamp. */
453
+ export function telemetryConsentStampPath(homeDir: string = home()): string {
454
+ return join(homeDir, ".ashlr", "telemetry-consent-shown");
455
+ }
456
+
457
+ /**
458
+ * Returns a one-liner consent notice the first time telemetry is enabled,
459
+ * then null on all subsequent calls (stamp-gated).
460
+ *
461
+ * Strictly opt-in: returns null immediately when telemetry is off.
462
+ */
463
+ export function maybeTelemetryConsentNotice(homeDir: string = home()): string | null {
464
+ if (!isTelemetryEnabled(homeDir)) return null;
465
+ const stampPath = telemetryConsentStampPath(homeDir);
466
+ try {
467
+ if (existsSync(stampPath)) return null;
468
+ mkdirSync(dirname(stampPath), { recursive: true });
469
+ writeFileSync(stampPath, new Date().toISOString(), "utf-8");
470
+ } catch {
471
+ /* best-effort — never break the hook */
472
+ }
473
+ return (
474
+ "[ashlr] Telemetry opt-in active. Collecting anonymized tool-shape metrics " +
475
+ "(no paths, no content). To disable: set ASHLR_TELEMETRY=off or " +
476
+ '~/.ashlr/config.json { "telemetry": "off" }. See docs/telemetry.md.\n'
477
+ );
478
+ }
479
+
480
+ // ---------------------------------------------------------------------------
481
+ // Buffer reader (used by the flusher)
482
+ // ---------------------------------------------------------------------------
483
+
484
+ /**
485
+ * Read all records from the buffer. Returns empty array on any error.
486
+ */
487
+ export function readTelemetryBuffer(homeDir: string = home()): TelemetryRecord[] {
488
+ try {
489
+ const bufPath = telemetryBufferPath(homeDir);
490
+ if (!existsSync(bufPath)) return [];
491
+ const raw = readFileSync(bufPath, "utf-8");
492
+ const records: TelemetryRecord[] = [];
493
+ for (const line of raw.split("\n")) {
494
+ if (!line.trim()) continue;
495
+ try {
496
+ records.push(JSON.parse(line) as TelemetryRecord);
497
+ } catch {
498
+ /* skip malformed lines */
499
+ }
500
+ }
501
+ return records;
502
+ } catch {
503
+ return [];
504
+ }
505
+ }
506
+
507
+ /**
508
+ * Truncate the buffer to only entries newer than `horizonTs` (epoch seconds).
509
+ * Called by the flusher after a successful POST.
510
+ */
511
+ export function truncateTelemetryBuffer(
512
+ horizonTs: number,
513
+ homeDir: string = home(),
514
+ ): void {
515
+ try {
516
+ const bufPath = telemetryBufferPath(homeDir);
517
+ const records = readTelemetryBuffer(homeDir);
518
+ const kept = records.filter((r) => r.ts > horizonTs);
519
+ _atomicRewrite(bufPath, kept.map((r) => JSON.stringify(r)));
520
+ } catch {
521
+ /* best-effort */
522
+ }
523
+ }
524
+
525
+ // ---------------------------------------------------------------------------
526
+ // WAD-D daily heartbeat (added for the weekly-active-developers metric)
527
+ // ---------------------------------------------------------------------------
528
+
529
+ /**
530
+ * Payload posted to /stats/daily-active. Only salted-hashes + UTC date +
531
+ * plugin version — no PII, no raw machine_id, no raw github login.
532
+ *
533
+ * The optional leadIndicators block ships per-client aggregate signals
534
+ * (booleans / counts / one timestamp) that the WAD-D server-side
535
+ * aggregator rolls into the daily lead-indicator JSON. Older clients
536
+ * omit the block entirely — the server treats missing fields as null,
537
+ * so older clients keep working with no schema break.
538
+ *
539
+ * What we DO NOT send: per-tool call counts, file paths, raw nudge events,
540
+ * streak history, any identifier other than the salted hashes above.
541
+ */
542
+ export interface DailyHeartbeatLeadIndicators {
543
+ /** True iff /ashlr-start onboarding wizard has completed at any point. */
544
+ onboarding_completed?: boolean;
545
+ /** True iff the ashlr status-line master switch is enabled. */
546
+ status_line_enabled?: boolean;
547
+ /** ISO timestamp of the first meaningful savings event; null when unset. */
548
+ first_savings_at?: string | null;
549
+ /** Current saving-streak length in days (capped at 0+, never negative). */
550
+ streak_days?: number;
551
+ /**
552
+ * /ashlr-savings slash-command invocations in the current ISO week.
553
+ * Reset each Monday UTC.
554
+ */
555
+ savings_invocations_this_week?: number;
556
+ /**
557
+ * Aggregate nudge acceptance ratio (clicked / shown) in [0, 1]; computed
558
+ * from the local nudge-events.jsonl. Null when no nudges have been shown.
559
+ */
560
+ nudge_accept_rate?: number | null;
561
+ }
562
+
563
+ export interface DailyHeartbeatPayload {
564
+ /** sha256(machine_id + quarterly_salt). 64 hex chars. */
565
+ machineHash: string;
566
+ /** sha256(github_login + quarterly_salt) when signed-in Pro/Team; else null. */
567
+ githubHash: string | null;
568
+ /** UTC date the heartbeat is for: "YYYY-MM-DD". */
569
+ date: string;
570
+ /** ashlr-plugin semver (from package.json). */
571
+ pluginVersion: string;
572
+ /**
573
+ * WAD-D lead-indicator block — optional for backward compat with older
574
+ * clients that don't ship the new fields. Server treats absence as
575
+ * "this client cannot report" (null) rather than "false / 0".
576
+ */
577
+ leadIndicators?: DailyHeartbeatLeadIndicators;
578
+ }
579
+
580
+ /**
581
+ * Optional override of the daily-heartbeat endpoint URL. Tests inject this
582
+ * so they don't hit the real api.ashlr.ai. Module-private — exported only
583
+ * via the test hook below.
584
+ */
585
+ let _heartbeatUrlOverride: string | null = null;
586
+ let _heartbeatFetchOverride: typeof fetch | null = null;
587
+ let _heartbeatLastEmittedDate: string | null = null;
588
+
589
+ /** Test hook: force the heartbeat to POST to a mock URL. */
590
+ export function _setDailyHeartbeatUrl(url: string | null): void {
591
+ _heartbeatUrlOverride = url;
592
+ }
593
+
594
+ /** Test hook: inject a custom fetch (e.g. to assert calls). */
595
+ export function _setDailyHeartbeatFetch(fn: typeof fetch | null): void {
596
+ _heartbeatFetchOverride = fn;
597
+ }
598
+
599
+ /** Test hook: reset the in-process last-emitted memo. */
600
+ export function _resetDailyHeartbeatMemo(): void {
601
+ _heartbeatLastEmittedDate = null;
602
+ }
603
+
604
+ /**
605
+ * Resolve the daily-active endpoint URL. Match the existing cloud-sync
606
+ * pattern (ASHLR_API_URL env override → https://api.ashlr.ai default), but
607
+ * routed to /stats/daily-active.
608
+ *
609
+ * The server-side handler is implemented in server/src/routes/daily-active.ts
610
+ * (POST /stats/daily-active, unauthenticated, idempotent upsert). Both client
611
+ * and server are live. The endpoint is intentionally fire-and-forget: network
612
+ * failures are swallowed silently and never surface to the user.
613
+ */
614
+ function dailyHeartbeatUrl(): string {
615
+ if (_heartbeatUrlOverride) return _heartbeatUrlOverride;
616
+ const base = process.env["ASHLR_API_URL"] ?? "https://api.ashlr.ai";
617
+ return `${base.replace(/\/+$/, "")}/stats/daily-active`;
618
+ }
619
+
620
+ /**
621
+ * Fire-and-forget POST of the daily heartbeat. Gated on telemetry consent.
622
+ *
623
+ * Guarantees:
624
+ * - Returns immediately (does NOT await the network call).
625
+ * - Silent on every error path — never breaks the recordSaving hot path.
626
+ * - No-op when telemetry consent is "off"/missing.
627
+ * - In-process idempotent for the (date, machineHash) tuple — a second
628
+ * call for the same UTC date from the same process is a no-op even if
629
+ * the stats.json daily_active gate ever races.
630
+ */
631
+ export function emitDailyHeartbeat(
632
+ payload: DailyHeartbeatPayload,
633
+ homeDir: string = home(),
634
+ ): void {
635
+ try {
636
+ if (!isTelemetryEnabled(homeDir)) return;
637
+ if (_heartbeatLastEmittedDate === payload.date) return;
638
+ _heartbeatLastEmittedDate = payload.date;
639
+
640
+ const url = dailyHeartbeatUrl();
641
+ const fetcher = _heartbeatFetchOverride ?? globalThis.fetch;
642
+
643
+ // Fire-and-forget — never await, never throw.
644
+ // The lead_indicators block is added at the top level so the server
645
+ // can rev the schema by just reading the optional fields — no zod
646
+ // refactor needed for older clients that omit it.
647
+ const body: Record<string, unknown> = {
648
+ identity_hash: payload.machineHash,
649
+ github_hash: payload.githubHash,
650
+ date: payload.date,
651
+ plugin_version: payload.pluginVersion,
652
+ };
653
+ if (payload.leadIndicators) {
654
+ body["lead_indicators"] = sanitizeLeadIndicators(payload.leadIndicators);
655
+ }
656
+ void Promise.resolve()
657
+ .then(() =>
658
+ fetcher(url, {
659
+ method: "POST",
660
+ headers: { "Content-Type": "application/json" },
661
+ body: JSON.stringify(body),
662
+ signal: AbortSignal.timeout(10_000),
663
+ }),
664
+ )
665
+ .catch(() => {
666
+ /* network failure — drop silently */
667
+ });
668
+ } catch {
669
+ /* never propagate */
670
+ }
671
+ }
672
+
673
+
674
+ /**
675
+ * Coerce lead-indicator fields to safe JSON-serializable values. Drops any
676
+ * non-boolean/non-number/non-string-iso fields rather than shipping
677
+ * garbage. Returns a fresh object (caller's input is not mutated).
678
+ *
679
+ * Privacy: this is the LAST line of defense before bytes leave the box.
680
+ * We intentionally do NOT pass arbitrary keys through — only the
681
+ * documented set lands in the payload.
682
+ */
683
+ export function sanitizeLeadIndicators(
684
+ raw: DailyHeartbeatLeadIndicators,
685
+ ): Record<string, unknown> {
686
+ const out: Record<string, unknown> = {};
687
+ if (typeof raw.onboarding_completed === "boolean") {
688
+ out["onboarding_completed"] = raw.onboarding_completed;
689
+ }
690
+ if (typeof raw.status_line_enabled === "boolean") {
691
+ out["status_line_enabled"] = raw.status_line_enabled;
692
+ }
693
+ if (raw.first_savings_at === null) {
694
+ out["first_savings_at"] = null;
695
+ } else if (typeof raw.first_savings_at === "string" && raw.first_savings_at.length > 0) {
696
+ out["first_savings_at"] = raw.first_savings_at;
697
+ }
698
+ if (typeof raw.streak_days === "number" && Number.isFinite(raw.streak_days) && raw.streak_days >= 0) {
699
+ out["streak_days"] = Math.floor(raw.streak_days);
700
+ }
701
+ if (
702
+ typeof raw.savings_invocations_this_week === "number" &&
703
+ Number.isFinite(raw.savings_invocations_this_week) &&
704
+ raw.savings_invocations_this_week >= 0
705
+ ) {
706
+ out["savings_invocations_this_week"] = Math.floor(raw.savings_invocations_this_week);
707
+ }
708
+ if (raw.nudge_accept_rate === null) {
709
+ out["nudge_accept_rate"] = null;
710
+ } else if (
711
+ typeof raw.nudge_accept_rate === "number" &&
712
+ Number.isFinite(raw.nudge_accept_rate) &&
713
+ raw.nudge_accept_rate >= 0 &&
714
+ raw.nudge_accept_rate <= 1
715
+ ) {
716
+ out["nudge_accept_rate"] = raw.nudge_accept_rate;
717
+ }
718
+ return out;
719
+ }