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,166 @@
1
+ /**
2
+ * ls-server-handlers — side-effect module.
3
+ *
4
+ * Importing this file registers the ashlr__ls tool into the shared registry
5
+ * (_tool-base.ts). Used by both the standalone entry point (ls-server.ts)
6
+ * and the router (_router.ts via _router-handlers.ts).
7
+ */
8
+
9
+ import { registerTool, type ToolCallContext, type ToolResult } from "./_tool-base";
10
+ import { existsSync, readdirSync, statSync } from "fs";
11
+ import { resolve } from "path";
12
+ import { spawnSync } from "child_process";
13
+ import { recordSaving as recordSavingCore } from "./_stats";
14
+ import { clampToCwd } from "./_cwd-clamp";
15
+
16
+ // ---------------------------------------------------------------------------
17
+ // Types + constants
18
+ // ---------------------------------------------------------------------------
19
+
20
+ interface LsOptions {
21
+ path?: string;
22
+ maxEntries?: number;
23
+ sizes?: boolean;
24
+ bypassSummary?: boolean;
25
+ }
26
+
27
+ interface Entry {
28
+ name: string;
29
+ dir: boolean;
30
+ size: number;
31
+ }
32
+
33
+ const DEFAULT_MAX = 80;
34
+
35
+ // ---------------------------------------------------------------------------
36
+ // Core logic (exported for tests)
37
+ // ---------------------------------------------------------------------------
38
+
39
+ function listDir(rootAbs: string): Entry[] | { error: string } {
40
+ let names: string[];
41
+ if (!existsSync(rootAbs)) return { error: `No such path: ${rootAbs}` };
42
+ try {
43
+ const st = statSync(rootAbs);
44
+ if (!st.isDirectory()) return { error: `Not a directory: ${rootAbs}` };
45
+ } catch (err) {
46
+ return { error: `Cannot stat ${rootAbs}: ${String(err)}` };
47
+ }
48
+
49
+ const git = spawnSync("git", ["-C", rootAbs, "rev-parse", "--is-inside-work-tree"], {
50
+ encoding: "utf-8",
51
+ });
52
+ if (git.status === 0 && git.stdout.trim() === "true") {
53
+ const set = new Set<string>();
54
+ const collectTopLevel = (args: string[]): void => {
55
+ const result = spawnSync("git", ["-C", rootAbs, ...args], { encoding: "utf-8" });
56
+ if (result.status !== 0) return;
57
+ for (const line of result.stdout.split("\n")) {
58
+ const first = line.split("/")[0];
59
+ if (first) set.add(first);
60
+ }
61
+ };
62
+ collectTopLevel(["ls-files"]);
63
+ collectTopLevel(["ls-files", "--others", "--exclude-standard"]);
64
+ if (set.size > 0) {
65
+ names = Array.from(set);
66
+ } else {
67
+ try { names = readdirSync(rootAbs); } catch (err) {
68
+ return { error: `readdir failed: ${String(err)}` };
69
+ }
70
+ }
71
+ } else {
72
+ try { names = readdirSync(rootAbs); } catch (err) {
73
+ return { error: `readdir failed: ${String(err)}` };
74
+ }
75
+ }
76
+
77
+ const entries: Entry[] = [];
78
+ for (const name of names) {
79
+ if (name === ".DS_Store") continue;
80
+ try {
81
+ const st = statSync(resolve(rootAbs, name));
82
+ entries.push({ name, dir: st.isDirectory(), size: st.size });
83
+ } catch {
84
+ // Broken symlink / permission denied — skip.
85
+ }
86
+ }
87
+ entries.sort((a, b) => {
88
+ if (a.dir !== b.dir) return a.dir ? -1 : 1;
89
+ return a.name.localeCompare(b.name);
90
+ });
91
+ return entries;
92
+ }
93
+
94
+ function formatSize(bytes: number): string {
95
+ if (bytes < 1024) return `${bytes}B`;
96
+ if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)}K`;
97
+ if (bytes < 1024 * 1024 * 1024) return `${(bytes / 1024 / 1024).toFixed(1)}M`;
98
+ return `${(bytes / 1024 / 1024 / 1024).toFixed(1)}G`;
99
+ }
100
+
101
+ function renderEntries(entries: Entry[], sizes: boolean, maxEntries: number, elided = 0): string {
102
+ const shown = entries.slice(0, maxEntries);
103
+ const lines: string[] = [];
104
+ for (const e of shown) {
105
+ const name = e.dir ? `${e.name}/` : e.name;
106
+ if (sizes && !e.dir) {
107
+ lines.push(`${formatSize(e.size).padStart(7)} ${name}`);
108
+ } else if (sizes) {
109
+ lines.push(`${"—".padStart(7)} ${name}`);
110
+ } else {
111
+ lines.push(name);
112
+ }
113
+ }
114
+ if (elided > 0) {
115
+ lines.push(`[… ${elided} entries elided — pass bypassSummary:true to see the full list]`);
116
+ }
117
+ return lines.join("\n");
118
+ }
119
+
120
+ export async function handleLs(args: LsOptions): Promise<string> {
121
+ const maxEntries = Math.max(1, Math.min(1000, args.maxEntries ?? DEFAULT_MAX));
122
+ const sizes = args.sizes === true;
123
+ const bypass = args.bypassSummary === true;
124
+
125
+ const clamp = clampToCwd(args.path, "ashlr__ls");
126
+ if (!clamp.ok) return clamp.message;
127
+ const rootAbs = clamp.abs;
128
+
129
+ const result = listDir(rootAbs);
130
+ if ("error" in result) return result.error;
131
+
132
+ const total = result.length;
133
+ const max = bypass ? total : maxEntries;
134
+ const elided = Math.max(0, total - max);
135
+ const rendered = renderEntries(result, sizes, max, elided);
136
+
137
+ const baseline = result.reduce((acc, e) => acc + e.name.length + 12, 0);
138
+ await recordSavingCore(baseline, rendered.length, "ashlr__ls");
139
+
140
+ const header = `${rootAbs} · ${total} entries${elided ? ` (${max} shown)` : ""}`;
141
+ return `${header}\n${rendered}`;
142
+ }
143
+
144
+ // ---------------------------------------------------------------------------
145
+ // Registration (side-effect on import)
146
+ // ---------------------------------------------------------------------------
147
+
148
+ registerTool({
149
+ name: "ashlr__ls",
150
+ description:
151
+ "Gitignore-aware directory listing. Compact columnar output with elision past maxEntries (default 80). Set bypassSummary:true to see all entries.",
152
+ inputSchema: {
153
+ type: "object",
154
+ properties: {
155
+ path: { type: "string", description: "Directory to list (absolute or cwd-relative). Default '.'" },
156
+ maxEntries: { type: "number", description: "Cap on entries shown (default 80, max 1000)" },
157
+ sizes: { type: "boolean", description: "Include human-readable file sizes" },
158
+ bypassSummary: { type: "boolean", description: "Return every entry without elision" },
159
+ },
160
+ additionalProperties: false,
161
+ },
162
+ handler: async (args: Record<string, unknown>, _ctx: ToolCallContext): Promise<ToolResult> => {
163
+ const text = await handleLs(args as unknown as LsOptions);
164
+ return { content: [{ type: "text", text }] };
165
+ },
166
+ });
@@ -0,0 +1,20 @@
1
+ #!/usr/bin/env bun
2
+ /**
3
+ * ashlr-ls MCP server — standalone entry point.
4
+ *
5
+ * Imports ls-server-handlers (registers ashlr__ls into the shared
6
+ * registry) then runs as a standalone stdio MCP server. Existing plugin.json
7
+ * entries continue to work unchanged through the v1.12 migration window.
8
+ *
9
+ * For in-process router use, import ls-server-handlers directly.
10
+ */
11
+
12
+ import "./ls-server-handlers";
13
+ import { runStandalone } from "./_tool-base";
14
+
15
+ // Re-export core logic so existing tests that import from this module keep working.
16
+ export { handleLs } from "./ls-server-handlers";
17
+
18
+ if (import.meta.main) {
19
+ await runStandalone("ashlr-ls", "0.1.0");
20
+ }
@@ -0,0 +1,51 @@
1
+ /**
2
+ * multi-edit-server-handlers — registers ashlr__multi_edit on the shared registry.
3
+ */
4
+
5
+ import { registerTool, type ToolCallContext, type ToolResult } from "./_tool-base";
6
+ import { ashlrMultiEdit, type MultiEditArgs } from "./multi-edit-server";
7
+
8
+ registerTool({
9
+ name: "ashlr__multi_edit",
10
+ description:
11
+ "Many edits across multiple files in one atomic call. Use instead of N separate " +
12
+ "Edit or ashlr__edit calls when touching more than one file — N round-trips collapse " +
13
+ "to 1, with full rollback on any failure. Compresses by returning one consolidated " +
14
+ "diff summary; typical savings (N−1) × per-call overhead. For a single file use " +
15
+ "ashlr__edit instead. For symbol renames use ashlr__edit_structural instead.",
16
+ inputSchema: {
17
+ type: "object",
18
+ properties: {
19
+ edits: {
20
+ type: "array",
21
+ description: "Ordered list of edits to apply atomically.",
22
+ items: {
23
+ type: "object",
24
+ properties: {
25
+ path: { type: "string", description: "Absolute or cwd-relative file path. File must exist." },
26
+ search: { type: "string", description: "Exact string to find in the file." },
27
+ replace: { type: "string", description: "String to replace it with." },
28
+ strict: {
29
+ type: "boolean",
30
+ description: "Default true: require exactly one match (error if 0 or 2+). Pass false to replace all occurrences.",
31
+ },
32
+ },
33
+ required: ["path", "search", "replace"],
34
+ },
35
+ },
36
+ },
37
+ required: ["edits"],
38
+ },
39
+ handler: async (args: Record<string, unknown>, _ctx: ToolCallContext): Promise<ToolResult> => {
40
+ try {
41
+ const text = await ashlrMultiEdit((args ?? {}) as unknown as MultiEditArgs);
42
+ return { content: [{ type: "text", text }] };
43
+ } catch (err) {
44
+ const message = err instanceof Error ? err.message : String(err);
45
+ // multi-edit returns the raw message (no prefix) so callers see the
46
+ // underlying tool's diagnostic verbatim — distinct from the tool-prefixed
47
+ // shape of other handlers.
48
+ return { content: [{ type: "text", text: message }], isError: true };
49
+ }
50
+ },
51
+ });
@@ -0,0 +1,422 @@
1
+ #!/usr/bin/env bun
2
+ /**
3
+ * ashlr-multi-edit MCP server.
4
+ *
5
+ * Exposes ashlr__multi_edit — atomic batched edits across N files in one
6
+ * roundtrip. Each edit is like ashlr__edit (path + search + replace + strict),
7
+ * but all N edits are applied atomically: if any fails, every prior edit is
8
+ * rolled back using the cached original content. Files are read once per path
9
+ * and written once per path at the end, after all edits succeed.
10
+ *
11
+ * Savings: baseline = sum(original.length + updated.length) across all files,
12
+ * which is what N naive Edit calls would have shipped. We record savings via
13
+ * the shared recordSaving from _stats.
14
+ */
15
+
16
+ import { Server } from "@modelcontextprotocol/sdk/server/index.js";
17
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
18
+ import {
19
+ CallToolRequestSchema,
20
+ ListToolsRequestSchema,
21
+ } from "@modelcontextprotocol/sdk/types.js";
22
+ import { readFile, rename, unlink, writeFile } from "fs/promises";
23
+ import { basename, dirname, join } from "path";
24
+ import { recordSaving } from "./_stats";
25
+ import { refreshGenomeAfterEdit } from "./_genome-live";
26
+ import { clampToCwd } from "./_cwd-clamp";
27
+ import { findFuzzyMatch } from "./_edit-match";
28
+ import { validateEdit } from "./_edit-validate";
29
+
30
+ // ---------------------------------------------------------------------------
31
+ // Types
32
+ // ---------------------------------------------------------------------------
33
+
34
+ interface SingleEdit {
35
+ path: string;
36
+ search: string;
37
+ replace: string;
38
+ /** Default true: require exactly one match. Pass false to replace all. */
39
+ strict?: boolean;
40
+ }
41
+
42
+ export interface MultiEditArgs {
43
+ edits: SingleEdit[];
44
+ }
45
+
46
+ // ---------------------------------------------------------------------------
47
+ // Token estimation (same formula as efficiency-server)
48
+ // ---------------------------------------------------------------------------
49
+
50
+ function estimateTokens(s: string): number {
51
+ return Math.ceil(s.length / 4);
52
+ }
53
+
54
+ function firstLine(s: string): string {
55
+ return (s.split("\n")[0] ?? "").slice(0, 72);
56
+ }
57
+
58
+ async function removeTemp(path: string): Promise<void> {
59
+ try {
60
+ await unlink(path);
61
+ } catch {
62
+ // Best effort only. A failed edit should report the original write error,
63
+ // not a cleanup miss for a temp file that may already be gone.
64
+ }
65
+ }
66
+
67
+ async function atomicWriteFile(path: string, content: string): Promise<void> {
68
+ const tempPath = join(
69
+ dirname(path),
70
+ `.${basename(path)}.${process.pid}.${Date.now()}.${Math.random().toString(36).slice(2)}.tmp`,
71
+ );
72
+
73
+ try {
74
+ await writeFile(tempPath, content, "utf-8");
75
+ await rename(tempPath, path);
76
+ } catch (err) {
77
+ await removeTemp(tempPath);
78
+ throw err;
79
+ }
80
+ }
81
+
82
+ async function writeChangedFilesAtomically(
83
+ changes: Array<{ path: string; original: string; updated: string }>,
84
+ ): Promise<void> {
85
+ const staged: Array<{ path: string; tempPath: string; original: string }> = [];
86
+ const committed: Array<{ path: string; original: string }> = [];
87
+
88
+ try {
89
+ // Stage every new file image before replacing any original file. If a
90
+ // later file cannot be written, no earlier original has changed.
91
+ for (const change of changes) {
92
+ const tempPath = join(
93
+ dirname(change.path),
94
+ `.${basename(change.path)}.${process.pid}.${Date.now()}.${Math.random().toString(36).slice(2)}.tmp`,
95
+ );
96
+ await writeFile(tempPath, change.updated, "utf-8");
97
+ staged.push({ path: change.path, tempPath, original: change.original });
98
+ }
99
+
100
+ for (const item of staged) {
101
+ await rename(item.tempPath, item.path);
102
+ committed.push({ path: item.path, original: item.original });
103
+ }
104
+ } catch (err) {
105
+ for (const item of staged) {
106
+ await removeTemp(item.tempPath);
107
+ }
108
+
109
+ const rollbackErrors: string[] = [];
110
+ for (const item of committed.reverse()) {
111
+ try {
112
+ await atomicWriteFile(item.path, item.original);
113
+ } catch (rollbackErr) {
114
+ const msg = rollbackErr instanceof Error ? rollbackErr.message : String(rollbackErr);
115
+ rollbackErrors.push(`${item.path}: ${msg}`);
116
+ }
117
+ }
118
+
119
+ const msg = err instanceof Error ? err.message : String(err);
120
+ const rollbackNote =
121
+ rollbackErrors.length > 0
122
+ ? `; rollback failed for ${rollbackErrors.join("; ")}`
123
+ : "";
124
+ throw new Error(`ashlr__multi_edit: failed to write changes: ${msg}${rollbackNote}`);
125
+ }
126
+ }
127
+
128
+ const FUZZY_SIZE_LIMIT = 2 * 1024 * 1024; // 2 MB
129
+
130
+ // ---------------------------------------------------------------------------
131
+ // Core logic
132
+ // ---------------------------------------------------------------------------
133
+
134
+ export async function ashlrMultiEdit(input: MultiEditArgs): Promise<string> {
135
+ const { edits } = input;
136
+ if (!Array.isArray(edits) || edits.length === 0) {
137
+ throw new Error("ashlr__multi_edit: 'edits' must be a non-empty array");
138
+ }
139
+
140
+ // Validate fields + clamp each path to cwd before any FS I/O. Fail-fast so
141
+ // we never partially apply a batch on an out-of-cwd path.
142
+ const clampedPaths: string[] = [];
143
+ for (let i = 0; i < edits.length; i++) {
144
+ const e = edits[i]!;
145
+ if (!e.path) throw new Error(`ashlr__multi_edit: edit[${i}] missing 'path'`);
146
+ if (!e.search) throw new Error(`ashlr__multi_edit: edit[${i}] missing 'search'`);
147
+ const clamp = clampToCwd(e.path, "ashlr__multi_edit");
148
+ if (!clamp.ok) throw new Error(clamp.message);
149
+ clampedPaths.push(clamp.abs);
150
+ }
151
+
152
+ // --- Phase 1: read each file once, coalescing by resolved path ---
153
+ const pathToOriginal = new Map<string, string>();
154
+ for (let i = 0; i < edits.length; i++) {
155
+ const e = edits[i]!;
156
+ const abs = clampedPaths[i]!;
157
+ if (pathToOriginal.has(abs)) continue;
158
+ let content: string;
159
+ try {
160
+ content = await readFile(abs, "utf-8");
161
+ } catch (err) {
162
+ const msg = err instanceof Error ? err.message : String(err);
163
+ throw new Error(`ashlr__multi_edit: cannot read ${e.path}: ${msg}`);
164
+ }
165
+ pathToOriginal.set(abs, content);
166
+ }
167
+
168
+ // --- Phase 2: apply edits in-memory, atomically ---
169
+ // Working copies — rolled back on any failure.
170
+ const working = new Map<string, string>(pathToOriginal);
171
+
172
+ // Per-path edit list for the summary (path → list of hunk summaries).
173
+ const perPathHunks = new Map<string, Array<{ search: string; replace: string; count: number; strict: boolean; fuzzy?: boolean }>>();
174
+
175
+ // Track per-path fuzzy notes for the summary.
176
+ const fuzzyNotes: string[] = [];
177
+
178
+ for (let i = 0; i < edits.length; i++) {
179
+ const e = edits[i]!;
180
+ const abs = clampedPaths[i]!;
181
+ const strict = e.strict !== false; // default true
182
+ const current = working.get(abs)!;
183
+
184
+ // Count occurrences.
185
+ let count = 0;
186
+ let idx = 0;
187
+ while ((idx = current.indexOf(e.search, idx)) !== -1) {
188
+ count++;
189
+ idx += e.search.length;
190
+ }
191
+
192
+ if (count === 0) {
193
+ // Try fuzzy fallback when in strict mode, file <= 2MB, fuzzy not disabled.
194
+ const fuzzyEnabled =
195
+ strict &&
196
+ current.length <= FUZZY_SIZE_LIMIT &&
197
+ process.env.ASHLR_EDIT_FUZZY !== "off";
198
+
199
+ if (fuzzyEnabled) {
200
+ const fuzzyMatch = findFuzzyMatch(current, e.search);
201
+ if (fuzzyMatch !== null) {
202
+ const updated =
203
+ current.slice(0, fuzzyMatch.start) +
204
+ e.replace +
205
+ current.slice(fuzzyMatch.end);
206
+ working.set(abs, updated);
207
+
208
+ const hunks = perPathHunks.get(abs) ?? [];
209
+ hunks.push({ search: e.search, replace: e.replace, count: 1, strict, fuzzy: true });
210
+ perPathHunks.set(abs, hunks);
211
+ fuzzyNotes.push(`edit[${i}] applied via fuzzy match (sim=${fuzzyMatch.score.toFixed(2)})`);
212
+ continue;
213
+ }
214
+ }
215
+
216
+ // Rollback: working map is already in-memory only, nothing written yet.
217
+ throw new Error(
218
+ `ashlr__multi_edit: edit[${i}] search string not found in ${e.path}`,
219
+ );
220
+ }
221
+ if (strict && count > 1) {
222
+ throw new Error(
223
+ `ashlr__multi_edit: edit[${i}] search matched ${count} times in ${e.path}; pass strict:false to replace all, or widen context to a unique span`,
224
+ );
225
+ }
226
+
227
+ // Use slice/concat (strict) or split/join (non-strict) to treat `e.replace`
228
+ // as a literal string. String.prototype.replace(string, string) would
229
+ // interpret `$&`, `$1`, `$'`, `$` in the replacement — silently
230
+ // corrupting any edit whose replacement contains a `$` followed by a
231
+ // digit, ampersand, backtick, or apostrophe.
232
+ let updated: string;
233
+ if (strict) {
234
+ const idx = current.indexOf(e.search);
235
+ updated = current.slice(0, idx) + e.replace + current.slice(idx + e.search.length);
236
+ } else {
237
+ updated = current.split(e.search).join(e.replace);
238
+ }
239
+
240
+ working.set(abs, updated);
241
+
242
+ const hunks = perPathHunks.get(abs) ?? [];
243
+ hunks.push({ search: e.search, replace: e.replace, count, strict });
244
+ perPathHunks.set(abs, hunks);
245
+ }
246
+
247
+ // --- Phase 2.5: validate each changed file before writing (Feature 3) ---
248
+ const validationWarnings: string[] = [];
249
+
250
+ for (const [abs, updatedContent] of working) {
251
+ const original = pathToOriginal.get(abs)!;
252
+ if (updatedContent === original) continue; // unchanged
253
+
254
+ const validation = await validateEdit(abs, original, updatedContent).catch(() => ({
255
+ introducedError: false,
256
+ detail: "",
257
+ }));
258
+
259
+ if (!validation.introducedError) continue;
260
+
261
+ if (process.env.ASHLR_EDIT_VALIDATE === "block") {
262
+ // Atomic block: throw before ANY file is written. Nothing is persisted.
263
+ let displayPath = abs;
264
+ try {
265
+ const rel = abs.startsWith(process.cwd()) ? abs.slice(process.cwd().length + 1) : abs;
266
+ displayPath = rel || abs;
267
+ } catch { /* use abs */ }
268
+ throw new Error(
269
+ `ashlr__multi_edit: refused — edit introduces a syntax error in ${displayPath} ` +
270
+ `(${validation.detail}); set ASHLR_EDIT_VALIDATE=warn to override`,
271
+ );
272
+ }
273
+
274
+ // Warn mode: collect for summary but still write.
275
+ let displayPath = abs;
276
+ try {
277
+ const rel = abs.startsWith(process.cwd()) ? abs.slice(process.cwd().length + 1) : abs;
278
+ displayPath = rel || abs;
279
+ } catch { /* use abs */ }
280
+ validationWarnings.push(`${displayPath}: ⚠ syntax — ${validation.detail}`);
281
+ }
282
+
283
+ // --- Phase 3: write each file once ---
284
+ const changedFiles: Array<{ path: string; original: string; updated: string }> = [];
285
+ for (const [abs, content] of working) {
286
+ // Only write files that changed.
287
+ const original = pathToOriginal.get(abs)!;
288
+ if (content !== original) {
289
+ changedFiles.push({ path: abs, original, updated: content });
290
+ }
291
+ }
292
+
293
+ await writeChangedFilesAtomically(changedFiles);
294
+
295
+ for (const file of changedFiles) {
296
+ // best-effort: refreshGenomeAfterEdit already swallows internally and never rejects; this outer catch is belt-and-braces against a pre-try sync throw (e.g. import init failure) so edits never fail because of observability.
297
+ refreshGenomeAfterEdit(file.path, file.original, file.updated).catch(() => {});
298
+ }
299
+
300
+ // --- Phase 4: build summary and record savings ---
301
+ // v1.22 Trust Pass: baseline is what N naive Edits would have SHIPPED — the
302
+ // sum of each hunk's (search + replace) bytes, NOT the full file contents
303
+ // twice. Aligns multi_edit accounting with the v1.18 single-edit fix
304
+ // (edit-server.ts uses `search.length + replace.length`). Previously this
305
+ // counted full file before+after for each touched file, which inflated the
306
+ // recorded savings 5-10× on small-hunk-into-large-file mass refactors.
307
+ let baseline = 0;
308
+ for (const [, hunks] of perPathHunks) {
309
+ for (const h of hunks) {
310
+ baseline += h.search.length + h.replace.length;
311
+ }
312
+ }
313
+
314
+ const fileCount = perPathHunks.size;
315
+ const totalEdits = edits.length;
316
+
317
+ const lines: string[] = [
318
+ `[ashlr__multi_edit] applied ${totalEdits} edit${totalEdits === 1 ? "" : "s"} across ${fileCount} file${fileCount === 1 ? "" : "s"}:`,
319
+ ];
320
+
321
+ for (const [abs, hunks] of perPathHunks) {
322
+ // Find a display path: relative to cwd if possible.
323
+ let displayPath = abs;
324
+ try {
325
+ const rel = abs.startsWith(process.cwd()) ? abs.slice(process.cwd().length + 1) : abs;
326
+ displayPath = rel || abs;
327
+ } catch { /* use abs */ }
328
+
329
+ lines.push(` ${displayPath} (${hunks.length} edit${hunks.length === 1 ? "" : "s"})`);
330
+ for (const h of hunks) {
331
+ lines.push(` - removed (${estimateTokens(h.search)} tok): ${firstLine(h.search)}${h.search.length > 72 ? "…" : ""}`);
332
+ lines.push(` + added (${estimateTokens(h.replace)} tok): ${firstLine(h.replace)}${h.replace.length > 72 ? "…" : ""}`);
333
+ }
334
+ }
335
+
336
+ const rawKB = Math.round(baseline / 1024);
337
+ const summaryLength = lines.join("\n").length;
338
+ const compactKB = Math.max(1, Math.round(summaryLength / 1024));
339
+ const savedTok = Math.max(0, Math.ceil((baseline - summaryLength) / 4));
340
+
341
+ lines.push(
342
+ `· total: ${totalEdits} hunk${totalEdits === 1 ? "" : "s"} · raw would have been ${rawKB} KB · compressed to ${compactKB} KB · saved ~${savedTok.toLocaleString()} tok`,
343
+ );
344
+
345
+ // Append fuzzy match notes.
346
+ for (const note of fuzzyNotes) {
347
+ lines.push(`[ashlr__multi_edit: ${note}]`);
348
+ }
349
+
350
+ // Append validation warnings.
351
+ for (const warn of validationWarnings) {
352
+ lines.push(`[ashlr__multi_edit: ⚠ syntax — ${warn}; review or fix in your next edit]`);
353
+ }
354
+
355
+ const summary = lines.join("\n");
356
+ await recordSaving(baseline, summary.length, "ashlr__multi_edit");
357
+ return summary;
358
+ }
359
+
360
+ // ---------------------------------------------------------------------------
361
+ // MCP wiring
362
+ // ---------------------------------------------------------------------------
363
+
364
+ const server = new Server(
365
+ { name: "ashlr-multi-edit", version: "0.1.0" },
366
+ { capabilities: { tools: {} } },
367
+ );
368
+
369
+ server.setRequestHandler(ListToolsRequestSchema, async () => ({
370
+ tools: [
371
+ {
372
+ name: "ashlr__multi_edit",
373
+ description:
374
+ "Atomic refactors across files — apply N edits in ONE roundtrip instead of N. " +
375
+ "Either ALL edits succeed or NONE are written (full rollback on any failure). " +
376
+ "Reads each target file once and writes it once regardless of how many edits target it. " +
377
+ "Use this instead of calling ashlr__edit N times for multi-file refactors; saves (N−1) × tool-call overhead and returns one consolidated diff summary.",
378
+ inputSchema: {
379
+ type: "object",
380
+ properties: {
381
+ edits: {
382
+ type: "array",
383
+ description: "Ordered list of edits to apply atomically.",
384
+ items: {
385
+ type: "object",
386
+ properties: {
387
+ path: { type: "string", description: "Absolute or cwd-relative file path. File must exist." },
388
+ search: { type: "string", description: "Exact string to find in the file." },
389
+ replace: { type: "string", description: "String to replace it with." },
390
+ strict: {
391
+ type: "boolean",
392
+ description: "Default true: require exactly one match (error if 0 or 2+). Pass false to replace all occurrences.",
393
+ },
394
+ },
395
+ required: ["path", "search", "replace"],
396
+ },
397
+ },
398
+ },
399
+ required: ["edits"],
400
+ },
401
+ },
402
+ ],
403
+ }));
404
+
405
+ server.setRequestHandler(CallToolRequestSchema, async (req) => {
406
+ const { name, arguments: args } = req.params;
407
+ try {
408
+ if (name === "ashlr__multi_edit") {
409
+ const text = await ashlrMultiEdit((args ?? {}) as unknown as MultiEditArgs);
410
+ return { content: [{ type: "text", text }] };
411
+ }
412
+ return { content: [{ type: "text", text: `Unknown tool: ${name}` }], isError: true };
413
+ } catch (err) {
414
+ const message = err instanceof Error ? err.message : String(err);
415
+ return { content: [{ type: "text", text: message }], isError: true };
416
+ }
417
+ });
418
+
419
+ if (import.meta.main) {
420
+ const transport = new StdioServerTransport();
421
+ await server.connect(transport);
422
+ }
@@ -0,0 +1,34 @@
1
+ /**
2
+ * notebook-edit-server-handlers — registers ashlr__notebook_edit into the shared tool registry.
3
+ */
4
+
5
+ import { registerTool, type ToolCallContext, type ToolResult } from "./_tool-base";
6
+ import { ashlrNotebookEdit, formatNotebookEditResult, type NotebookEditArgs } from "./notebook-edit-server";
7
+
8
+ registerTool({
9
+ name: "ashlr__notebook_edit",
10
+ description:
11
+ "Edit a single cell in a Jupyter notebook (.ipynb). Use instead of native NotebookEdit " +
12
+ "when the notebook is >5KB or has many unrelated cells — only the edited cell and its " +
13
+ "immediate neighbors are returned, with `[N cells unchanged]` for the rest. The full " +
14
+ "notebook is never echoed. Typical savings: 65-75% on multi-cell notebooks. " +
15
+ "Args: notebookPath, cellIndex, newSource, cellType (optional: code|markdown).",
16
+ inputSchema: {
17
+ type: "object",
18
+ properties: {
19
+ notebookPath: { type: "string", description: "Absolute or cwd-relative path to the .ipynb notebook" },
20
+ cellIndex: { type: "number", description: "Zero-based index of the cell to edit" },
21
+ newSource: { type: "string", description: "New source content for the cell" },
22
+ cellType: {
23
+ type: "string",
24
+ enum: ["code", "markdown"],
25
+ description: "Optional: change the cell type (code or markdown)",
26
+ },
27
+ },
28
+ required: ["notebookPath", "cellIndex", "newSource"],
29
+ },
30
+ handler: async (args: Record<string, unknown>, _ctx: ToolCallContext): Promise<ToolResult> => {
31
+ const result = await ashlrNotebookEdit(args as unknown as NotebookEditArgs);
32
+ return { content: [{ type: "text", text: formatNotebookEditResult(result) }] };
33
+ },
34
+ });