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,1136 @@
1
+ #!/usr/bin/env bun
2
+ /**
3
+ * ashlr-bash MCP server.
4
+ *
5
+ * Exposes ashlr__bash — a token-efficient replacement for Claude Code's
6
+ * built-in Bash tool. Auto-compresses long stdout, recognizes a handful of
7
+ * common commands and returns compact structured summaries instead of raw
8
+ * output, and refuses catastrophic patterns. stderr is never compressed —
9
+ * errors must reach the agent intact.
10
+ *
11
+ * Persists savings to ~/.ashlr/stats.json using the same schema as the
12
+ * efficiency server so the status line aggregates cleanly.
13
+ */
14
+
15
+ import { Server } from "@modelcontextprotocol/sdk/server/index.js";
16
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
17
+ import {
18
+ CallToolRequestSchema,
19
+ ListToolsRequestSchema,
20
+ } from "@modelcontextprotocol/sdk/types.js";
21
+ import { spawn, type ChildProcess } from "child_process";
22
+ import { existsSync } from "fs";
23
+ import {
24
+ summarizeGitStatus,
25
+ summarizeLs,
26
+ summarizeFind,
27
+ summarizePs,
28
+ summarizeNpmLs,
29
+ summarizeDockerPs,
30
+ summarizeKubectlGet,
31
+ summarizeNpmAudit,
32
+ findSummarizer,
33
+ isLargeDiffCommand,
34
+ DIFF_LLM_THRESHOLD_BYTES,
35
+ } from "./_bash-summarizers-registry.js";
36
+
37
+ /**
38
+ * Resolve the shell to use for executing commands.
39
+ * On Windows, use PowerShell (pwsh preferred, powershell fallback) with -Command.
40
+ * On POSIX, use $SHELL or /bin/sh.
41
+ * Returns [shellBin, shellArgs] — the command is appended to shellArgs.
42
+ */
43
+ function resolveShell(): [string, string[]] {
44
+ if (process.platform === "win32") {
45
+ const pwsh =
46
+ typeof (globalThis as { Bun?: { which(b: string): string | null } }).Bun !== "undefined"
47
+ ? (globalThis as { Bun: { which(b: string): string | null } }).Bun.which("pwsh") ??
48
+ (globalThis as { Bun: { which(b: string): string | null } }).Bun.which("powershell")
49
+ : null;
50
+ const bin = pwsh ?? "powershell";
51
+ return [bin, ["-NoProfile", "-NonInteractive", "-Command"]];
52
+ }
53
+ return [process.env.SHELL || "/bin/sh", ["-c"]];
54
+ }
55
+ import { mkdir, readFile, writeFile } from "fs/promises";
56
+ import { homedir } from "os";
57
+ import { dirname, join } from "path";
58
+ import { randomBytes } from "crypto";
59
+ import { summarizeIfLarge, PROMPTS, confidenceBadge, confidenceTier } from "./_summarize";
60
+ import { recordSaving as recordSavingCore } from "./_stats";
61
+ import { logEvent } from "./_events";
62
+ import { clampToCwd } from "./_cwd-clamp";
63
+
64
+ // Bash always records under the "ashlr__bash" tool bucket.
65
+ async function recordSaving(rawBytes: number, compactBytes: number): Promise<number> {
66
+ return recordSavingCore(rawBytes, compactBytes, "ashlr__bash");
67
+ }
68
+
69
+ // ---------------------------------------------------------------------------
70
+ // Compression
71
+ // ---------------------------------------------------------------------------
72
+
73
+ const COMPRESS_THRESHOLD = 2048;
74
+ // Head/tail sizes were tuned for the dominant case: build/test/install logs
75
+ // where the failing assertion or stack trace lands in the last few hundred
76
+ // bytes, while the head establishes which command/phase produced the output.
77
+ // 800 head + 800 tail keeps the structural signal on both ends and still
78
+ // fits comfortably under typical 2KB token budgets after framing.
79
+ const HEAD_BYTES = 800;
80
+ const TAIL_BYTES = 800;
81
+ // On a failing command (non-zero exit), the last 4KB almost certainly holds
82
+ // the error stack. We expand the tail so errors are never lost to elision.
83
+ const FAIL_TAIL_BYTES = 4096;
84
+
85
+ interface SnipOptions {
86
+ /** When true, emit a sharper warning that an error may be in the elided gap. */
87
+ errorAware?: boolean;
88
+ /** When true, expand tail capture so non-zero-exit errors aren't dropped. */
89
+ exitedNonZero?: boolean;
90
+ }
91
+
92
+ function snipBytes(s: string, opts: SnipOptions = {}): { out: string; saved: number } {
93
+ if (s.length <= COMPRESS_THRESHOLD) return { out: s, saved: 0 };
94
+ const tailBytes = opts.exitedNonZero ? FAIL_TAIL_BYTES : TAIL_BYTES;
95
+ // If expanding the tail would take us over the full length, just emit raw.
96
+ if (HEAD_BYTES + tailBytes >= s.length) return { out: s, saved: 0 };
97
+ const elided = s.length - HEAD_BYTES - tailBytes;
98
+ const warning = opts.errorAware
99
+ ? `\n[... ${elided.toLocaleString()} bytes elided · LLM summary unavailable · an error may be in this gap · pass bypassSummary:true for the full output ...]\n`
100
+ : `\n[... ${elided.toLocaleString()} bytes of output elided ...]\n`;
101
+ const out = s.slice(0, HEAD_BYTES) + warning + s.slice(-tailBytes);
102
+ return { out, saved: s.length - out.length };
103
+ }
104
+
105
+ // ---------------------------------------------------------------------------
106
+ // Safety
107
+ // ---------------------------------------------------------------------------
108
+
109
+ const DANGEROUS = [
110
+ /\brm\s+-rf?\s+\/(?:\s|$)/, // rm -rf /
111
+ /\brm\s+-rf?\s+\/\*/, // rm -rf /*
112
+ /\brm\s+-rf?\s+~(?:\s|$)/, // rm -rf ~
113
+ /\brm\s+-rf?\s+\$HOME(?:\s|$)/, // rm -rf $HOME
114
+ /:\(\)\s*\{\s*:\|\:&\s*\}\s*;\s*:/, // fork bomb
115
+ /\bmkfs(\.\w+)?\b/, // mkfs
116
+ /\bdd\s+if=.*of=\/dev\/[sh]d/, // dd to a raw disk
117
+ />\s*\/dev\/sda/, // pipe to raw disk
118
+ ];
119
+
120
+ function refusalReason(cmd: string): string | null {
121
+ const trimmed = cmd.trim();
122
+ for (const pat of DANGEROUS) {
123
+ if (pat.test(trimmed)) {
124
+ return `ashlr__bash refused: command matches catastrophic pattern (${pat}). Use Claude Code's built-in Bash if you really intended this.`;
125
+ }
126
+ }
127
+ return null;
128
+ }
129
+
130
+ // ---------------------------------------------------------------------------
131
+ // Command-specific summarizers
132
+ // ---------------------------------------------------------------------------
133
+
134
+ interface Summarized {
135
+ text: string;
136
+ rawBytes: number;
137
+ compactBytes: number;
138
+ }
139
+
140
+ function tryCatRefusal(cmd: string): string | null {
141
+ // Detect a leading `cat <file>` (single-file, not a pipe / heredoc / multi-arg).
142
+ const t = cmd.trim();
143
+ if (!/^cat\s+\S/.test(t)) return null;
144
+ if (/[|<>]/.test(t)) return null; // pipe/redirect — leave alone
145
+ const parts = t.split(/\s+/);
146
+ if (parts.length < 2) return null;
147
+ // Skip flags except common ones; if all non-flag args are present treat as cat.
148
+ const args = parts.slice(1).filter((a) => !a.startsWith("-"));
149
+ if (args.length === 0) return null;
150
+ return `ashlr__bash refused: use ashlr__read for file contents (path: ${args[0]}). It auto-compresses files > 2KB and tracks savings.`;
151
+ }
152
+
153
+ // Summarizer functions imported from ./_bash-summarizers-registry.js
154
+
155
+
156
+
157
+ async function tryStructuredSummary(
158
+ command: string,
159
+ stdout: string,
160
+ cwd: string,
161
+ ): Promise<string | null> {
162
+ const trimmed = command.trim();
163
+
164
+ // git status (porcelain or human) — branch and ahead/behind via separate call.
165
+ if (/^git\s+status(\s|$)/.test(trimmed)) {
166
+ let porcelain = stdout;
167
+ let branchHint: string | undefined;
168
+ if (!/--porcelain/.test(trimmed)) {
169
+ // Re-run a porcelain query to compute counts deterministically.
170
+ try {
171
+ const r = await runRaw("git status --porcelain", cwd, 5000);
172
+ porcelain = r.stdout;
173
+ } catch {
174
+ // Fall back to whatever we have.
175
+ }
176
+ }
177
+ try {
178
+ const b = await runRaw("git rev-parse --abbrev-ref HEAD", cwd, 5000);
179
+ branchHint = b.stdout.trim() || undefined;
180
+ } catch { /* ignore */ }
181
+ let summary = summarizeGitStatus(porcelain, branchHint);
182
+ try {
183
+ const ab = await runRaw("git rev-list --left-right --count @{u}...HEAD", cwd, 5000);
184
+ const m = ab.stdout.trim().match(/^(\d+)\s+(\d+)$/);
185
+ if (m) {
186
+ const behind = Number(m[1]);
187
+ const ahead = Number(m[2]);
188
+ if (ahead || behind) summary += ` · ahead ${ahead} behind ${behind}`;
189
+ }
190
+ } catch { /* no upstream — skip */ }
191
+ return summary;
192
+ }
193
+
194
+ // ls -la / ls -l
195
+ if (/^ls(\s+-\w+)*(\s|$)/.test(trimmed)) {
196
+ return summarizeLs(stdout);
197
+ }
198
+
199
+ // find . -name ...
200
+ if (/^find\s/.test(trimmed)) {
201
+ return summarizeFind(stdout);
202
+ }
203
+
204
+ // ps aux / ps -ef
205
+ if (/^ps\s+(aux|-ef)\b/.test(trimmed)) {
206
+ return summarizePs(stdout, cwd);
207
+ }
208
+
209
+ // npm ls / bun pm ls
210
+ if (/^(npm\s+ls|bun\s+pm\s+ls)\b/.test(trimmed)) {
211
+ return summarizeNpmLs(stdout);
212
+ }
213
+
214
+ // docker ps / docker-compose ps
215
+ if (/^(docker(-compose)?\s+ps|docker\s+container\s+ls)\b/.test(trimmed)) {
216
+ return summarizeDockerPs(stdout);
217
+ }
218
+
219
+ // kubectl get <resource>
220
+ if (/^kubectl\s+get\b/.test(trimmed)) {
221
+ return summarizeKubectlGet(stdout);
222
+ }
223
+
224
+ // npm audit (text or --json)
225
+ if (/^npm\s+audit\b/.test(trimmed)) {
226
+ return summarizeNpmAudit(stdout);
227
+ }
228
+
229
+ // Fallback: route through the v1.18 registry so new summarizers (git log,
230
+ // test runners, tsc, package installs) fire without per-command branches.
231
+ const fn = findSummarizer(trimmed);
232
+ if (fn) {
233
+ try {
234
+ const out = fn(stdout);
235
+ if (out !== null && out.length > 0) return out;
236
+ } catch { /* registry summarizers must never throw; fall through */ }
237
+ }
238
+
239
+ return null;
240
+ }
241
+
242
+ // ---------------------------------------------------------------------------
243
+ // Subprocess
244
+ // ---------------------------------------------------------------------------
245
+
246
+ interface RunResult {
247
+ stdout: string;
248
+ stderr: string;
249
+ exitCode: number | null;
250
+ signal: NodeJS.Signals | null;
251
+ durationMs: number;
252
+ timedOut: boolean;
253
+ }
254
+
255
+ function runRaw(command: string, cwd: string, timeoutMs: number): Promise<RunResult> {
256
+ return new Promise((resolveP) => {
257
+ const start = Date.now();
258
+ // Use resolveShell() for cross-platform shell selection.
259
+ // On Windows: PowerShell (pwsh/powershell) with -Command.
260
+ // On POSIX: $SHELL or /bin/sh with -c.
261
+ const [shell, shellArgs] = resolveShell();
262
+ // On POSIX, `detached: true` gives the child its own process group so we
263
+ // can later SIGKILL the whole group (`-pid`) instead of just the shell —
264
+ // otherwise forked grandchildren (npm install, cargo build, etc.) leak
265
+ // and keep running after timeout. Windows has no process groups, so we
266
+ // stick with the default spawn options and fall back to child.kill().
267
+ const isWin = process.platform === "win32";
268
+ const child = spawn(shell, [...shellArgs, command], {
269
+ cwd,
270
+ env: process.env,
271
+ stdio: ["ignore", "pipe", "pipe"],
272
+ detached: !isWin,
273
+ });
274
+ let stdout = "";
275
+ let stderr = "";
276
+ let timedOut = false;
277
+ const cap = 5 * 1024 * 1024; // 5MB hard cap to avoid OOM on runaway output
278
+ child.stdout.on("data", (b: Buffer) => {
279
+ if (stdout.length < cap) stdout += b.toString("utf-8");
280
+ });
281
+ child.stderr.on("data", (b: Buffer) => {
282
+ if (stderr.length < cap) stderr += b.toString("utf-8");
283
+ });
284
+ const timer = setTimeout(() => {
285
+ timedOut = true;
286
+ if (isWin) {
287
+ try { child.kill("SIGKILL"); } catch { /* already dead */ }
288
+ } else if (child.pid != null) {
289
+ // Negative pid => kill the process group. Wrapped in try/catch because
290
+ // the group may already be gone (exited between timer fire and kill).
291
+ try { process.kill(-child.pid, "SIGKILL"); } catch {
292
+ // Fall back to killing just the shell if the group call failed
293
+ // (e.g. ESRCH because the leader already reaped).
294
+ try { child.kill("SIGKILL"); } catch { /* already dead */ }
295
+ }
296
+ } else {
297
+ try { child.kill("SIGKILL"); } catch { /* already dead */ }
298
+ }
299
+ }, timeoutMs);
300
+ child.on("close", (code, signal) => {
301
+ clearTimeout(timer);
302
+ resolveP({
303
+ stdout,
304
+ stderr,
305
+ exitCode: code,
306
+ signal,
307
+ durationMs: Date.now() - start,
308
+ timedOut,
309
+ });
310
+ });
311
+ child.on("error", (err) => {
312
+ clearTimeout(timer);
313
+ resolveP({
314
+ stdout,
315
+ stderr: stderr + `\n[spawn error: ${err.message}]`,
316
+ exitCode: 127,
317
+ signal: null,
318
+ durationMs: Date.now() - start,
319
+ timedOut,
320
+ });
321
+ });
322
+ });
323
+ }
324
+
325
+ // ---------------------------------------------------------------------------
326
+ // Main tool
327
+ // ---------------------------------------------------------------------------
328
+
329
+ export interface BashArgs {
330
+ command: string;
331
+ cwd?: string;
332
+ timeout_ms?: number;
333
+ compact?: boolean;
334
+ bypassSummary?: boolean;
335
+ }
336
+
337
+ export async function ashlrBash(args: BashArgs): Promise<string> {
338
+ const command = args.command;
339
+ if (typeof command !== "string" || command.length === 0) {
340
+ return "ashlr__bash error: 'command' is required";
341
+ }
342
+ // Clamp the shell's working directory to process.cwd() so a prompt-injected
343
+ // caller can't pivot the shell to /, $HOME, or any ancestor — which would
344
+ // defeat the content-focused refusals below (tryCatRefusal only checks the
345
+ // command string, not the effective directory) and expose parent repos.
346
+ const clamp = clampToCwd(args.cwd, "ashlr__bash");
347
+ if (clamp.ok === false) return clamp.message;
348
+ const cwd = clamp.abs;
349
+ const timeoutCheck = validateNumber(args.timeout_ms, "timeout_ms", MIN_TIMEOUT_MS, MAX_TIMEOUT_MS);
350
+ if (timeoutCheck.ok === false) return `ashlr__bash error: ${timeoutCheck.message}`;
351
+ const timeoutMs = Number.isNaN(timeoutCheck.value) ? 60_000 : timeoutCheck.value;
352
+ const compact = args.compact !== false;
353
+
354
+ const refusal = refusalReason(command);
355
+ if (refusal) return `$ ${command}\n${refusal}`;
356
+
357
+ const catMsg = tryCatRefusal(command);
358
+ if (catMsg) return `$ ${command}\n${catMsg}`;
359
+
360
+ const res = await runRaw(command, cwd, timeoutMs);
361
+
362
+ const exitLabel = res.timedOut
363
+ ? `timed out after ${timeoutMs}ms (killed)`
364
+ : res.signal
365
+ ? `killed by ${res.signal}`
366
+ : `exit ${res.exitCode}`;
367
+
368
+ const rawStdoutBytes = res.stdout.length;
369
+ let body: string;
370
+ let compactBytes: number;
371
+ let savedNote = "";
372
+ let structuredSummaryFired = false;
373
+
374
+ if (compact) {
375
+ const structured = await tryStructuredSummary(command, res.stdout, cwd);
376
+ if (structured !== null) {
377
+ body = structured;
378
+ compactBytes = body.length;
379
+ structuredSummaryFired = true;
380
+ } else if (isLargeDiffCommand(command) && rawStdoutBytes > DIFF_LLM_THRESHOLD_BYTES && !args.bypassSummary) {
381
+ // git diff / git show — route through the diff-specific prompt at a
382
+ // lower threshold (4 KB). Diffs are denser than generic bash output.
383
+ const s = await summarizeIfLarge(res.stdout, {
384
+ toolName: "ashlr__bash",
385
+ systemPrompt: PROMPTS.diff,
386
+ bypass: false,
387
+ });
388
+ if (s.summarized || s.fellBack) {
389
+ body = s.text;
390
+ compactBytes = s.outputBytes;
391
+ } else {
392
+ body = res.stdout;
393
+ compactBytes = rawStdoutBytes;
394
+ }
395
+ } else if (rawStdoutBytes > 16_384 && !args.bypassSummary) {
396
+ // Try LLM summarization on the RAW stdout for large pass-through output.
397
+ // Falls back to snipBytes truncation if the LLM is unreachable / declined.
398
+ const s = await summarizeIfLarge(res.stdout, {
399
+ toolName: "ashlr__bash",
400
+ systemPrompt: PROMPTS.bash,
401
+ bypass: false,
402
+ });
403
+ if (s.summarized || s.fellBack) {
404
+ body = s.text;
405
+ compactBytes = s.outputBytes;
406
+ } else {
407
+ // LLM failed AND we're over the threshold — warn loudly, since the
408
+ // most likely scenario is that an error is hiding in the elided gap.
409
+ const exitedNonZero = res.exitCode != null && res.exitCode !== 0;
410
+ const { out, saved } = snipBytes(res.stdout, {
411
+ errorAware: true,
412
+ exitedNonZero,
413
+ });
414
+ if (exitedNonZero && saved > 0) {
415
+ await logEvent("tool_escalate", {
416
+ tool: "ashlr__bash",
417
+ reason: "nonzero-exit-elided",
418
+ extra: { exitCode: res.exitCode },
419
+ });
420
+ }
421
+ body = out;
422
+ compactBytes = out.length;
423
+ if (saved > 0) savedNote = ` · [compact saved ${saved.toLocaleString()} bytes]`;
424
+ }
425
+ } else {
426
+ // Baseline snip path — widen tail on non-zero exit so errors never drop.
427
+ const { out, saved } = snipBytes(res.stdout, {
428
+ exitedNonZero: res.exitCode != null && res.exitCode !== 0,
429
+ });
430
+ body = out;
431
+ compactBytes = out.length;
432
+ if (saved > 0) savedNote = ` · [compact saved ${saved.toLocaleString()} bytes]`;
433
+ }
434
+ } else {
435
+ body = res.stdout;
436
+ compactBytes = res.stdout.length;
437
+ }
438
+
439
+ // Track savings for the stdout side only (stderr is not compressed).
440
+ if (compact && rawStdoutBytes > 0) {
441
+ await recordSaving(rawStdoutBytes, compactBytes);
442
+ if (!savedNote && rawStdoutBytes > compactBytes) {
443
+ savedNote = ` · [compact saved ${(rawStdoutBytes - compactBytes).toLocaleString()} bytes]`;
444
+ } else if (!savedNote) {
445
+ savedNote = " · [compact saved 0 bytes]";
446
+ }
447
+ }
448
+
449
+ // stderr passes through uncompressed and labeled.
450
+ const stderrBlock = res.stderr.length > 0
451
+ ? `\n--- stderr ---\n${res.stderr}`
452
+ : "";
453
+
454
+ const trailer = `· ${exitLabel} · ${res.durationMs}ms${savedNote}`;
455
+ // Compose: command echo, body, optional stderr, trailer.
456
+ const composed = `$ ${command}\n${body}${body.endsWith("\n") || body.length === 0 ? "" : "\n"}${stderrBlock}${stderrBlock.endsWith("\n") ? "" : "\n"}${trailer}`;
457
+
458
+ // Append confidence badge only when compression actually fired.
459
+ if (compact && rawStdoutBytes > compactBytes) {
460
+ const exitedNonZero = res.exitCode != null && res.exitCode !== 0;
461
+ const bashBadgeOpts = {
462
+ toolName: "ashlr__bash",
463
+ rawBytes: rawStdoutBytes,
464
+ outputBytes: compactBytes,
465
+ fellBack: !structuredSummaryFired && rawStdoutBytes > 16_384 && compactBytes < rawStdoutBytes,
466
+ nonZeroExit: exitedNonZero && rawStdoutBytes > compactBytes,
467
+ };
468
+ if (confidenceTier(bashBadgeOpts) === "low") {
469
+ await logEvent("tool_low_confidence_shipped", { tool: "ashlr__bash", reason: "low-confidence" });
470
+ }
471
+ return composed + confidenceBadge(bashBadgeOpts);
472
+ }
473
+ return composed;
474
+ }
475
+
476
+ // ---------------------------------------------------------------------------
477
+ // Background sessions (tail mode)
478
+ // ---------------------------------------------------------------------------
479
+
480
+ interface Session {
481
+ id: string;
482
+ pid: number;
483
+ command: string;
484
+ cwd: string;
485
+ startedAt: number;
486
+ proc: ChildProcess | null;
487
+ // tracked separately because ChildProcess stdin/stdout/stderr may be null
488
+ // depending on stdio config — our spawn always uses pipes, but the narrow
489
+ // type is awkward to express.
490
+ stdout: string;
491
+ stderr: string;
492
+ offset: number; // bytes of stdout the agent has already seen
493
+ stderrOffset: number;
494
+ cumulativeBytes: number;
495
+ exitCode: number | null;
496
+ signal: NodeJS.Signals | null;
497
+ timeoutTimer: ReturnType<typeof setTimeout> | null;
498
+ lastActivity: number;
499
+ dataEmitter: Set<() => void>; // wait_ms listeners
500
+ byteCapExceeded: boolean;
501
+ }
502
+
503
+ const SESSIONS = new Map<string, Session>();
504
+ // Prefer $HOME when explicitly set (matches the rest of the ashlr codebase).
505
+ // On Windows `homedir()` reads USERPROFILE, which causes tests that set HOME
506
+ // to leak into the real user profile; honoring $HOME keeps behavior uniform.
507
+ const ASHLR_HOME = process.env.HOME ?? homedir();
508
+ const SESSIONS_PATH = join(ASHLR_HOME, ".ashlr", "bash-sessions.json");
509
+ const MAX_SESSIONS = 16;
510
+ const MAX_CUMULATIVE_BYTES = 50 * 1024 * 1024;
511
+ const DEFAULT_START_TIMEOUT_MS = 300_000;
512
+ const MIN_TIMEOUT_MS = 100;
513
+ const MAX_TIMEOUT_MS = 600_000;
514
+ const MIN_WAIT_MS = 0;
515
+ const MAX_WAIT_MS = 30_000;
516
+ const MIN_TAIL_BYTES = 1;
517
+ const MAX_TAIL_BYTES = 1_000_000;
518
+ const IS_WINDOWS = process.platform === "win32";
519
+
520
+ function validateNumber(
521
+ value: unknown,
522
+ name: string,
523
+ min: number,
524
+ max: number,
525
+ ): { ok: true; value: number } | { ok: false; message: string } {
526
+ if (value === undefined) return { ok: true, value: NaN };
527
+ if (typeof value !== "number" || !Number.isFinite(value)) {
528
+ return { ok: false, message: `${name} must be a finite number` };
529
+ }
530
+ if (value < min || value > max) {
531
+ return { ok: false, message: `${name} must be between ${min} and ${max}` };
532
+ }
533
+ return { ok: true, value: Math.floor(value) };
534
+ }
535
+
536
+ // ---------------------------------------------------------------------------
537
+ // External session providers (test-watch, etc.)
538
+ //
539
+ // Other modules can plug into the bash-session tool surface so ashlr__bash_list
540
+ // enumerates their sessions, and ashlr__bash_tail / ashlr__bash_stop route to
541
+ // them by id. This keeps one tool surface for all background work without
542
+ // coupling bash-server to test-watch internals.
543
+ // ---------------------------------------------------------------------------
544
+
545
+ export interface ExternalSession {
546
+ id: string;
547
+ pid: number;
548
+ command: string;
549
+ cwd: string;
550
+ startedAt: number;
551
+ cumulativeBytes: number;
552
+ kind: string;
553
+ }
554
+
555
+ export interface ExternalSessionProvider {
556
+ kind: string;
557
+ list(): ExternalSession[];
558
+ tail(id: string, waitMs: number, maxBytes: number): Promise<string | null>;
559
+ stop(id: string, signal?: NodeJS.Signals): string | null;
560
+ has(id: string): boolean;
561
+ }
562
+
563
+ const EXTERNAL_PROVIDERS: ExternalSessionProvider[] = [];
564
+
565
+ export function registerExternalSessions(provider: ExternalSessionProvider): void {
566
+ // Replace any previously-registered provider of the same kind (idempotent
567
+ // on re-import during tests).
568
+ const idx = EXTERNAL_PROVIDERS.findIndex((p) => p.kind === provider.kind);
569
+ if (idx >= 0) EXTERNAL_PROVIDERS[idx] = provider;
570
+ else EXTERNAL_PROVIDERS.push(provider);
571
+ }
572
+
573
+ function findExternalProvider(id: string): ExternalSessionProvider | null {
574
+ for (const p of EXTERNAL_PROVIDERS) {
575
+ if (p.has(id)) return p;
576
+ }
577
+ return null;
578
+ }
579
+
580
+ interface PersistedSession {
581
+ id: string;
582
+ pid: number;
583
+ command: string;
584
+ cwd: string;
585
+ startedAt: number;
586
+ exitCode: number | null;
587
+ cumulativeBytes: number;
588
+ }
589
+
590
+ function pidAlive(pid: number): boolean {
591
+ if (!pid || pid <= 0) return false;
592
+ try {
593
+ process.kill(pid, 0);
594
+ return true;
595
+ } catch {
596
+ return false;
597
+ }
598
+ }
599
+
600
+ async function persistSessions(): Promise<void> {
601
+ await mkdir(dirname(SESSIONS_PATH), { recursive: true });
602
+ const payload: PersistedSession[] = [];
603
+ for (const s of SESSIONS.values()) {
604
+ payload.push({
605
+ id: s.id,
606
+ pid: s.pid,
607
+ command: s.command,
608
+ cwd: s.cwd,
609
+ startedAt: s.startedAt,
610
+ exitCode: s.exitCode,
611
+ cumulativeBytes: s.cumulativeBytes,
612
+ });
613
+ }
614
+ await writeFile(SESSIONS_PATH, JSON.stringify(payload, null, 2));
615
+ }
616
+
617
+ /**
618
+ * Load any persisted sessions from a prior server run. We cannot reattach
619
+ * to the stdio of a process we didn't spawn, so we only restore metadata
620
+ * for PIDs that are still alive. Dead PIDs are cleaned up (file pruned).
621
+ * Live "zombie" sessions have no proc — tail will report that output is
622
+ * no longer capturable but the process is still running.
623
+ */
624
+ async function reloadSessions(): Promise<void> {
625
+ if (!existsSync(SESSIONS_PATH)) return;
626
+ let raw: PersistedSession[];
627
+ try {
628
+ raw = JSON.parse(await readFile(SESSIONS_PATH, "utf-8"));
629
+ } catch {
630
+ return;
631
+ }
632
+ for (const p of raw) {
633
+ if (!pidAlive(p.pid)) continue; // dead PID — prune
634
+ SESSIONS.set(p.id, {
635
+ id: p.id,
636
+ pid: p.pid,
637
+ command: p.command,
638
+ cwd: p.cwd,
639
+ startedAt: p.startedAt,
640
+ proc: null, // cannot reattach streams
641
+ stdout: "",
642
+ stderr: "",
643
+ offset: 0,
644
+ stderrOffset: 0,
645
+ cumulativeBytes: p.cumulativeBytes,
646
+ exitCode: p.exitCode,
647
+ signal: null,
648
+ timeoutTimer: null,
649
+ lastActivity: Date.now(),
650
+ dataEmitter: new Set(),
651
+ byteCapExceeded: false,
652
+ });
653
+ }
654
+ await persistSessions();
655
+ }
656
+
657
+ function newId(): string {
658
+ return randomBytes(4).toString("hex");
659
+ }
660
+
661
+ function signalProcessTree(
662
+ pid: number,
663
+ child: ChildProcess | null,
664
+ signal: NodeJS.Signals,
665
+ ): boolean {
666
+ if (IS_WINDOWS) {
667
+ if (child) {
668
+ try {
669
+ return child.kill(signal);
670
+ } catch {
671
+ return false;
672
+ }
673
+ }
674
+ try {
675
+ process.kill(pid, signal);
676
+ return true;
677
+ } catch {
678
+ return false;
679
+ }
680
+ }
681
+
682
+ if (pid > 0) {
683
+ // Background bash sessions are spawned detached on POSIX, so their pid is
684
+ // also the process-group id. Fall back to the bare pid for older restored
685
+ // sessions that may have been started before process-group cleanup existed.
686
+ try {
687
+ process.kill(-pid, signal);
688
+ return true;
689
+ } catch {
690
+ try {
691
+ process.kill(pid, signal);
692
+ return true;
693
+ } catch {
694
+ // Fall through to ChildProcess.kill below.
695
+ }
696
+ }
697
+ }
698
+
699
+ if (child) {
700
+ try {
701
+ return child.kill(signal);
702
+ } catch {
703
+ return false;
704
+ }
705
+ }
706
+ return false;
707
+ }
708
+
709
+ function killForByteCap(session: Session, child: ChildProcess): void {
710
+ if (session.byteCapExceeded) return;
711
+ session.byteCapExceeded = true;
712
+ signalProcessTree(session.pid, child, "SIGKILL");
713
+ session.stderr += `\n[ashlr: session exceeded ${MAX_CUMULATIVE_BYTES} bytes - killed]\n`;
714
+ }
715
+
716
+ function gcOldestInactive(): void {
717
+ if (SESSIONS.size < MAX_SESSIONS) return;
718
+ // Prefer evicting an already-exited session; else evict the one with the
719
+ // oldest lastActivity.
720
+ const entries = [...SESSIONS.values()];
721
+ const exited = entries.filter((s) => s.exitCode !== null);
722
+ const candidates = exited.length > 0 ? exited : entries;
723
+ candidates.sort((a, b) => a.lastActivity - b.lastActivity);
724
+ const victim = candidates[0];
725
+ if (victim) {
726
+ signalProcessTree(victim.pid, victim.proc, "SIGKILL");
727
+ if (victim.timeoutTimer) clearTimeout(victim.timeoutTimer);
728
+ SESSIONS.delete(victim.id);
729
+ }
730
+ }
731
+
732
+ export interface StartArgs {
733
+ command: string;
734
+ cwd?: string;
735
+ timeout_ms?: number;
736
+ }
737
+
738
+ export async function ashlrBashStart(args: StartArgs): Promise<string> {
739
+ const command = args.command;
740
+ if (typeof command !== "string" || command.length === 0) {
741
+ return "ashlr__bash_start error: 'command' is required";
742
+ }
743
+ const clamp = clampToCwd(args.cwd, "ashlr__bash_start");
744
+ if (clamp.ok === false) return clamp.message;
745
+ const cwd = clamp.abs;
746
+ const timeoutCheck = validateNumber(args.timeout_ms, "timeout_ms", MIN_TIMEOUT_MS, MAX_TIMEOUT_MS);
747
+ if (timeoutCheck.ok === false) return `ashlr__bash_start error: ${timeoutCheck.message}`;
748
+ const timeoutMs = Number.isNaN(timeoutCheck.value) ? DEFAULT_START_TIMEOUT_MS : timeoutCheck.value;
749
+
750
+ const refusal = refusalReason(command);
751
+ if (refusal) return `[refused] ${refusal}`;
752
+
753
+ gcOldestInactive();
754
+
755
+ const id = newId();
756
+ const [shell, shellArgs] = resolveShell();
757
+ const child = spawn(shell, [...shellArgs, command], {
758
+ cwd,
759
+ env: process.env,
760
+ stdio: ["ignore", "pipe", "pipe"],
761
+ detached: !IS_WINDOWS,
762
+ }) as ChildProcess;
763
+ if (!child.stdout || !child.stderr) {
764
+ return `[start] spawn failed: no stdio pipes`;
765
+ }
766
+
767
+ const session: Session = {
768
+ id,
769
+ pid: child.pid ?? -1,
770
+ command,
771
+ cwd,
772
+ startedAt: Date.now(),
773
+ proc: child,
774
+ stdout: "",
775
+ stderr: "",
776
+ offset: 0,
777
+ stderrOffset: 0,
778
+ cumulativeBytes: 0,
779
+ exitCode: null,
780
+ signal: null,
781
+ timeoutTimer: null,
782
+ lastActivity: Date.now(),
783
+ dataEmitter: new Set(),
784
+ byteCapExceeded: false,
785
+ };
786
+
787
+ child.stdout.on("data", (b: Buffer) => {
788
+ const s = b.toString("utf-8");
789
+ session.stdout += s;
790
+ session.cumulativeBytes += b.length;
791
+ session.lastActivity = Date.now();
792
+ if (session.cumulativeBytes > MAX_CUMULATIVE_BYTES) {
793
+ killForByteCap(session, child);
794
+ }
795
+ for (const fn of [...session.dataEmitter]) fn();
796
+ });
797
+ child.stderr.on("data", (b: Buffer) => {
798
+ session.stderr += b.toString("utf-8");
799
+ session.cumulativeBytes += b.length;
800
+ session.lastActivity = Date.now();
801
+ if (session.cumulativeBytes > MAX_CUMULATIVE_BYTES) {
802
+ killForByteCap(session, child);
803
+ }
804
+ for (const fn of [...session.dataEmitter]) fn();
805
+ });
806
+ child.on("close", (code, signal) => {
807
+ session.exitCode = code;
808
+ session.signal = signal;
809
+ if (session.timeoutTimer) clearTimeout(session.timeoutTimer);
810
+ session.timeoutTimer = null;
811
+ session.lastActivity = Date.now();
812
+ for (const fn of [...session.dataEmitter]) fn();
813
+ persistSessions().catch(() => { /* best-effort */ });
814
+ });
815
+ child.on("error", (err) => {
816
+ session.stderr += `\n[spawn error: ${err.message}]\n`;
817
+ session.exitCode = 127;
818
+ for (const fn of [...session.dataEmitter]) fn();
819
+ });
820
+
821
+ session.timeoutTimer = setTimeout(() => {
822
+ if (session.exitCode === null) {
823
+ signalProcessTree(session.pid, child, "SIGKILL");
824
+ }
825
+ }, timeoutMs);
826
+
827
+ SESSIONS.set(id, session);
828
+ await persistSessions();
829
+
830
+ return `[started] id=${id} · pid=${session.pid} · $ ${command}`;
831
+ }
832
+
833
+ export interface TailArgs {
834
+ id: string;
835
+ max_bytes?: number;
836
+ wait_ms?: number;
837
+ }
838
+
839
+ function sleep(ms: number): Promise<void> {
840
+ return new Promise((r) => setTimeout(r, ms));
841
+ }
842
+
843
+ export async function ashlrBashTail(args: TailArgs): Promise<string> {
844
+ const id = args.id;
845
+ const maxBytesCheck = validateNumber(args.max_bytes, "max_bytes", MIN_TAIL_BYTES, MAX_TAIL_BYTES);
846
+ if (maxBytesCheck.ok === false) return `ashlr__bash_tail error: ${maxBytesCheck.message}`;
847
+ const waitCheck = validateNumber(args.wait_ms, "wait_ms", MIN_WAIT_MS, MAX_WAIT_MS);
848
+ if (waitCheck.ok === false) return `ashlr__bash_tail error: ${waitCheck.message}`;
849
+ const maxBytes = Number.isNaN(maxBytesCheck.value) ? 2048 : maxBytesCheck.value;
850
+ const waitMs = Number.isNaN(waitCheck.value) ? 1500 : waitCheck.value;
851
+
852
+ // Route to external provider (e.g. test-watch) if it owns this id.
853
+ const ext = findExternalProvider(id);
854
+ if (ext) {
855
+ const out = await ext.tail(id, waitMs, maxBytes);
856
+ if (out !== null) return out;
857
+ }
858
+
859
+ const s = SESSIONS.get(id);
860
+ if (!s) return `[tail] unknown id: ${id}`;
861
+
862
+ const hasNew = () =>
863
+ s.stdout.length > s.offset ||
864
+ s.stderr.length > s.stderrOffset ||
865
+ s.exitCode !== null;
866
+
867
+ if (!hasNew() && waitMs > 0) {
868
+ // Event-driven wait: install a one-shot listener that resolves when new
869
+ // output arrives or the process exits. Fall back to a timeout so we
870
+ // always honor wait_ms as a ceiling.
871
+ await new Promise<void>((resolve) => {
872
+ let done = false;
873
+ const finish = () => {
874
+ if (done) return;
875
+ done = true;
876
+ s.dataEmitter.delete(notify);
877
+ clearTimeout(t);
878
+ resolve();
879
+ };
880
+ const notify = () => finish();
881
+ const t = setTimeout(finish, waitMs);
882
+ s.dataEmitter.add(notify);
883
+ // Race guard — if data landed between the initial check and now.
884
+ if (hasNew()) finish();
885
+ });
886
+ }
887
+
888
+ const newStdout = s.stdout.slice(s.offset);
889
+ const newStderr = s.stderr.slice(s.stderrOffset);
890
+ const newBytes = newStdout.length;
891
+ s.offset = s.stdout.length;
892
+ s.stderrOffset = s.stderr.length;
893
+ s.lastActivity = Date.now();
894
+
895
+ // Compress the stdout tail window when it exceeds threshold.
896
+ let body: string;
897
+ if (newStdout.length > COMPRESS_THRESHOLD) {
898
+ const { out } = snipBytes(newStdout);
899
+ body = out;
900
+ } else if (newStdout.length > maxBytes) {
901
+ // Respect caller's max_bytes: keep trailing portion.
902
+ body = `[... ${(newStdout.length - maxBytes).toLocaleString()} bytes elided ...]\n` +
903
+ newStdout.slice(-maxBytes);
904
+ } else {
905
+ body = newStdout;
906
+ }
907
+
908
+ const exited = s.exitCode !== null || s.signal !== null;
909
+ const statusLabel = exited
910
+ ? s.signal
911
+ ? `exit ${s.exitCode ?? "?"} · signal ${s.signal}`
912
+ : `exit ${s.exitCode}`
913
+ : "running";
914
+ const header = `[${id} · pid ${s.pid} · ${statusLabel}] +${newBytes} bytes since last poll`;
915
+ const stderrBlock = newStderr.length > 0 ? `\n--- stderr ---\n${newStderr}` : "";
916
+ const output = `${header}\n${body}${body.endsWith("\n") || body.length === 0 ? "" : "\n"}${stderrBlock}`;
917
+
918
+ if (exited) {
919
+ // Drop the session after reporting the final tail.
920
+ if (s.timeoutTimer) clearTimeout(s.timeoutTimer);
921
+ SESSIONS.delete(id);
922
+ await persistSessions();
923
+ }
924
+ return output;
925
+ }
926
+
927
+ export interface StopArgs {
928
+ id: string;
929
+ signal?: string;
930
+ }
931
+
932
+ export async function ashlrBashStop(args: StopArgs): Promise<string> {
933
+ const id = args.id;
934
+ const signal = (args.signal ?? "SIGTERM") as NodeJS.Signals;
935
+ // Route to external provider (e.g. test-watch) if it owns this id.
936
+ const ext = findExternalProvider(id);
937
+ if (ext) {
938
+ const out = ext.stop(id, signal);
939
+ if (out !== null) return out;
940
+ }
941
+ const s = SESSIONS.get(id);
942
+ if (!s) return `[stop] unknown id: ${id}`;
943
+
944
+ if (s.exitCode !== null) {
945
+ SESSIONS.delete(id);
946
+ await persistSessions();
947
+ return `[${id}] already exited · exit ${s.exitCode}`;
948
+ }
949
+
950
+ signalProcessTree(s.pid, s.proc, signal);
951
+
952
+ // Wait up to 2s; if still alive, SIGKILL.
953
+ const deadline = Date.now() + 2000;
954
+ while (Date.now() < deadline && s.exitCode === null && (s.proc || pidAlive(s.pid))) {
955
+ await sleep(50);
956
+ }
957
+ if (s.exitCode === null && (s.proc || pidAlive(s.pid))) {
958
+ signalProcessTree(s.pid, s.proc, "SIGKILL");
959
+ // Give the close handler a tick.
960
+ const kd = Date.now() + 500;
961
+ while (Date.now() < kd && s.exitCode === null && (s.proc || pidAlive(s.pid))) await sleep(25);
962
+ }
963
+
964
+ const exitLabel = s.signal
965
+ ? `signal ${s.signal}`
966
+ : s.exitCode !== null
967
+ ? `exit ${s.exitCode}`
968
+ : `signal ${signal}`;
969
+ if (s.timeoutTimer) clearTimeout(s.timeoutTimer);
970
+ SESSIONS.delete(id);
971
+ await persistSessions();
972
+ return `[${id}] stopped · ${exitLabel}`;
973
+ }
974
+
975
+ export async function ashlrBashList(): Promise<string> {
976
+ // Collect both native bash sessions and any external providers (test-watch).
977
+ interface Row {
978
+ id: string;
979
+ pid: number;
980
+ startedAt: number;
981
+ cumulativeBytes: number;
982
+ command: string;
983
+ kind: string;
984
+ }
985
+ const rowsData: Row[] = [];
986
+ for (const s of SESSIONS.values()) {
987
+ rowsData.push({
988
+ id: s.id,
989
+ pid: s.pid,
990
+ startedAt: s.startedAt,
991
+ cumulativeBytes: s.cumulativeBytes,
992
+ command: s.command,
993
+ kind: "bash",
994
+ });
995
+ }
996
+ for (const p of EXTERNAL_PROVIDERS) {
997
+ for (const e of p.list()) {
998
+ rowsData.push({
999
+ id: e.id,
1000
+ pid: e.pid,
1001
+ startedAt: e.startedAt,
1002
+ cumulativeBytes: e.cumulativeBytes,
1003
+ command: e.command,
1004
+ kind: e.kind,
1005
+ });
1006
+ }
1007
+ }
1008
+
1009
+ if (rowsData.length === 0) return "[list] no active sessions";
1010
+
1011
+ const rows: string[] = [
1012
+ "id | kind | pid | started | bytes | command",
1013
+ "---------+------------+---------+-------------------+-----------+--------",
1014
+ ];
1015
+ for (const r of rowsData) {
1016
+ const started = new Date(r.startedAt).toISOString().replace("T", " ").slice(0, 19);
1017
+ const cmd = r.command.length > 40 ? r.command.slice(0, 37) + "..." : r.command;
1018
+ rows.push(
1019
+ `${r.id.padEnd(8)} | ${r.kind.padEnd(10)} | ${String(r.pid).padEnd(7)} | ${started} | ${String(r.cumulativeBytes).padEnd(9)} | ${cmd}`,
1020
+ );
1021
+ }
1022
+ return rows.join("\n");
1023
+ }
1024
+
1025
+ // ---------------------------------------------------------------------------
1026
+ // MCP server wiring
1027
+ // ---------------------------------------------------------------------------
1028
+
1029
+ const server = new Server(
1030
+ { name: "ashlr-bash", version: "0.1.0" },
1031
+ { capabilities: { tools: {} } },
1032
+ );
1033
+
1034
+ server.setRequestHandler(ListToolsRequestSchema, async () => ({
1035
+ tools: [
1036
+ {
1037
+ name: "ashlr__bash",
1038
+ description:
1039
+ "Run a shell command. Auto-compresses stdout > 2KB (head + tail with elided middle), and emits compact structured summaries for common commands (git status, ls, find, ps, npm ls). stderr is never compressed. Refuses catastrophic patterns and `cat <file>` (use ashlr__read instead). Lower-token alternative to the built-in Bash tool.",
1040
+ inputSchema: {
1041
+ type: "object",
1042
+ properties: {
1043
+ command: { type: "string", description: "Shell command to run" },
1044
+ cwd: { type: "string", description: "Working directory (default: process.cwd())" },
1045
+ timeout_ms: { type: "number", description: "Kill after N ms (100..600000; default 60000)" },
1046
+ compact: { type: "boolean", description: "Auto-compress long output (default true)" },
1047
+ bypassSummary: { type: "boolean", description: "Skip LLM summarization of long output" },
1048
+ },
1049
+ required: ["command"],
1050
+ },
1051
+ },
1052
+ {
1053
+ name: "ashlr__bash_start",
1054
+ description:
1055
+ "Spawn a long-running shell command in the background and return a session id. Use ashlr__bash_tail to poll incremental output, ashlr__bash_stop to kill, ashlr__bash_list to enumerate. Ideal for watchers, tails, long builds, and anything where streaming >> one-shot.",
1056
+ inputSchema: {
1057
+ type: "object",
1058
+ properties: {
1059
+ command: { type: "string", description: "Shell command to run" },
1060
+ cwd: { type: "string", description: "Working directory (default: process.cwd())" },
1061
+ timeout_ms: { type: "number", description: "Max lifetime before SIGKILL (100..600000; default 300000 = 5min)" },
1062
+ },
1063
+ required: ["command"],
1064
+ },
1065
+ },
1066
+ {
1067
+ name: "ashlr__bash_tail",
1068
+ description:
1069
+ "Read new stdout/stderr since the last poll for a background session. Auto-compresses the tail window when > 2KB. If wait_ms > 0, blocks (event-driven) until output arrives or the process exits, up to that ceiling.",
1070
+ inputSchema: {
1071
+ type: "object",
1072
+ properties: {
1073
+ id: { type: "string", description: "Session id from ashlr__bash_start" },
1074
+ max_bytes: { type: "number", description: "Tail window size (1..1000000; default 2048)" },
1075
+ wait_ms: { type: "number", description: "Block up to N ms for new output (0..30000; default 1500; 0 = return immediately)" },
1076
+ },
1077
+ required: ["id"],
1078
+ },
1079
+ },
1080
+ {
1081
+ name: "ashlr__bash_stop",
1082
+ description:
1083
+ "Kill a background session by id. Sends SIGTERM (configurable) and escalates to SIGKILL after 2s if still alive.",
1084
+ inputSchema: {
1085
+ type: "object",
1086
+ properties: {
1087
+ id: { type: "string" },
1088
+ signal: { type: "string", description: "Default SIGTERM; escalates to SIGKILL after 2s" },
1089
+ },
1090
+ required: ["id"],
1091
+ },
1092
+ },
1093
+ {
1094
+ name: "ashlr__bash_list",
1095
+ description: "List active background sessions: id | pid | started | cumulative bytes | command.",
1096
+ inputSchema: { type: "object", properties: {} },
1097
+ },
1098
+ ],
1099
+ }));
1100
+
1101
+ server.setRequestHandler(CallToolRequestSchema, async (req) => {
1102
+ const { name, arguments: args } = req.params;
1103
+ try {
1104
+ if (name === "ashlr__bash") {
1105
+ const text = await ashlrBash(args as unknown as BashArgs);
1106
+ return { content: [{ type: "text", text }] };
1107
+ }
1108
+ if (name === "ashlr__bash_start") {
1109
+ const text = await ashlrBashStart(args as unknown as StartArgs);
1110
+ return { content: [{ type: "text", text }] };
1111
+ }
1112
+ if (name === "ashlr__bash_tail") {
1113
+ const text = await ashlrBashTail(args as unknown as TailArgs);
1114
+ return { content: [{ type: "text", text }] };
1115
+ }
1116
+ if (name === "ashlr__bash_stop") {
1117
+ const text = await ashlrBashStop(args as unknown as StopArgs);
1118
+ return { content: [{ type: "text", text }] };
1119
+ }
1120
+ if (name === "ashlr__bash_list") {
1121
+ const text = await ashlrBashList();
1122
+ return { content: [{ type: "text", text }] };
1123
+ }
1124
+ return { content: [{ type: "text", text: `Unknown tool: ${name}` }], isError: true };
1125
+ } catch (err) {
1126
+ const message = err instanceof Error ? err.message : String(err);
1127
+ return { content: [{ type: "text", text: `ashlr__bash error: ${message}` }], isError: true };
1128
+ }
1129
+ });
1130
+
1131
+ await reloadSessions();
1132
+
1133
+ if (import.meta.main) {
1134
+ const transport = new StdioServerTransport();
1135
+ await server.connect(transport);
1136
+ }