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,751 @@
1
+ /**
2
+ * ashlr-search-replace-regex — v1.20 multi-file regex search/replace.
3
+ *
4
+ * Companion to `ashlr__edit` (literal-only, single file) and
5
+ * `ashlr__rename_file` (file rename + importer update). Users frequently
6
+ * need multi-file regex-based search/replace — e.g. "change every
7
+ * `logger.info(` to `log.info(` across src/". Before this tool they had
8
+ * to fall back to native shell + sed / native MultiEdit spanning many
9
+ * files; this lands the pattern inside the ashlr router so savings are
10
+ * credited and safety rails (cwd-clamp, binary refusal, caps) apply.
11
+ *
12
+ * Scope rules:
13
+ * - `roots` default to the primary project root and are cwd-clamped.
14
+ * Paths that escape cwd → refusal.
15
+ * - Candidate discovery uses ripgrep (`rg -l`) with the compiled regex.
16
+ * `include` / `exclude` globs are forwarded as `--glob` / `--glob !`
17
+ * — same syntax as `ashlr__edit_structural`.
18
+ * - Candidates are capped at `maxFiles` (default 200). Overflow is
19
+ * reported as a non-fatal warning, not a refusal.
20
+ * - Per-file replacements are capped at `maxMatchesPerFile` (default
21
+ * 100). Extras are left untouched and flagged.
22
+ * - Binary files are skipped by extension AND by null-byte sniff on
23
+ * the first 512 bytes (defense in depth against mislabeled assets).
24
+ *
25
+ * Safety:
26
+ * - Zero-width / empty-match patterns (e.g. `.*` on an empty string)
27
+ * are refused at validate time. The tool requires that every match
28
+ * consume at least one character, so a runaway replaceAll can't
29
+ * produce an infinite-length file.
30
+ * - Writes are atomic per-file (temp file + rename) so a crash
31
+ * mid-write can't corrupt partial content — the reader either sees
32
+ * the old file or the new file, never a half-written one.
33
+ * - dryRun: true returns the planned change set (file list + counts)
34
+ * without touching disk.
35
+ *
36
+ * No `--force` mode for v1. The regex + include/exclude + caps are
37
+ * expressive enough to scope any reasonable rewrite; if the model wants
38
+ * a truly global rewrite it can set `roots=["."]` explicitly and own
39
+ * the blast radius.
40
+ */
41
+
42
+ import { readFile, rename, writeFile, stat } from "fs/promises";
43
+ import { accessSync, realpathSync, statSync } from "fs";
44
+ import { spawnSync } from "child_process";
45
+ import { extname, normalize as pathNormalize, relative, resolve as pathResolve, sep } from "path";
46
+ import { minimatch } from "minimatch";
47
+ import { clampToCwd, primaryProjectRoot } from "./_cwd-clamp";
48
+
49
+ // ---------------------------------------------------------------------------
50
+ // Types
51
+ // ---------------------------------------------------------------------------
52
+
53
+ export interface SearchReplaceRegexArgs {
54
+ pattern: string;
55
+ replacement: string;
56
+ flags?: string;
57
+ include?: string[];
58
+ exclude?: string[];
59
+ dryRun?: boolean;
60
+ maxFiles?: number;
61
+ maxMatchesPerFile?: number;
62
+ roots?: string[];
63
+ }
64
+
65
+ export interface FilePlan {
66
+ /** Absolute path of the file. */
67
+ path: string;
68
+ /** Number of matches that will be replaced in this file. */
69
+ matches: number;
70
+ /** True if matches were capped by `maxMatchesPerFile`. */
71
+ capped: boolean;
72
+ /** Byte size of the original file. */
73
+ originalBytes: number;
74
+ /** Byte size of the rewritten file (post-replacement). */
75
+ newBytes: number;
76
+ }
77
+
78
+ export interface SearchReplaceRegexPlan {
79
+ /** Per-file planned changes. Only files with ≥1 match are included. */
80
+ files: FilePlan[];
81
+ /** Candidate files rg surfaced but that were skipped (binary, read error, etc.). */
82
+ skipped: number;
83
+ /** Non-fatal warnings (maxFiles truncation, per-file cap hit, binary skip, …). */
84
+ warnings: string[];
85
+ }
86
+
87
+ export type SearchReplaceRegexResult =
88
+ | { ok: true; plan: SearchReplaceRegexPlan }
89
+ | { ok: false; reason: string };
90
+
91
+ // ---------------------------------------------------------------------------
92
+ // Constants
93
+ // ---------------------------------------------------------------------------
94
+
95
+ const DEFAULT_MAX_FILES = 200;
96
+ const DEFAULT_MAX_MATCHES_PER_FILE = 100;
97
+ const MAX_FILE_BYTES = 50 * 1024 * 1024;
98
+
99
+ /** Binary extensions we flat-out skip (no regex rewrite makes sense). */
100
+ const BINARY_EXTS = new Set([
101
+ ".png", ".jpg", ".jpeg", ".gif", ".webp", ".avif", ".ico", ".bmp", ".tiff",
102
+ ".pdf", ".wasm", ".woff", ".woff2", ".ttf", ".otf", ".eot",
103
+ ".mp3", ".mp4", ".mov", ".avi", ".webm", ".ogg", ".wav", ".flac",
104
+ ".zip", ".tar", ".gz", ".bz2", ".7z", ".rar",
105
+ ".so", ".dylib", ".dll", ".exe", ".class",
106
+ ".db", ".sqlite", ".sqlite3",
107
+ ]);
108
+
109
+ /** Cap per-file scanning at this byte count for the null-byte sniff. */
110
+ const SNIFF_BYTES = 512;
111
+
112
+ // ---------------------------------------------------------------------------
113
+ // rg resolver (duplicated from rename-file-server; see note there)
114
+ // ---------------------------------------------------------------------------
115
+
116
+ function resolveRg(): string {
117
+ const candidates = [
118
+ "/opt/homebrew/bin/rg",
119
+ "/usr/local/bin/rg",
120
+ "/usr/bin/rg",
121
+ "/opt/homebrew/lib/node_modules/@openai/codex/node_modules/@openai/codex-darwin-arm64/vendor/aarch64-apple-darwin/path/rg",
122
+ "/opt/homebrew/lib/node_modules/@openai/codex/bin/rg",
123
+ ];
124
+ const bun = (globalThis as { Bun?: { which(bin: string): string | null } }).Bun;
125
+ const fromBun = bun ? bun.which("rg") : null;
126
+ if (fromBun) return fromBun;
127
+ for (const p of candidates) {
128
+ try {
129
+ accessSync(p);
130
+ return p;
131
+ } catch {
132
+ // keep trying
133
+ }
134
+ }
135
+ return "rg";
136
+ }
137
+
138
+ // ---------------------------------------------------------------------------
139
+ // Regex validation
140
+ // ---------------------------------------------------------------------------
141
+
142
+ /**
143
+ * Compile the user-supplied pattern + flags into a global RegExp and
144
+ * verify it can't match the empty string (which would cause a runaway
145
+ * replaceAll / infinite-length output). We always force the `g` flag on
146
+ * — cross-file replace only makes sense globally.
147
+ */
148
+ function compilePattern(
149
+ pattern: string,
150
+ flags: string | undefined,
151
+ ): { ok: true; re: RegExp } | { ok: false; reason: string } {
152
+ if (!pattern || typeof pattern !== "string") {
153
+ return { ok: false, reason: "'pattern' is required and must be a string" };
154
+ }
155
+ if (pattern.length === 0) {
156
+ return { ok: false, reason: "'pattern' must not be empty" };
157
+ }
158
+
159
+ // Normalize flags. Strip any caller-supplied `g` (we re-add it) and
160
+ // whitelist the subset we support — `i`, `m`, `s`, `u`. `y` (sticky)
161
+ // is refused because it breaks String.replaceAll's global-replace
162
+ // contract, and `d` has no effect on replacement.
163
+ const rawFlags = (flags ?? "").replace(/g/g, "");
164
+ for (const f of rawFlags) {
165
+ if (!"imsu".includes(f)) {
166
+ return { ok: false, reason: `unsupported flag '${f}' — only i, m, s, u are supported (g is implicit)` };
167
+ }
168
+ }
169
+ const finalFlags = "g" + rawFlags;
170
+
171
+ let re: RegExp;
172
+ try {
173
+ re = new RegExp(pattern, finalFlags);
174
+ } catch (err) {
175
+ return { ok: false, reason: `invalid regex: ${(err as Error).message}` };
176
+ }
177
+
178
+ // Reject zero-width / empty-match patterns. We probe by running the
179
+ // regex against a single-char string — if `.exec` produces a match of
180
+ // length zero, the pattern accepts the empty string and would loop
181
+ // infinitely against any input. Reset lastIndex after probing.
182
+ const probe = " ";
183
+ re.lastIndex = 0;
184
+ const m = re.exec(probe);
185
+ if (m && m[0].length === 0) {
186
+ return { ok: false, reason: "pattern matches the empty string (zero-width match would cause runaway replacement)" };
187
+ }
188
+ // Second probe: empty string directly. Some patterns (e.g. `^`) only
189
+ // fire against empty input.
190
+ re.lastIndex = 0;
191
+ const m2 = re.exec("");
192
+ if (m2 && m2[0].length === 0) {
193
+ return { ok: false, reason: "pattern matches the empty string (zero-width match would cause runaway replacement)" };
194
+ }
195
+ re.lastIndex = 0;
196
+ return { ok: true, re };
197
+ }
198
+
199
+ // ---------------------------------------------------------------------------
200
+ // Binary detection
201
+ // ---------------------------------------------------------------------------
202
+
203
+ function isBinaryByExt(path: string): boolean {
204
+ return BINARY_EXTS.has(extname(path).toLowerCase());
205
+ }
206
+
207
+ /**
208
+ * Read the first `SNIFF_BYTES` of `path` and check for NUL bytes. If a
209
+ * file contains \x00 in its header, we treat it as binary regardless of
210
+ * extension.
211
+ */
212
+ function looksBinary(buf: Buffer): boolean {
213
+ const len = Math.min(buf.length, SNIFF_BYTES);
214
+ for (let i = 0; i < len; i++) {
215
+ if (buf[i] === 0) return true;
216
+ }
217
+ return false;
218
+ }
219
+
220
+ // ---------------------------------------------------------------------------
221
+ // Candidate discovery via ripgrep
222
+ // ---------------------------------------------------------------------------
223
+
224
+ /**
225
+ * Enumerate files under `searchRoots` that contain at least one match
226
+ * for `pattern`. Honors include/exclude globs. Caps the result set at
227
+ * `cap` and reports truncation via the returned flag.
228
+ *
229
+ * We hand the user's regex directly to rg (Rust `regex` crate) — its
230
+ * syntax is a superset of the JS subset most users write, so practical
231
+ * patterns (`logger\.info\(`, `\bfoo\b`, etc.) behave identically. If a
232
+ * pattern uses JS-only syntax (lookbehind, named backrefs in some
233
+ * forms), rg will reject it during discovery and we'll fall through
234
+ * with zero candidates — a no-op, not a crash.
235
+ *
236
+ * include/exclude globs are forwarded to rg AND re-applied in JS via
237
+ * minimatch. The JS post-filter is the source of truth: rg's --glob
238
+ * behaviour with absolute search roots differs across rg versions on
239
+ * Windows (the glob may be matched against the full path instead of the
240
+ * path relative to the search root), making it unreliable as the sole
241
+ * filter. Passing them to rg is still worthwhile for performance — it
242
+ * narrows the candidate set early — but correctness is guaranteed by the
243
+ * minimatch pass below.
244
+ */
245
+
246
+ /**
247
+ * Determine whether an absolute path `p` matches a set of include and/or
248
+ * exclude glob patterns. Patterns are matched against the POSIX-normalised
249
+ * path relative to `base` (forward slashes, no leading "./"). Both `include`
250
+ * and `exclude` use minimatch with `{dot:true, matchBase:true}` so patterns
251
+ * like "src/**\/*.ts" or "**\/vendor\/**" work regardless of platform.
252
+ *
253
+ * Rules:
254
+ * - If `include` is non-empty, the file must match at least one pattern.
255
+ * - If `exclude` is non-empty, the file must not match any pattern.
256
+ * - If both are empty, the file is always accepted.
257
+ */
258
+ function matchesGlobs(
259
+ p: string,
260
+ base: string,
261
+ include: string[] | undefined,
262
+ exclude: string[] | undefined,
263
+ ): boolean {
264
+ // Relative path with forward slashes regardless of platform.
265
+ const rel = relative(base, p).split(sep).join("/");
266
+ const opts = { dot: true, matchBase: true };
267
+
268
+ if (include && include.length > 0) {
269
+ const matched = include.some((g) => minimatch(rel, g, opts));
270
+ if (!matched) return false;
271
+ }
272
+ if (exclude && exclude.length > 0) {
273
+ const excluded = exclude.some((g) => minimatch(rel, g, opts));
274
+ if (excluded) return false;
275
+ }
276
+ return true;
277
+ }
278
+
279
+ function discoverCandidates(
280
+ pattern: string,
281
+ flags: string,
282
+ searchRoots: string[],
283
+ include: string[] | undefined,
284
+ exclude: string[] | undefined,
285
+ cap: number,
286
+ ): { files: string[]; truncated: boolean; rgFailed: boolean } {
287
+ const rg = resolveRg();
288
+ const args: string[] = ["--files-with-matches"];
289
+
290
+ if (flags.includes("i")) args.push("--ignore-case");
291
+ if (flags.includes("m")) args.push("--multiline");
292
+ if (flags.includes("s")) {
293
+ // `s` (dotall) in JS = "." matches \n. rg's `--multiline-dotall` plus
294
+ // `--multiline` gives the same effect.
295
+ args.push("--multiline");
296
+ args.push("--multiline-dotall");
297
+ }
298
+
299
+ // Pass globs to rg for a performance pre-filter (narrows the candidate set
300
+ // early). Correctness is re-verified below by the JS matchesGlobs() pass —
301
+ // see the note above discoverCandidates for why rg alone is insufficient.
302
+ // Cross-platform note: rg's `--glob` interpretation on Windows is brittle
303
+ // (case-insensitive drive letters, slash-direction semantics differ
304
+ // between rg builds). We still pass the globs as a perf hint, but on
305
+ // Windows the JS post-filter (matchesGlobs) is the load-bearing piece —
306
+ // skipping the pre-filter here for include/exclude on win32 would only
307
+ // cost extra files to scan, not correctness. We accept that small cost
308
+ // in exchange for predictable behavior.
309
+ if (process.platform !== "win32") {
310
+ for (const g of include ?? []) args.push("--glob", g);
311
+ for (const g of exclude ?? []) args.push("--glob", `!${g}`);
312
+ }
313
+
314
+ // Signal end-of-flags before the pattern + roots so a pattern starting
315
+ // with `-` isn't misparsed as a flag.
316
+ args.push("--regexp", pattern);
317
+ for (const r of searchRoots) args.push(r);
318
+
319
+ const res = spawnSync(rg, args, { encoding: "utf-8", timeout: 30_000 });
320
+ // rg exits 0 = matches found, 1 = no matches, 2 = error
321
+ if (res.status === 2 || (res.error && !res.stdout)) {
322
+ return { files: [], truncated: false, rgFailed: true };
323
+ }
324
+
325
+ // Canonical form of each search root — used by matchesGlobs to compute the
326
+ // path relative to whichever root contains the candidate.
327
+ const canonicalRoots = searchRoots.map((r) => {
328
+ try { return realpathSync(r); } catch { return pathResolve(r); }
329
+ });
330
+
331
+ const files = new Set<string>();
332
+ let truncated = false;
333
+ for (const line of (res.stdout ?? "").split("\n")) {
334
+ // trim() strips both '\r' (Windows CRLF from rg output) and whitespace.
335
+ const p = line.trim();
336
+ if (!p) continue;
337
+ // Normalize separators for the current platform so that paths emitted by
338
+ // rg on Windows (which may use forward slashes for --glob-matched results)
339
+ // are consistent with what statSync / clampToCwd expect.
340
+ const normalized = pathNormalize(p);
341
+ // Canonicalize so macOS /var → /private/var matches the clamp output.
342
+ let canonical: string;
343
+ try {
344
+ canonical = realpathSync(normalized);
345
+ } catch {
346
+ canonical = pathResolve(normalized);
347
+ }
348
+
349
+ // JS-side include/exclude post-filter. rg's --glob is a performance
350
+ // pre-filter only; this pass is the correctness gate. We match against
351
+ // the path relative to whichever root contains the file (longest prefix
352
+ // wins so nested roots are handled correctly).
353
+ if (include?.length || exclude?.length) {
354
+ let base = canonicalRoots[0] ?? process.cwd();
355
+ for (const r of canonicalRoots) {
356
+ if (canonical.startsWith(r) && r.length > base.length) base = r;
357
+ }
358
+ if (!matchesGlobs(canonical, base, include, exclude)) continue;
359
+ }
360
+
361
+ if (files.size >= cap) {
362
+ truncated = true;
363
+ break;
364
+ }
365
+ files.add(canonical);
366
+ }
367
+ return { files: Array.from(files), truncated, rgFailed: false };
368
+ }
369
+
370
+ // ---------------------------------------------------------------------------
371
+ // Replacement with a per-file match cap
372
+ // ---------------------------------------------------------------------------
373
+
374
+ /**
375
+ * Apply the regex against `source`, returning `{ updated, count, capped }`.
376
+ * Walks matches manually (rather than calling `String.replaceAll`) so we
377
+ * can respect the per-file cap deterministically — once we hit `cap`
378
+ * replacements, further matches are left in place and `capped=true` is
379
+ * returned.
380
+ *
381
+ * The regex is assumed to have the global `g` flag (enforced by the
382
+ * compile step). Capture groups (`$1`, `$&`) in `replacement` are
383
+ * supported via JS's native semantics by delegating each replacement to
384
+ * `source.substring(m.index, m.index + m[0].length).replace(singleRe, repl)`
385
+ * where `singleRe` is a non-global copy of the compiled pattern — that
386
+ * way `$n` backrefs resolve correctly without us reimplementing JS's
387
+ * replacement mini-DSL.
388
+ */
389
+ export function applyRegex(
390
+ source: string,
391
+ re: RegExp,
392
+ replacement: string,
393
+ cap: number,
394
+ ): { updated: string; count: number; capped: boolean } {
395
+ // Non-global sibling for single-match replacement with backref support.
396
+ const singleFlags = re.flags.replace(/g/g, "");
397
+ const singleRe = new RegExp(re.source, singleFlags);
398
+
399
+ let out = "";
400
+ let lastIdx = 0;
401
+ let count = 0;
402
+ re.lastIndex = 0;
403
+
404
+ let m: RegExpExecArray | null;
405
+ while ((m = re.exec(source))) {
406
+ // Zero-width match defence in depth — the compile step already
407
+ // rejects these but a regex could theoretically produce an
408
+ // empty match mid-stream (e.g. `a*`). Bail out if we see one.
409
+ if (m[0].length === 0) {
410
+ re.lastIndex = m.index + 1;
411
+ continue;
412
+ }
413
+ if (count >= cap) break;
414
+
415
+ out += source.slice(lastIdx, m.index);
416
+ // JS's `replace` on a fresh non-global regex applied to the exact
417
+ // match text resolves $1…$n / $& / $` / $' correctly.
418
+ const rewritten = m[0].replace(singleRe, replacement);
419
+ out += rewritten;
420
+ lastIdx = m.index + m[0].length;
421
+ count += 1;
422
+ }
423
+
424
+ const capped = count >= cap && re.exec(source) !== null;
425
+ re.lastIndex = 0;
426
+
427
+ out += source.slice(lastIdx);
428
+ return { updated: out, count, capped };
429
+ }
430
+
431
+ // ---------------------------------------------------------------------------
432
+ // Atomic per-file write
433
+ // ---------------------------------------------------------------------------
434
+
435
+ /**
436
+ * Write `content` to `path` atomically (temp file in same directory, then
437
+ * rename). Keeps reads consistent even if the process dies mid-write.
438
+ */
439
+ async function atomicWrite(path: string, content: string): Promise<void> {
440
+ const tmp = `${path}.${process.pid}.${Date.now()}.tmp`;
441
+ await writeFile(tmp, content, "utf-8");
442
+ await rename(tmp, path);
443
+ }
444
+
445
+ // ---------------------------------------------------------------------------
446
+ // Core planner
447
+ // ---------------------------------------------------------------------------
448
+
449
+ export async function planSearchReplaceRegex(
450
+ args: SearchReplaceRegexArgs,
451
+ ): Promise<SearchReplaceRegexResult> {
452
+ const {
453
+ pattern,
454
+ replacement,
455
+ flags,
456
+ include,
457
+ exclude,
458
+ maxFiles = DEFAULT_MAX_FILES,
459
+ maxMatchesPerFile = DEFAULT_MAX_MATCHES_PER_FILE,
460
+ roots,
461
+ } = args;
462
+
463
+ if (typeof replacement !== "string") {
464
+ return { ok: false, reason: "'replacement' is required and must be a string" };
465
+ }
466
+
467
+ const compiled = compilePattern(pattern, flags);
468
+ if (!compiled.ok) return compiled;
469
+
470
+ // Resolve + clamp search roots.
471
+ const searchRoots: string[] = [];
472
+ if (Array.isArray(roots) && roots.length > 0) {
473
+ for (const r of roots) {
474
+ const c = clampToCwd(r, "ashlr__search_replace_regex");
475
+ if (!c.ok) return { ok: false, reason: c.message };
476
+ searchRoots.push(c.abs);
477
+ }
478
+ } else {
479
+ const c = clampToCwd(primaryProjectRoot(), "ashlr__search_replace_regex");
480
+ if (!c.ok) return { ok: false, reason: c.message };
481
+ searchRoots.push(c.abs);
482
+ }
483
+
484
+ const warnings: string[] = [];
485
+
486
+ const rawFlags = (flags ?? "").replace(/g/g, "");
487
+ const finalFlags = "g" + rawFlags;
488
+ const { files: candidates, truncated, rgFailed } = discoverCandidates(
489
+ pattern,
490
+ finalFlags,
491
+ searchRoots,
492
+ include,
493
+ exclude,
494
+ maxFiles,
495
+ );
496
+
497
+ if (rgFailed) {
498
+ return {
499
+ ok: false,
500
+ reason: "ripgrep failed to scan — likely a regex syntax rg doesn't accept (e.g. JS lookbehind). Try a simpler pattern.",
501
+ };
502
+ }
503
+
504
+ if (truncated) {
505
+ warnings.push(
506
+ `ripgrep returned more than ${maxFiles} candidates — only the first ${maxFiles} were scanned. ` +
507
+ `Rerun with a larger maxFiles or narrower include/roots.`,
508
+ );
509
+ }
510
+
511
+ const filesPlan: FilePlan[] = [];
512
+ let skipped = 0;
513
+
514
+ for (const candidate of candidates) {
515
+ // Clamp each candidate too — rg shouldn't return paths outside cwd
516
+ // since we gave it cwd-scoped roots, but defense in depth: if a
517
+ // symlink in the roots points outside cwd, the clamp refuses.
518
+ const clamp = clampToCwd(candidate, "ashlr__search_replace_regex");
519
+ if (!clamp.ok) {
520
+ skipped++;
521
+ warnings.push(`${candidate}: skipped — outside cwd`);
522
+ continue;
523
+ }
524
+
525
+ if (isBinaryByExt(candidate)) {
526
+ skipped++;
527
+ warnings.push(`${candidate}: skipped — binary extension`);
528
+ continue;
529
+ }
530
+
531
+ // Stat + size guard: refuse 0-byte, non-regular, and absurdly-large
532
+ // files (>50 MB) so a stray ISO in the roots can't block the tool.
533
+ let st;
534
+ try {
535
+ st = statSync(candidate);
536
+ } catch {
537
+ skipped++;
538
+ continue;
539
+ }
540
+ if (!st.isFile()) {
541
+ skipped++;
542
+ continue;
543
+ }
544
+ if (st.size === 0) {
545
+ skipped++;
546
+ continue;
547
+ }
548
+ if (st.size > MAX_FILE_BYTES) {
549
+ skipped++;
550
+ warnings.push(
551
+ `${candidate}: skipped — file is ${st.size} bytes, over the ${MAX_FILE_BYTES} byte safety cap`,
552
+ );
553
+ continue;
554
+ }
555
+
556
+ let raw: Buffer;
557
+ try {
558
+ raw = await readFile(candidate);
559
+ } catch {
560
+ skipped++;
561
+ continue;
562
+ }
563
+
564
+ if (looksBinary(raw)) {
565
+ skipped++;
566
+ warnings.push(`${candidate}: skipped — binary content (NUL byte in first ${SNIFF_BYTES}B)`);
567
+ continue;
568
+ }
569
+
570
+ const source = raw.toString("utf-8");
571
+ const { updated, count, capped } = applyRegex(source, compiled.re, replacement, maxMatchesPerFile);
572
+ if (count === 0) {
573
+ // rg said there were matches but our regex saw none — possible if
574
+ // rg's regex engine differs at some edge (unicode classes, etc.).
575
+ // Skip silently.
576
+ skipped++;
577
+ continue;
578
+ }
579
+ if (capped) {
580
+ warnings.push(
581
+ `${candidate}: match cap hit — only first ${maxMatchesPerFile} replacements applied, remaining matches left in place.`,
582
+ );
583
+ }
584
+ filesPlan.push({
585
+ path: candidate,
586
+ matches: count,
587
+ capped,
588
+ originalBytes: Buffer.byteLength(source, "utf-8"),
589
+ newBytes: Buffer.byteLength(updated, "utf-8"),
590
+ });
591
+ }
592
+
593
+ return {
594
+ ok: true,
595
+ plan: {
596
+ files: filesPlan,
597
+ skipped,
598
+ warnings,
599
+ },
600
+ };
601
+ }
602
+
603
+ // ---------------------------------------------------------------------------
604
+ // Apply (writes with per-file rollback)
605
+ // ---------------------------------------------------------------------------
606
+
607
+ /**
608
+ * Re-read each planned file, re-apply the regex, write atomically.
609
+ * On any write failure, best-effort rolls back previously-written files
610
+ * from the cached pre-image. We read-and-replace again here (rather
611
+ * than caching the planner's `updated` strings) so the plan stays
612
+ * accurate even if the file changed between plan and apply — if a
613
+ * file no longer has matches, we simply skip it.
614
+ */
615
+ export async function applySearchReplaceRegexPlan(
616
+ plan: SearchReplaceRegexPlan,
617
+ args: SearchReplaceRegexArgs,
618
+ ): Promise<{
619
+ written: number;
620
+ totalReplacements: number;
621
+ totalOriginalBytes: number;
622
+ totalNewBytes: number;
623
+ rollbackWarnings: string[];
624
+ }> {
625
+ const compiled = compilePattern(args.pattern, args.flags);
626
+ if (!compiled.ok) {
627
+ // Shouldn't happen (planner already validated), but be safe.
628
+ throw new Error(compiled.reason);
629
+ }
630
+ const cap = args.maxMatchesPerFile ?? DEFAULT_MAX_MATCHES_PER_FILE;
631
+
632
+ const writtenPreImages: Array<{ path: string; original: string }> = [];
633
+ const rollbackWarnings: string[] = [];
634
+ let totalReplacements = 0;
635
+ let totalOriginalBytes = 0;
636
+ let totalNewBytes = 0;
637
+
638
+ for (const fp of plan.files) {
639
+ let source: string;
640
+ try {
641
+ source = await readFile(fp.path, "utf-8");
642
+ } catch (err) {
643
+ rollbackWarnings.push(`failed to re-read ${fp.path}: ${(err as Error).message}`);
644
+ continue;
645
+ }
646
+
647
+ const { updated, count } = applyRegex(source, compiled.re, args.replacement, cap);
648
+ if (count === 0) continue;
649
+
650
+ try {
651
+ await atomicWrite(fp.path, updated);
652
+ writtenPreImages.push({ path: fp.path, original: source });
653
+ totalReplacements += count;
654
+ totalOriginalBytes += Buffer.byteLength(source, "utf-8");
655
+ totalNewBytes += Buffer.byteLength(updated, "utf-8");
656
+ } catch (err) {
657
+ // Roll back everything written so far.
658
+ for (const prev of writtenPreImages) {
659
+ try {
660
+ await writeFile(prev.path, prev.original, "utf-8");
661
+ } catch (rbErr) {
662
+ rollbackWarnings.push(
663
+ `rollback failed for ${prev.path}: ${(rbErr as Error).message}`,
664
+ );
665
+ }
666
+ }
667
+ const msg = err instanceof Error ? err.message : String(err);
668
+ throw new Error(`failed to write ${fp.path}: ${msg}`);
669
+ }
670
+ }
671
+
672
+ return {
673
+ written: writtenPreImages.length,
674
+ totalReplacements,
675
+ totalOriginalBytes,
676
+ totalNewBytes,
677
+ rollbackWarnings,
678
+ };
679
+ }
680
+
681
+ // ---------------------------------------------------------------------------
682
+ // Top-level callable (used by the MCP handler)
683
+ // ---------------------------------------------------------------------------
684
+
685
+ export async function ashlrSearchReplaceRegex(
686
+ args: SearchReplaceRegexArgs,
687
+ ): Promise<{ text: string; originalBytes: number; newBytes: number }> {
688
+ const planRes = await planSearchReplaceRegex(args);
689
+ if (!planRes.ok) {
690
+ throw new Error(planRes.reason);
691
+ }
692
+ const { plan } = planRes;
693
+ const dryRun = args.dryRun === true;
694
+
695
+ const fileCount = plan.files.length;
696
+ const totalMatches = plan.files.reduce((s, f) => s + f.matches, 0);
697
+
698
+ const lines: string[] = [];
699
+ lines.push(
700
+ `[ashlr__search_replace_regex] ${dryRun ? "(dry run) " : ""}` +
701
+ `${fileCount} file${fileCount === 1 ? "" : "s"}, ${totalMatches} replacement${totalMatches === 1 ? "" : "s"}` +
702
+ (plan.skipped > 0 ? `, ${plan.skipped} candidate${plan.skipped === 1 ? "" : "s"} skipped` : ""),
703
+ );
704
+
705
+ if (plan.warnings.length > 0) {
706
+ lines.push(" warnings:");
707
+ for (const w of plan.warnings) lines.push(` ${w}`);
708
+ }
709
+
710
+ // Compact per-file breakdown when the change set is small.
711
+ if (fileCount <= 10) {
712
+ for (const fp of plan.files) {
713
+ lines.push(` ${fp.path}: ${fp.matches} match${fp.matches === 1 ? "" : "es"}${fp.capped ? " (capped)" : ""}`);
714
+ }
715
+ }
716
+
717
+ let totalOriginalBytes = 0;
718
+ let totalNewBytes = 0;
719
+
720
+ if (dryRun) {
721
+ for (const fp of plan.files) {
722
+ totalOriginalBytes += fp.originalBytes;
723
+ totalNewBytes += fp.newBytes;
724
+ }
725
+ lines.push(" (dry run — no files written)");
726
+ return { text: lines.join("\n"), originalBytes: totalOriginalBytes, newBytes: totalNewBytes };
727
+ }
728
+
729
+ if (fileCount === 0) {
730
+ return { text: lines.join("\n"), originalBytes: 0, newBytes: 0 };
731
+ }
732
+
733
+ const applied = await applySearchReplaceRegexPlan(plan, args);
734
+ totalOriginalBytes = applied.totalOriginalBytes;
735
+ totalNewBytes = applied.totalNewBytes;
736
+
737
+ lines.push(
738
+ ` ${applied.written} file${applied.written === 1 ? "" : "s"} written, ` +
739
+ `${applied.totalReplacements} total replacement${applied.totalReplacements === 1 ? "" : "s"}`,
740
+ );
741
+ if (applied.rollbackWarnings.length > 0) {
742
+ lines.push(" rollback warnings:");
743
+ for (const w of applied.rollbackWarnings) lines.push(` ${w}`);
744
+ }
745
+
746
+ return { text: lines.join("\n"), originalBytes: totalOriginalBytes, newBytes: totalNewBytes };
747
+ }
748
+
749
+ // Silence unused-import warnings from `stat` — kept in scope for future
750
+ // use (symlink detection).
751
+ void stat;