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,151 @@
1
+ /**
2
+ * Shared HTTP helpers used by both ashlr-http and ashlr-webfetch servers.
3
+ * Extracted here so webfetch-server.ts can import without triggering
4
+ * http-server's top-level `await server.connect(transport)`.
5
+ */
6
+
7
+ // ---------- safety ----------
8
+
9
+ export function isPrivateHost(host: string): boolean {
10
+ if (process.env.ASHLR_HTTP_ALLOW_PRIVATE === "1") return false;
11
+ const h = host.toLowerCase();
12
+ if (h === "localhost" || h === "::1" || h.endsWith(".localhost")) return true;
13
+ const v4 = /^(\d+)\.(\d+)\.(\d+)\.(\d+)$/.exec(h);
14
+ if (v4) {
15
+ const [a, b] = [parseInt(v4[1]!, 10), parseInt(v4[2]!, 10)];
16
+ if (a === 10) return true;
17
+ if (a === 127) return true;
18
+ if (a === 192 && b === 168) return true;
19
+ if (a === 172 && b >= 16 && b <= 31) return true;
20
+ if (a === 169 && b === 254) return true; // link-local (metadata endpoint)
21
+ if (a === 0) return true; // RFC 1122 "this network"
22
+ if (a >= 224) return true; // multicast + reserved
23
+ }
24
+ return false;
25
+ }
26
+
27
+ // ---------- safe fetch with manual redirect validation ----------
28
+
29
+ export interface SafeFetchOptions extends RequestInit {
30
+ /** Max redirect hops. Default 5. */
31
+ maxRedirects?: number;
32
+ /** AbortSignal to cancel the whole chain (not just one hop). */
33
+ signal?: AbortSignal;
34
+ }
35
+
36
+ /**
37
+ * fetch() with SSRF-safe manual redirect handling.
38
+ *
39
+ * The native `fetch(url, { redirect: "follow" })` silently follows 3xx
40
+ * redirects without re-checking the target — an attacker can hand us a
41
+ * public URL that redirects to 127.0.0.1 or 169.254.169.254 (cloud
42
+ * metadata) and bypass `isPrivateHost` entirely. This wrapper validates
43
+ * every hop: if any redirect Location points at a private host (and
44
+ * `ASHLR_HTTP_ALLOW_PRIVATE` isn't set), it throws before any bytes are
45
+ * read from the internal target.
46
+ *
47
+ * Callers should use this in place of fetch() anywhere user-supplied URLs
48
+ * are involved.
49
+ */
50
+ export async function safeFetch(
51
+ url: string,
52
+ opts: SafeFetchOptions = {},
53
+ ): Promise<Response> {
54
+ const maxRedirects = opts.maxRedirects ?? 5;
55
+ let currentUrl = url;
56
+ const { maxRedirects: _drop, ...fetchOpts } = opts;
57
+
58
+ for (let hop = 0; hop <= maxRedirects; hop++) {
59
+ let parsed: URL;
60
+ try { parsed = new URL(currentUrl); } catch { throw new Error(`invalid URL: ${currentUrl}`); }
61
+ if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
62
+ throw new Error(`unsupported scheme: ${parsed.protocol} (http/https only)`);
63
+ }
64
+ if (isPrivateHost(parsed.hostname)) {
65
+ throw new Error(
66
+ hop === 0
67
+ ? `refusing private host ${parsed.hostname}; set ASHLR_HTTP_ALLOW_PRIVATE=1 to override`
68
+ : `refusing redirect to private host ${parsed.hostname} (hop ${hop}); set ASHLR_HTTP_ALLOW_PRIVATE=1 to override`,
69
+ );
70
+ }
71
+
72
+ const res = await fetch(currentUrl, { ...fetchOpts, redirect: "manual" });
73
+ // 2xx/4xx/5xx → return directly, caller handles. 3xx → follow if we have budget.
74
+ if (res.status < 300 || res.status >= 400) return res;
75
+ const location = res.headers.get("location");
76
+ if (!location) return res; // malformed 3xx, return as-is
77
+ // Resolve relative redirects against the current URL.
78
+ currentUrl = new URL(location, currentUrl).toString();
79
+ }
80
+ throw new Error(`too many redirects (> ${maxRedirects})`);
81
+ }
82
+
83
+ // ---------- HTML compressor ----------
84
+
85
+ export function compressHtml(raw: string): string {
86
+ let s = raw;
87
+ s = s.replace(/<script[\s\S]*?<\/script>/gi, "");
88
+ s = s.replace(/<style[\s\S]*?<\/style>/gi, "");
89
+ s = s.replace(/<noscript[\s\S]*?<\/noscript>/gi, "");
90
+ s = s.replace(/<svg[\s\S]*?<\/svg>/gi, "");
91
+ s = s.replace(/<(nav|footer|aside|header|form)[^>]*>[\s\S]*?<\/\1>/gi, "");
92
+ const mainMatch = s.match(/<(main|article)\b[^>]*>([\s\S]*?)<\/\1>/i);
93
+ if (mainMatch) s = mainMatch[2]!;
94
+ s = s.replace(/<h([1-6])[^>]*>([\s\S]*?)<\/h\1>/gi, (_, lvl, t) => "\n" + "#".repeat(+lvl) + " " + stripTags(t) + "\n");
95
+ s = s.replace(/<li[^>]*>([\s\S]*?)<\/li>/gi, (_, t) => "• " + stripTags(t) + "\n");
96
+ s = s.replace(/<a [^>]*href="([^"]+)"[^>]*>([\s\S]*?)<\/a>/gi, (_, href, t) => stripTags(t) + ` (${href})`);
97
+ s = s.replace(/<pre[^>]*>([\s\S]*?)<\/pre>/gi, (_, t) => "\n```\n" + decodeEntities(stripTags(t)) + "\n```\n");
98
+ s = s.replace(/<code[^>]*>([\s\S]*?)<\/code>/gi, (_, t) => "`" + stripTags(t) + "`");
99
+ s = s.replace(/<br\s*\/?>/gi, "\n");
100
+ s = s.replace(/<p[^>]*>/gi, "\n");
101
+ s = s.replace(/<\/p>/gi, "\n");
102
+ s = stripTags(s);
103
+ s = decodeEntities(s);
104
+ s = s.replace(/\n{3,}/g, "\n\n").trim();
105
+ return s;
106
+ }
107
+
108
+ function stripTags(s: string): string {
109
+ return s.replace(/<[^>]+>/g, "");
110
+ }
111
+
112
+ function decodeEntities(s: string): string {
113
+ return s
114
+ .replace(/&nbsp;/g, " ").replace(/&amp;/g, "&").replace(/&lt;/g, "<")
115
+ .replace(/&gt;/g, ">").replace(/&quot;/g, '"').replace(/&#39;/g, "'")
116
+ .replace(/&#(\d+);/g, (_, n) => String.fromCodePoint(+n));
117
+ }
118
+
119
+ // ---------- JSON compressor ----------
120
+
121
+ export function compressJson(raw: string): string {
122
+ try {
123
+ const obj = JSON.parse(raw);
124
+ return JSON.stringify(elideLongArrays(obj), null, 2);
125
+ } catch {
126
+ return raw;
127
+ }
128
+ }
129
+
130
+ function elideLongArrays(v: any): any {
131
+ if (Array.isArray(v)) {
132
+ if (v.length > 20) {
133
+ const head = v.slice(0, 10).map(elideLongArrays);
134
+ const tail = v.slice(-5).map(elideLongArrays);
135
+ return [...head, `[... ${v.length - 15} elided ...]`, ...tail];
136
+ }
137
+ return v.map(elideLongArrays);
138
+ }
139
+ if (v && typeof v === "object") {
140
+ const o: any = {};
141
+ for (const [k, val] of Object.entries(v)) {
142
+ if (typeof val === "string" && val.length > 300) {
143
+ o[k] = val.slice(0, 280) + "…";
144
+ } else {
145
+ o[k] = elideLongArrays(val);
146
+ }
147
+ }
148
+ return o;
149
+ }
150
+ return v;
151
+ }
@@ -0,0 +1,151 @@
1
+ /**
2
+ * _identity-hash.ts — WAD-D identity hashing.
3
+ *
4
+ * The WAD-D (weekly-active-developers-using-it-daily) north-star metric needs
5
+ * a way to count *distinct developers* on the backend without ever learning
6
+ * who any of them actually are. This module provides that anonymous identity.
7
+ *
8
+ * Identity scheme:
9
+ * - machineHash = sha256(machine_id + quarterly_salt)
10
+ * * machine_id is a stable random UUID stored at ~/.ashlr/machine-id,
11
+ * generated on first call. Never overwritten. No PII.
12
+ * * quarterly_salt = "salt_${year}Q${quarter}" derived deterministically
13
+ * from the current UTC date. Rotates every 3 months without any manual
14
+ * ops or remote config — the same client running 3 months later
15
+ * computes a *different* hash for the same machine_id. The backend
16
+ * therefore cannot reconstruct cross-quarter user identity.
17
+ *
18
+ * - githubHash = sha256(github_login + quarterly_salt) IFF a Pro/Team
19
+ * token has been validated AND the cached UserMe payload includes a
20
+ * `githubLogin`. Otherwise `null` — never inferred, never guessed.
21
+ *
22
+ * Both hashes are hex-encoded sha256 (lowercase, 64 chars each). Identical
23
+ * across calls within the same quarter; different across quarters.
24
+ *
25
+ * No new dependencies — uses Node's built-in `crypto.createHash`.
26
+ */
27
+
28
+ import { createHash, randomUUID } from "crypto";
29
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "fs";
30
+ import { homedir } from "os";
31
+ import { dirname, join } from "path";
32
+
33
+ // ---------------------------------------------------------------------------
34
+ // Paths
35
+ // ---------------------------------------------------------------------------
36
+
37
+ function home(): string {
38
+ return process.env.HOME ?? homedir();
39
+ }
40
+
41
+ export function machineIdPath(homeDir: string = home()): string {
42
+ return join(homeDir, ".ashlr", "machine-id");
43
+ }
44
+
45
+ function proTokenCachePath(homeDir: string = home()): string {
46
+ return join(homeDir, ".ashlr", "pro-token-cache.json");
47
+ }
48
+
49
+ // ---------------------------------------------------------------------------
50
+ // machine_id — read or generate
51
+ // ---------------------------------------------------------------------------
52
+
53
+ /**
54
+ * Read the stable machine id from disk, generating one on first call.
55
+ * Always returns a non-empty string. Never throws.
56
+ */
57
+ export function getOrCreateMachineId(homeDir: string = home()): string {
58
+ const p = machineIdPath(homeDir);
59
+ try {
60
+ if (existsSync(p)) {
61
+ const raw = readFileSync(p, "utf-8").trim();
62
+ if (raw.length > 0) return raw;
63
+ }
64
+ } catch {
65
+ /* fall through to create */
66
+ }
67
+ const id = randomUUID();
68
+ try {
69
+ mkdirSync(dirname(p), { recursive: true });
70
+ writeFileSync(p, id, "utf-8");
71
+ } catch {
72
+ /* best-effort — in-memory id still returned */
73
+ }
74
+ return id;
75
+ }
76
+
77
+ // ---------------------------------------------------------------------------
78
+ // Quarterly salt — deterministic from current UTC year + quarter
79
+ // ---------------------------------------------------------------------------
80
+
81
+ /**
82
+ * Derive the quarterly salt for a given Date (defaults to now, UTC).
83
+ * Format: `salt_${year}Q${quarter}` where quarter ∈ {1, 2, 3, 4}.
84
+ *
85
+ * Exported so tests can mock the date input deterministically.
86
+ */
87
+ export function quarterlySalt(now: Date = new Date()): string {
88
+ const year = now.getUTCFullYear();
89
+ const monthIndex = now.getUTCMonth(); // 0–11
90
+ const quarter = Math.floor(monthIndex / 3) + 1; // 1–4
91
+ return `salt_${year}Q${quarter}`;
92
+ }
93
+
94
+ // ---------------------------------------------------------------------------
95
+ // githubLogin — try to read from the pro-token cache (best-effort)
96
+ // ---------------------------------------------------------------------------
97
+
98
+ /**
99
+ * Returns the github_login of the currently signed-in Pro/Team user, or null
100
+ * when not signed in or when the cache doesn't include the field.
101
+ *
102
+ * This is intentionally tolerant: we never want a malformed cache or a
103
+ * partial UserMe response to throw on the hot path.
104
+ */
105
+ export function readGithubLoginFromCache(homeDir: string = home()): string | null {
106
+ try {
107
+ const p = proTokenCachePath(homeDir);
108
+ if (!existsSync(p)) return null;
109
+ const raw = readFileSync(p, "utf-8");
110
+ const parsed = JSON.parse(raw) as { githubLogin?: unknown; valid?: unknown };
111
+ if (parsed.valid !== true) return null;
112
+ if (typeof parsed.githubLogin !== "string") return null;
113
+ const login = parsed.githubLogin.trim();
114
+ return login.length > 0 ? login : null;
115
+ } catch {
116
+ return null;
117
+ }
118
+ }
119
+
120
+ // ---------------------------------------------------------------------------
121
+ // Public API
122
+ // ---------------------------------------------------------------------------
123
+
124
+ export interface IdentityHash {
125
+ /** sha256(machine_id + quarterly_salt) — hex, lowercase, 64 chars. */
126
+ machineHash: string;
127
+ /** sha256(github_login + quarterly_salt) when signed-in Pro/Team; else null. */
128
+ githubHash: string | null;
129
+ }
130
+
131
+ /**
132
+ * Compute the WAD-D identity hash pair. Defaults to "now" in UTC for the
133
+ * quarterly salt; tests can pass an explicit `now` to verify quarter rotation.
134
+ */
135
+ export function getIdentityHash(opts: { now?: Date; homeDir?: string } = {}): IdentityHash {
136
+ const homeDir = opts.homeDir ?? home();
137
+ const now = opts.now ?? new Date();
138
+ const salt = quarterlySalt(now);
139
+
140
+ const machineId = getOrCreateMachineId(homeDir);
141
+ const machineHash = sha256Hex(machineId + salt);
142
+
143
+ const login = readGithubLoginFromCache(homeDir);
144
+ const githubHash = login ? sha256Hex(login + salt) : null;
145
+
146
+ return { machineHash, githubHash };
147
+ }
148
+
149
+ function sha256Hex(input: string): string {
150
+ return createHash("sha256").update(input).digest("hex");
151
+ }
@@ -0,0 +1,132 @@
1
+ /**
2
+ * Anthropic Haiku 4.5 provider.
3
+ *
4
+ * Availability: checks ANTHROPIC_API_KEY env var, then falls back to reading
5
+ * ~/.claude/.credentials.json (where Claude Code stores its OAuth token).
6
+ *
7
+ * On availability: returns true + caches.
8
+ * On network failure: throws so the dispatcher can fall through to next provider.
9
+ */
10
+
11
+ import { readFile } from "fs/promises";
12
+ import { homedir } from "os";
13
+ import { join } from "path";
14
+ import type { LlmProvider, LlmSummarizeResult } from "./types.ts";
15
+
16
+ const MODEL = "claude-haiku-4-5-20251001";
17
+ const API_BASE = "https://api.anthropic.com";
18
+ const ANTHROPIC_VERSION = "2023-06-01";
19
+
20
+ function home(): string {
21
+ return process.env.HOME ?? homedir();
22
+ }
23
+
24
+ /**
25
+ * Try to read the Anthropic API key from Claude Code's credential store.
26
+ * Claude Code writes OAuth tokens to ~/.claude/.credentials.json under the
27
+ * `claudeAiOauth` key. We extract `accessToken` from that entry.
28
+ *
29
+ * Returns null if the file doesn't exist or doesn't contain an API key shape.
30
+ */
31
+ async function readClaudeCredentials(): Promise<string | null> {
32
+ try {
33
+ const path = join(home(), ".claude", ".credentials.json");
34
+ const raw = await readFile(path, "utf-8");
35
+ const parsed = JSON.parse(raw) as Record<string, unknown>;
36
+ // Claude Code OAuth shape: { claudeAiOauth: { accessToken: "..." } }
37
+ const oauth = parsed["claudeAiOauth"];
38
+ if (oauth && typeof oauth === "object") {
39
+ const token = (oauth as Record<string, unknown>)["accessToken"];
40
+ if (typeof token === "string" && token.length > 0) return token;
41
+ }
42
+ return null;
43
+ } catch {
44
+ return null;
45
+ }
46
+ }
47
+
48
+ /** Resolve API key: env var takes priority over credential file. */
49
+ async function resolveApiKey(): Promise<string | null> {
50
+ const env = process.env.ANTHROPIC_API_KEY;
51
+ if (env && env.trim().length > 0) return env.trim();
52
+ return readClaudeCredentials();
53
+ }
54
+
55
+ // Cache availability check for the process lifetime (key presence is stable).
56
+ let _availableCache: boolean | null = null;
57
+
58
+ export const anthropicProvider: LlmProvider = {
59
+ name: "anthropic",
60
+
61
+ async isAvailable(): Promise<boolean> {
62
+ if (_availableCache !== null) return _availableCache;
63
+ const key = await resolveApiKey();
64
+ _availableCache = key !== null && key.length > 0;
65
+ return _availableCache;
66
+ },
67
+
68
+ async summarize(
69
+ text: string,
70
+ prompt: string,
71
+ opts?: { maxTokens?: number },
72
+ ): Promise<LlmSummarizeResult> {
73
+ const apiKey = await resolveApiKey();
74
+ if (!apiKey) throw new Error("Anthropic: no API key available");
75
+
76
+ const maxTokens = opts?.maxTokens ?? 800;
77
+ const t0 = Date.now();
78
+
79
+ const ctl = new AbortController();
80
+ const timer = setTimeout(() => ctl.abort(), 15_000);
81
+
82
+ let res: Response;
83
+ try {
84
+ res = await fetch(`${API_BASE}/v1/messages`, {
85
+ method: "POST",
86
+ signal: ctl.signal,
87
+ headers: {
88
+ "Content-Type": "application/json",
89
+ "x-api-key": apiKey,
90
+ "anthropic-version": ANTHROPIC_VERSION,
91
+ },
92
+ body: JSON.stringify({
93
+ model: MODEL,
94
+ max_tokens: maxTokens,
95
+ system: prompt,
96
+ messages: [{ role: "user", content: text }],
97
+ }),
98
+ });
99
+ } finally {
100
+ clearTimeout(timer);
101
+ }
102
+
103
+ if (!res.ok) {
104
+ const body = await res.text().catch(() => "");
105
+ throw new Error(`Anthropic API error ${res.status}: ${body.slice(0, 200)}`);
106
+ }
107
+
108
+ const latencyMs = Date.now() - t0;
109
+ const j = (await res.json()) as {
110
+ content?: Array<{ type: string; text?: string }>;
111
+ usage?: { input_tokens?: number; output_tokens?: number };
112
+ };
113
+
114
+ const output = (j.content ?? [])
115
+ .filter((b) => b.type === "text")
116
+ .map((b) => b.text ?? "")
117
+ .join("")
118
+ .trim();
119
+
120
+ if (!output) throw new Error("Anthropic: empty response");
121
+
122
+ const inTokens = j.usage?.input_tokens ?? 0;
123
+ const outTokens = j.usage?.output_tokens ?? 0;
124
+
125
+ return { output, inTokens, outTokens, latencyMs };
126
+ },
127
+ };
128
+
129
+ /** Test hook: reset the availability cache (so tests can toggle ANTHROPIC_API_KEY). */
130
+ export function _resetAnthropicAvailabilityCache(): void {
131
+ _availableCache = null;
132
+ }
@@ -0,0 +1,195 @@
1
+ /**
2
+ * Cloud LLM provider — ashlr hosted Anthropic Haiku proxy.
3
+ *
4
+ * For Pro users who don't have their own Anthropic API key. The backend at
5
+ * POST /llm/summarize handles billing, rate limiting, and cost caps so the
6
+ * user pays a flat $12/mo instead of managing Anthropic credits.
7
+ *
8
+ * Endpoint: ASHLR_API_URL (default: https://api.ashlr.ai) + /llm/summarize
9
+ * Auth: Authorization: Bearer <contents of ~/.ashlr/pro-token>
10
+ * Timeout: 5s (Pro users expect fast responses)
11
+ *
12
+ * Error handling:
13
+ * 429 (rate-limit OR daily cap) → throws CloudRateLimitError (dispatcher falls through)
14
+ * 5xx → throws generic Error (dispatcher falls through)
15
+ * network / timeout → throws generic Error (dispatcher falls through)
16
+ */
17
+
18
+ import { existsSync } from "fs";
19
+ import { readFile, stat } from "fs/promises";
20
+ import { homedir } from "os";
21
+ import { join } from "path";
22
+ import type { LlmProvider, LlmSummarizeResult } from "./types.ts";
23
+
24
+ // ---------------------------------------------------------------------------
25
+ // Constants
26
+ // ---------------------------------------------------------------------------
27
+
28
+ const SUMMARIZE_TIMEOUT_MS = 5_000;
29
+ /** Cache isAvailable() result for 24h — token presence is stable within a session. */
30
+ const AVAIL_CACHE_TTL_MS = 24 * 60 * 60 * 1_000;
31
+
32
+ // ---------------------------------------------------------------------------
33
+ // Typed error for rate-limit / daily-cap responses
34
+ // ---------------------------------------------------------------------------
35
+
36
+ export class CloudRateLimitError extends Error {
37
+ /** Value of the Retry-After header when present (seconds), else undefined. */
38
+ readonly retryAfterSecs: number | undefined;
39
+ /** HTTP status returned by the backend (always 429). */
40
+ readonly statusCode: number;
41
+
42
+ constructor(message: string, statusCode: number, retryAfterSecs?: number) {
43
+ super(message);
44
+ this.name = "CloudRateLimitError";
45
+ this.statusCode = statusCode;
46
+ this.retryAfterSecs = retryAfterSecs;
47
+ }
48
+ }
49
+
50
+ // ---------------------------------------------------------------------------
51
+ // Helpers
52
+ // ---------------------------------------------------------------------------
53
+
54
+ function home(): string {
55
+ return process.env.HOME ?? homedir();
56
+ }
57
+
58
+ function proTokenPath(): string {
59
+ return join(home(), ".ashlr", "pro-token");
60
+ }
61
+
62
+ function apiBase(): string {
63
+ return (process.env.ASHLR_API_URL ?? "https://api.ashlr.ai").replace(/\/$/, "");
64
+ }
65
+
66
+ async function readProToken(): Promise<string | null> {
67
+ const path = proTokenPath();
68
+ if (!existsSync(path)) return null;
69
+ try {
70
+ const token = (await readFile(path, "utf-8")).trim();
71
+ return token.length > 0 ? token : null;
72
+ } catch {
73
+ return null;
74
+ }
75
+ }
76
+
77
+ // ---------------------------------------------------------------------------
78
+ // Availability cache
79
+ // ---------------------------------------------------------------------------
80
+
81
+ let _availCache: { available: boolean; expiresAt: number } | null = null;
82
+
83
+ /** Reset availability cache (test hook). */
84
+ export function _resetCloudAvailabilityCache(): void {
85
+ _availCache = null;
86
+ }
87
+
88
+ // ---------------------------------------------------------------------------
89
+ // Provider
90
+ // ---------------------------------------------------------------------------
91
+
92
+ export const cloudProvider: LlmProvider = {
93
+ name: "cloud",
94
+
95
+ async isAvailable(): Promise<boolean> {
96
+ const now = Date.now();
97
+ if (_availCache && now < _availCache.expiresAt) return _availCache.available;
98
+
99
+ const path = proTokenPath();
100
+ let available = false;
101
+
102
+ if (existsSync(path)) {
103
+ try {
104
+ // Lightweight check: file exists AND was validated within last 24h
105
+ // (Track P1 will upgrade this to full JWT validation when it lands).
106
+ const s = await stat(path);
107
+ const token = (await readFile(path, "utf-8")).trim();
108
+ available = token.length > 0 && now - s.mtimeMs < AVAIL_CACHE_TTL_MS;
109
+ } catch {
110
+ available = false;
111
+ }
112
+ }
113
+
114
+ _availCache = { available, expiresAt: now + AVAIL_CACHE_TTL_MS };
115
+ return available;
116
+ },
117
+
118
+ async summarize(
119
+ text: string,
120
+ prompt: string,
121
+ opts?: { maxTokens?: number },
122
+ ): Promise<LlmSummarizeResult> {
123
+ const token = await readProToken();
124
+ if (!token) throw new Error("cloud provider: no pro-token found");
125
+
126
+ const url = `${apiBase()}/llm/summarize`;
127
+ const maxTokens = opts?.maxTokens;
128
+ const t0 = Date.now();
129
+
130
+ const ctl = new AbortController();
131
+ const timer = setTimeout(() => ctl.abort(), SUMMARIZE_TIMEOUT_MS);
132
+
133
+ let res: Response;
134
+ try {
135
+ res = await fetch(url, {
136
+ method: "POST",
137
+ signal: ctl.signal,
138
+ headers: {
139
+ "Content-Type": "application/json",
140
+ Authorization: `Bearer ${token}`,
141
+ },
142
+ body: JSON.stringify({
143
+ text,
144
+ systemPrompt: prompt,
145
+ toolName: "ashlr__cloud",
146
+ ...(maxTokens !== undefined ? { maxTokens } : {}),
147
+ }),
148
+ });
149
+ } finally {
150
+ clearTimeout(timer);
151
+ }
152
+
153
+ // 429 — rate limit (per-minute bucket) OR daily cap exhausted
154
+ if (res.status === 429) {
155
+ const body = await res.text().catch(() => "");
156
+ const retryHeader = res.headers.get("retry-after");
157
+ const retryAfterSecs = retryHeader ? parseInt(retryHeader, 10) : undefined;
158
+ throw new CloudRateLimitError(
159
+ `Cloud LLM rate limit: ${body.slice(0, 200)}`,
160
+ 429,
161
+ Number.isFinite(retryAfterSecs) ? retryAfterSecs : undefined,
162
+ );
163
+ }
164
+
165
+ // 5xx — backend error; let dispatcher fall through to onnx/local/snipCompact
166
+ if (res.status >= 500) {
167
+ const body = await res.text().catch(() => "");
168
+ throw new Error(`Cloud LLM error ${res.status}: ${body.slice(0, 200)}`);
169
+ }
170
+
171
+ if (!res.ok) {
172
+ const body = await res.text().catch(() => "");
173
+ throw new Error(`Cloud LLM error ${res.status}: ${body.slice(0, 200)}`);
174
+ }
175
+
176
+ const latencyMs = Date.now() - t0;
177
+
178
+ const j = (await res.json()) as {
179
+ summary?: string;
180
+ inputTokens?: number;
181
+ outputTokens?: number;
182
+ };
183
+
184
+ if (!j.summary || j.summary.trim().length === 0) {
185
+ throw new Error("Cloud LLM: empty summary in response");
186
+ }
187
+
188
+ return {
189
+ output: j.summary.trim(),
190
+ inTokens: j.inputTokens ?? 0,
191
+ outTokens: j.outputTokens ?? 0,
192
+ latencyMs,
193
+ };
194
+ },
195
+ };