@asrunjs/asrun 0.0.0-stage → 0.5.2

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 (305) hide show
  1. package/.claude/commands/explore-case.md +18 -0
  2. package/.claude/commands/gen-spec.md +16 -0
  3. package/.claude/commands/heal-spec.md +26 -0
  4. package/.claude/commands/make-case.md +11 -0
  5. package/.claude/settings.json +5 -0
  6. package/.claude/skills/exploring-cases/SKILL.md +146 -0
  7. package/.claude/skills/exploring-cases/references/assertion-targets.md +103 -0
  8. package/.claude/skills/exploring-cases/references/handoff-card.js +128 -0
  9. package/.claude/skills/exploring-cases/references/open-questions.md +116 -0
  10. package/.claude/skills/exploring-cases/references/readiness.md +146 -0
  11. package/.claude/skills/exploring-cases/references/user-handoff.md +157 -0
  12. package/.claude/skills/generating-specs/SKILL.md +219 -0
  13. package/.claude/skills/generating-specs/references/conventions.md +148 -0
  14. package/.claude/skills/making-cases/SKILL.md +72 -0
  15. package/.claude/skills/making-cases/references/gaps.md +36 -0
  16. package/.claude/skills/making-cases/references/normalizing.md +39 -0
  17. package/.claude/skills/making-cases/references/splitting.md +28 -0
  18. package/.claude/skills/making-cases/references/writes-and-email.md +37 -0
  19. package/AGENTS.md +159 -0
  20. package/CLAUDE.md +2 -0
  21. package/LICENSE +21 -0
  22. package/README.md +511 -2
  23. package/bin/asrun.mjs +49 -0
  24. package/dist/lib/account-lock.d.ts +6 -0
  25. package/dist/lib/account-lock.js +52 -0
  26. package/dist/lib/assertions.d.ts +14 -0
  27. package/dist/lib/assertions.js +39 -0
  28. package/dist/lib/baselines.d.ts +43 -0
  29. package/dist/lib/baselines.js +108 -0
  30. package/dist/lib/build-artifacts.d.ts +8 -0
  31. package/dist/lib/build-artifacts.js +10 -0
  32. package/dist/lib/case-doc.d.ts +311 -0
  33. package/dist/lib/case-doc.js +1117 -0
  34. package/dist/lib/case-id.d.ts +11 -0
  35. package/dist/lib/case-id.js +17 -0
  36. package/dist/lib/case-ops.d.ts +26 -0
  37. package/dist/lib/case-ops.js +110 -0
  38. package/dist/lib/cases.d.ts +16 -0
  39. package/dist/lib/cases.js +51 -0
  40. package/dist/lib/cli.d.ts +10 -0
  41. package/dist/lib/cli.js +18 -0
  42. package/dist/lib/command.d.ts +24 -0
  43. package/dist/lib/command.js +90 -0
  44. package/dist/lib/downloads.d.ts +42 -0
  45. package/dist/lib/downloads.js +84 -0
  46. package/dist/lib/email.d.ts +10 -0
  47. package/dist/lib/email.js +19 -0
  48. package/dist/lib/ffmpeg.d.ts +8 -0
  49. package/dist/lib/ffmpeg.js +50 -0
  50. package/dist/lib/fixtures.d.ts +40 -0
  51. package/dist/lib/fixtures.js +149 -0
  52. package/dist/lib/login-card.d.ts +22 -0
  53. package/dist/lib/login-card.js +183 -0
  54. package/dist/lib/node-cli.d.ts +15 -0
  55. package/dist/lib/node-cli.js +36 -0
  56. package/dist/lib/paths.d.ts +25 -0
  57. package/dist/lib/paths.js +88 -0
  58. package/dist/lib/pdf.d.ts +71 -0
  59. package/dist/lib/pdf.js +140 -0
  60. package/dist/lib/profiles.d.ts +62 -0
  61. package/dist/lib/profiles.js +174 -0
  62. package/dist/lib/ready.d.ts +36 -0
  63. package/dist/lib/ready.js +53 -0
  64. package/dist/lib/records.d.ts +33 -0
  65. package/dist/lib/records.js +61 -0
  66. package/dist/lib/run-filters.d.ts +19 -0
  67. package/dist/lib/run-filters.js +38 -0
  68. package/dist/lib/run-summary.d.ts +90 -0
  69. package/dist/lib/run-summary.js +35 -0
  70. package/dist/lib/session.d.ts +30 -0
  71. package/dist/lib/session.js +111 -0
  72. package/dist/lib/summary-reporter.d.ts +16 -0
  73. package/dist/lib/summary-reporter.js +146 -0
  74. package/dist/lib/targets.d.ts +19 -0
  75. package/dist/lib/targets.js +26 -0
  76. package/dist/lib/ui-build.d.ts +10 -0
  77. package/dist/lib/ui-build.js +30 -0
  78. package/dist/lib/values.d.ts +29 -0
  79. package/dist/lib/values.js +85 -0
  80. package/dist/lib/write-gate.d.ts +41 -0
  81. package/dist/lib/write-gate.js +129 -0
  82. package/dist/playwright.config.d.ts +2 -0
  83. package/dist/playwright.config.js +92 -0
  84. package/dist/scripts/build.d.ts +1 -0
  85. package/dist/scripts/build.js +21 -0
  86. package/dist/scripts/check-cases.d.ts +1 -0
  87. package/dist/scripts/check-cases.js +149 -0
  88. package/dist/scripts/cleanup.d.ts +1 -0
  89. package/dist/scripts/cleanup.js +25 -0
  90. package/dist/scripts/cli.d.ts +1 -0
  91. package/dist/scripts/cli.js +28 -0
  92. package/dist/scripts/doctor.d.ts +1 -0
  93. package/dist/scripts/doctor.js +296 -0
  94. package/dist/scripts/explore.d.ts +1 -0
  95. package/dist/scripts/explore.js +75 -0
  96. package/dist/scripts/go.mjs +188 -0
  97. package/dist/scripts/heal-evidence.mjs +143 -0
  98. package/dist/scripts/init.d.ts +22 -0
  99. package/dist/scripts/init.js +332 -0
  100. package/dist/scripts/lint-specs.d.ts +1 -0
  101. package/dist/scripts/lint-specs.js +141 -0
  102. package/dist/scripts/login.d.ts +1 -0
  103. package/dist/scripts/login.js +75 -0
  104. package/dist/scripts/new-case.d.ts +1 -0
  105. package/dist/scripts/new-case.js +19 -0
  106. package/dist/scripts/render-pdf.d.ts +1 -0
  107. package/dist/scripts/render-pdf.js +44 -0
  108. package/dist/scripts/report.d.ts +1 -0
  109. package/dist/scripts/report.js +14 -0
  110. package/dist/scripts/review-baselines.d.ts +1 -0
  111. package/dist/scripts/review-baselines.js +30 -0
  112. package/dist/scripts/run.d.ts +1 -0
  113. package/dist/scripts/run.js +423 -0
  114. package/dist/scripts/status.d.ts +1 -0
  115. package/dist/scripts/status.js +31 -0
  116. package/dist/scripts/ui-build.d.ts +1 -0
  117. package/dist/scripts/ui-build.js +7 -0
  118. package/dist/scripts/ui-dev.d.ts +1 -0
  119. package/dist/scripts/ui-dev.js +3 -0
  120. package/dist/scripts/ui.d.ts +1 -0
  121. package/dist/scripts/ui.js +13 -0
  122. package/dist/ui/dev.d.ts +1 -0
  123. package/dist/ui/dev.js +36 -0
  124. package/dist/ui/server/actions.d.ts +95 -0
  125. package/dist/ui/server/actions.js +365 -0
  126. package/dist/ui/server/ai-models.d.ts +3 -0
  127. package/dist/ui/server/ai-models.js +347 -0
  128. package/dist/ui/server/ai-stream.d.ts +66 -0
  129. package/dist/ui/server/ai-stream.js +408 -0
  130. package/dist/ui/server/app.d.ts +4 -0
  131. package/dist/ui/server/app.js +395 -0
  132. package/dist/ui/server/cases.d.ts +23 -0
  133. package/dist/ui/server/cases.js +170 -0
  134. package/dist/ui/server/files.d.ts +5 -0
  135. package/dist/ui/server/files.js +67 -0
  136. package/dist/ui/server/index.d.ts +1 -0
  137. package/dist/ui/server/index.js +33 -0
  138. package/dist/ui/server/jobs.d.ts +80 -0
  139. package/dist/ui/server/jobs.js +317 -0
  140. package/dist/ui/server/profiles.d.ts +35 -0
  141. package/dist/ui/server/profiles.js +371 -0
  142. package/dist/ui/server/watch-files.d.ts +4 -0
  143. package/dist/ui/server/watch-files.js +36 -0
  144. package/dist/ui/shared/ai-commands.d.ts +88 -0
  145. package/dist/ui/shared/ai-commands.js +177 -0
  146. package/dist/ui/shared/case-id.d.ts +1 -0
  147. package/dist/ui/shared/case-id.js +1 -0
  148. package/dist/ui/shared/jobs.d.ts +6 -0
  149. package/dist/ui/shared/jobs.js +7 -0
  150. package/dist/ui/shared/product.d.ts +3 -0
  151. package/dist/ui/shared/product.js +3 -0
  152. package/dist/ui/shared/run-filters.d.ts +1 -0
  153. package/dist/ui/shared/run-filters.js +1 -0
  154. package/dist/ui/shared/site-address.d.ts +6 -0
  155. package/dist/ui/shared/site-address.js +11 -0
  156. package/dist/ui/shared/types.d.ts +371 -0
  157. package/dist/ui/shared/types.js +1 -0
  158. package/docs/agents/framework-dev.md +79 -0
  159. package/docs/theme-guide.html +922 -0
  160. package/package.json +109 -3
  161. package/templates/.claude/commands/explore-case.md +22 -0
  162. package/templates/.claude/commands/gen-spec.md +21 -0
  163. package/templates/.claude/commands/heal-spec.md +31 -0
  164. package/templates/.claude/commands/make-case.md +13 -0
  165. package/templates/.claude/settings.json +5 -0
  166. package/templates/.mcp.json +8 -0
  167. package/templates/AGENTS.md +64 -0
  168. package/templates/CLAUDE.md +1 -0
  169. package/templates/cases/_template.md +57 -0
  170. package/templates/gitignore +13 -0
  171. package/templates/opencode.jsonc +16 -0
  172. package/templates/package.json +31 -0
  173. package/templates/playwright.config.ts +3 -0
  174. package/templates/pnpm-workspace.yaml +5 -0
  175. package/templates/profiles.yaml +42 -0
  176. package/templates/seed.spec.ts +9 -0
  177. package/templates/tsconfig.json +14 -0
  178. package/ui/dist/apple-touch-icon.png +0 -0
  179. package/ui/dist/assets/apl-gfMMiTNf.js +1 -0
  180. package/ui/dist/assets/asciiarmor-C_eisQCp.js +1 -0
  181. package/ui/dist/assets/asn1-DCl_8MCW.js +1 -0
  182. package/ui/dist/assets/asterisk-BEn0Cwpx.js +1 -0
  183. package/ui/dist/assets/brainfuck-D5EjA2JK.js +1 -0
  184. package/ui/dist/assets/clike-xvONpD0Z.js +1 -0
  185. package/ui/dist/assets/clojure-BflJGmDX.js +1 -0
  186. package/ui/dist/assets/cmake-84nbDH0I.js +1 -0
  187. package/ui/dist/assets/cobol-DHLQUbN7.js +1 -0
  188. package/ui/dist/assets/coffeescript-De_zI4J0.js +1 -0
  189. package/ui/dist/assets/commonlisp-DgAKBe0r.js +1 -0
  190. package/ui/dist/assets/crystal-Bl6YKCG_.js +1 -0
  191. package/ui/dist/assets/css-CnaYoQib.js +1 -0
  192. package/ui/dist/assets/cypher-p9eYesGn.js +1 -0
  193. package/ui/dist/assets/d-DWkQyNnU.js +1 -0
  194. package/ui/dist/assets/diff-ChtP43wD.js +1 -0
  195. package/ui/dist/assets/dist-BGbe0-Aa.js +1 -0
  196. package/ui/dist/assets/dist-Ba-HC662.js +2 -0
  197. package/ui/dist/assets/dist-BtMLMthJ.js +1 -0
  198. package/ui/dist/assets/dist-Bw5-uxs9.js +1 -0
  199. package/ui/dist/assets/dist-BzulKuec.js +1 -0
  200. package/ui/dist/assets/dist-C-Oyo1ZT.js +1 -0
  201. package/ui/dist/assets/dist-C8MdhqgG.js +1 -0
  202. package/ui/dist/assets/dist-CG1pHn5m.js +1 -0
  203. package/ui/dist/assets/dist-CgIQYY1o.js +1 -0
  204. package/ui/dist/assets/dist-CmpteMYY.js +9 -0
  205. package/ui/dist/assets/dist-DDJNvivM.js +1 -0
  206. package/ui/dist/assets/dist-DF3F_I8N.js +1 -0
  207. package/ui/dist/assets/dist-DSy3dVf4.js +11 -0
  208. package/ui/dist/assets/dist-Dlorc3Sx.js +1 -0
  209. package/ui/dist/assets/dist-Dq6sW5Ur.js +1 -0
  210. package/ui/dist/assets/dist-Dr9crbBz.js +13 -0
  211. package/ui/dist/assets/dist-jjxQutEm.js +1 -0
  212. package/ui/dist/assets/dockerfile-DJkHQgkl.js +1 -0
  213. package/ui/dist/assets/dtd-DTF72YNO.js +1 -0
  214. package/ui/dist/assets/dylan-S7ucR4pR.js +1 -0
  215. package/ui/dist/assets/ebnf-D4c0_ac3.js +1 -0
  216. package/ui/dist/assets/ecl-BVIeWmwY.js +1 -0
  217. package/ui/dist/assets/eiffel-BEjRio4Q.js +1 -0
  218. package/ui/dist/assets/elm-Cshzl8qu.js +1 -0
  219. package/ui/dist/assets/erlang-BV0c4Hdn.js +1 -0
  220. package/ui/dist/assets/factor-LKH5gN8L.js +1 -0
  221. package/ui/dist/assets/fcl-ClOhbFR7.js +1 -0
  222. package/ui/dist/assets/forth-D6RzAl64.js +1 -0
  223. package/ui/dist/assets/fortran-Bt6PBEDR.js +1 -0
  224. package/ui/dist/assets/gas-BHEdbvp9.js +1 -0
  225. package/ui/dist/assets/gherkin-oBAE_ms0.js +1 -0
  226. package/ui/dist/assets/groovy-CUrWKPia.js +1 -0
  227. package/ui/dist/assets/haskell-BRsoo5mP.js +1 -0
  228. package/ui/dist/assets/haxe-DjVOVvNP.js +1 -0
  229. package/ui/dist/assets/http-CLVgA2GD.js +1 -0
  230. package/ui/dist/assets/idl-BUZw3wgd.js +1 -0
  231. package/ui/dist/assets/index-CTmnF0Wp.css +2 -0
  232. package/ui/dist/assets/index-P_Yo0ZZn.js +338 -0
  233. package/ui/dist/assets/javascript-lS2r2ifg.js +1 -0
  234. package/ui/dist/assets/julia-CQfgDRfP.js +1 -0
  235. package/ui/dist/assets/livescript-BXxG0Yva.js +1 -0
  236. package/ui/dist/assets/lua-DPCEkfD_.js +1 -0
  237. package/ui/dist/assets/mathematica-u8YMmbU0.js +1 -0
  238. package/ui/dist/assets/mbox-BEMVcqjs.js +1 -0
  239. package/ui/dist/assets/mirc-C9zpzAZU.js +1 -0
  240. package/ui/dist/assets/mllike-B45xgp2S.js +1 -0
  241. package/ui/dist/assets/modelica-CqHI_q-D.js +1 -0
  242. package/ui/dist/assets/mscgen-qgSLujhx.js +1 -0
  243. package/ui/dist/assets/mumps-BSzj6Xyz.js +1 -0
  244. package/ui/dist/assets/nginx-C-ORUuei.js +1 -0
  245. package/ui/dist/assets/nsis-viPvXFiq.js +1 -0
  246. package/ui/dist/assets/ntriples-CzMBulkp.js +1 -0
  247. package/ui/dist/assets/octave-D2Q8cUp1.js +1 -0
  248. package/ui/dist/assets/oz-BwTct_5T.js +1 -0
  249. package/ui/dist/assets/pascal-Dp_KdFgX.js +1 -0
  250. package/ui/dist/assets/perl-BkSkiI9m.js +1 -0
  251. package/ui/dist/assets/pig-AF4Hwhju.js +1 -0
  252. package/ui/dist/assets/powershell-D8WpQPaX.js +1 -0
  253. package/ui/dist/assets/properties-C357ku5U.js +1 -0
  254. package/ui/dist/assets/protobuf-D1ij_kNL.js +1 -0
  255. package/ui/dist/assets/pug-BqCHZ5xT.js +1 -0
  256. package/ui/dist/assets/puppet-BZyAS7I4.js +1 -0
  257. package/ui/dist/assets/python-B_1Jd8HM.js +1 -0
  258. package/ui/dist/assets/q-B9NhS7L_.js +1 -0
  259. package/ui/dist/assets/r-CxDmU1I4.js +1 -0
  260. package/ui/dist/assets/rpm-gpDK5sbt.js +1 -0
  261. package/ui/dist/assets/ruby-ChfNwlCY.js +1 -0
  262. package/ui/dist/assets/sas-C2OyD2Y6.js +1 -0
  263. package/ui/dist/assets/scheme-D9qYN0V5.js +1 -0
  264. package/ui/dist/assets/shell-DwuoZtxw.js +1 -0
  265. package/ui/dist/assets/sieve-tLWDfEkZ.js +1 -0
  266. package/ui/dist/assets/simple-mode-DXiLM1-G.js +1 -0
  267. package/ui/dist/assets/smalltalk-BLp5-jwC.js +1 -0
  268. package/ui/dist/assets/solr-B78wSQJQ.js +1 -0
  269. package/ui/dist/assets/sparql-Dz-mlCAC.js +1 -0
  270. package/ui/dist/assets/spreadsheet-CTIncYxj.js +1 -0
  271. package/ui/dist/assets/sql-DuQMFDvy.js +1 -0
  272. package/ui/dist/assets/stex-DNzd62Q_.js +1 -0
  273. package/ui/dist/assets/stylus-BOVgKnng.js +1 -0
  274. package/ui/dist/assets/swift-DKB6_1j6.js +1 -0
  275. package/ui/dist/assets/tcl-BGfruO1u.js +1 -0
  276. package/ui/dist/assets/textile-C4jVoHG1.js +1 -0
  277. package/ui/dist/assets/tiddlywiki-VlUiK86J.js +1 -0
  278. package/ui/dist/assets/tiki-CvUEFusT.js +1 -0
  279. package/ui/dist/assets/toml-BPTmHmyx.js +1 -0
  280. package/ui/dist/assets/troff-C2dAgh7P.js +1 -0
  281. package/ui/dist/assets/ttcn-PPSTk1ow.js +1 -0
  282. package/ui/dist/assets/ttcn-cfg-2b4Gb5sE.js +1 -0
  283. package/ui/dist/assets/turtle-Cx3rYX2g.js +1 -0
  284. package/ui/dist/assets/vb-BthdM_4N.js +1 -0
  285. package/ui/dist/assets/vbscript-D9f6rSsU.js +1 -0
  286. package/ui/dist/assets/velocity-CqftRsjg.js +1 -0
  287. package/ui/dist/assets/verilog-CjI9Y6RI.js +1 -0
  288. package/ui/dist/assets/vhdl-kS471soi.js +1 -0
  289. package/ui/dist/assets/webidl-D5jiJvOU.js +1 -0
  290. package/ui/dist/assets/xquery-BKJx4mf9.js +1 -0
  291. package/ui/dist/assets/yacas-CXXgtw0p.js +1 -0
  292. package/ui/dist/assets/z80-Co3PbHdE.js +1 -0
  293. package/ui/dist/favicon.ico +0 -0
  294. package/ui/dist/favicon.svg +11 -0
  295. package/ui/dist/help/ai-tools.png +0 -0
  296. package/ui/dist/help/case-explore.png +0 -0
  297. package/ui/dist/help/case-generate.png +0 -0
  298. package/ui/dist/help/cases-memo.png +0 -0
  299. package/ui/dist/help/profile-form.png +0 -0
  300. package/ui/dist/help/profiles-login.png +0 -0
  301. package/ui/dist/help/reports.png +0 -0
  302. package/ui/dist/help/run-tests.png +0 -0
  303. package/ui/dist/index.html +23 -0
  304. package/ui/dist/logos/claude.svg +1 -0
  305. package/ui/dist/logos/opencode.svg +1 -0
@@ -0,0 +1,408 @@
1
+ const MAX_DETAIL = 180;
2
+ const MAX_ERROR = 300;
3
+ const MAX_REASONING = 2000;
4
+ /** A call's output is a preview, not a transcript: the first few lines say what it did without flooding the log. */
5
+ const PREVIEW_LINES = 6;
6
+ const PREVIEW_LINE = 200;
7
+ const PREVIEW_CHARS = 500;
8
+ const ANSI = /\u001b\[[0-9;?]*[ -/]*[@-~]/g;
9
+ const SANDBOX_ERROR = /listen EPERM|outside of the sandbox|sandbox/i;
10
+ /** A saved login is a password (AGENTS.md F5): never copy what a call read from `.auth` into the log. */
11
+ const SECRET_PATH = /(^|[\\/])\.auth([\\/]|$)/;
12
+ const oneLine = (value, max) => {
13
+ const text = String(value ?? '').replace(/\s+/g, ' ').trim();
14
+ return text.length > max ? `${text.slice(0, max - 1)}…` : text;
15
+ };
16
+ const cap = (text, max) => (text.length > max ? `${text.slice(0, max - 1)}…` : text);
17
+ const seconds = (ms) => (ms < 90_000 ? `${Math.round(ms / 1000)} s` : `${(ms / 60_000).toFixed(1)} min`);
18
+ /** The first thing a piece of code does, for a tool whose only argument is the code (`execute`). */
19
+ function firstStatement(code) {
20
+ const line = code
21
+ .split('\n')
22
+ .map((text) => text.trim())
23
+ .find((text) => text !== '' && !text.startsWith('//') && !text.startsWith('#'));
24
+ return line ?? '';
25
+ }
26
+ /** `mcp__playwright-test__browser_click` -> `browser_click`. */
27
+ const toolName = (name) => name.replace(/^mcp__[^_]+(?:-[^_]+)*__/, '');
28
+ /**
29
+ * What the call is about, in a few words: the intent the agent gave, or the argument that names the thing it touched.
30
+ * The names differ per CLI and per tool (`file_path` for Claude, `path` for OpenCode; `pattern` for glob and grep;
31
+ * `code` for a code-mode `execute`), so every one of them is looked at.
32
+ */
33
+ function describe(input) {
34
+ if (!input || typeof input !== 'object')
35
+ return '';
36
+ const args = input;
37
+ const key = [
38
+ 'intent',
39
+ 'description',
40
+ 'skill',
41
+ 'name',
42
+ 'command',
43
+ 'path',
44
+ 'file_path',
45
+ 'filePath',
46
+ 'filename',
47
+ 'pattern',
48
+ 'query',
49
+ 'url',
50
+ 'element',
51
+ ].find((name) => typeof args[name] === 'string' && args[name].trim() !== '');
52
+ if (key)
53
+ return oneLine(args[key], MAX_DETAIL);
54
+ if (typeof args.code === 'string')
55
+ return oneLine(firstStatement(args.code), MAX_DETAIL);
56
+ // Nothing known: the longest string still tells more than the bare tool name.
57
+ const longest = Object.values(args)
58
+ .filter((value) => typeof value === 'string')
59
+ .sort((a, b) => b.length - a.length)[0];
60
+ return longest ? oneLine(longest, MAX_DETAIL) : '';
61
+ }
62
+ const callOf = (name, input) => ({
63
+ name: toolName(name),
64
+ detail: describe(input),
65
+ ok: true,
66
+ pending: true,
67
+ });
68
+ const labelText = (call) => (call.detail ? `${call.name}: ${call.detail}` : call.name);
69
+ /**
70
+ * The first lines of what a call returned, with the colors a shell put in it. Long output keeps its head only: the
71
+ * point is to see what happened, not to copy a file into the log.
72
+ */
73
+ function previewText(value) {
74
+ const text = (typeof value === 'string' ? value : value == null ? '' : String(value))
75
+ .replace(/\r\n?/g, '\n')
76
+ .replace(/\t/g, ' ')
77
+ .trim();
78
+ if (!text)
79
+ return null;
80
+ const lines = text.split('\n');
81
+ const kept = [];
82
+ let chars = 0;
83
+ for (const line of lines) {
84
+ if (kept.length >= PREVIEW_LINES || chars >= PREVIEW_CHARS)
85
+ break;
86
+ // A cut line loses its colors, so a half escape sequence cannot bleed into the rest of the block.
87
+ const shown = line.length > PREVIEW_LINE ? oneLine(line.replace(ANSI, ''), PREVIEW_LINE) : line;
88
+ kept.push(shown);
89
+ chars += shown.length;
90
+ }
91
+ const rest = lines.length - kept.length;
92
+ return kept.join('\n') + (rest > 0 ? `\n… (${rest} more line${rest === 1 ? '' : 's'})` : '');
93
+ }
94
+ const HANDOFF_CFG = '__handoffCfg';
95
+ const HANDOFF_CARD = /handoff-card\.js/;
96
+ /** What the card returns when it stops waiting (`{outcome: 'done' | 'skip' | 'condition' | 'timeout'}`). */
97
+ const HANDOFF_RESULT = /\\?"outcome\\?"\s*:\s*\\?"(?:done|skip|condition|timeout)\\?"/;
98
+ /**
99
+ * Said when a card came back that the UI was never told about: only a call of its own that sets the options lets the
100
+ * UI know a person is needed, so a card that ran with the options of an earlier handoff (or with the options set inside
101
+ * the call that runs the card) has no badge and no notification.
102
+ */
103
+ const MISSED_HANDOFF_HINT = 'HINT: this handoff was not announced: the web UI learns a handoff from the call that sets __handoffCfg, and this ' +
104
+ 'card ran without one (see user-handoff.md, "Two calls, every time"), so there was no "Your turn" badge and no ' +
105
+ 'notification.';
106
+ /** The browser tool a handoff runs through: `browser_run_code`, `browser_run_code_unsafe`, ... */
107
+ const BROWSER_RUN_CODE = /browser_run_code/;
108
+ /**
109
+ * How long "the options are set" survives calls that are not part of the handoff. The agent sets the options and then
110
+ * runs the card, and in between it may take a screenshot or read a file; the badge (and a notification) must not blink
111
+ * off and on for that. Long enough for those gaps, short enough that options never followed by a card do not leave the
112
+ * badge on.
113
+ */
114
+ const HANDOFF_CFG_TTL_MS = 300_000;
115
+ /** `message: '...'` of the options call that comes right before the card. */
116
+ function handoffMessage(code) {
117
+ const match = /message\s*:\s*(['"`])((?:\\.|(?!\1)[^\\])*)\1/.exec(code);
118
+ return match ? match[2].replace(/\\(['"`\\])/g, '$1').trim() || null : null;
119
+ }
120
+ const codeOf = (input) => {
121
+ if (!input || typeof input !== 'object')
122
+ return '';
123
+ const code = input.code;
124
+ return typeof code === 'string' ? code : '';
125
+ };
126
+ /**
127
+ * Which handoff call this is, if any. The browser tool is called either directly or from a CLI's code mode: OpenCode's
128
+ * `execute` tool takes a `code` string that holds `tools["playwright-test"].browser_run_code_unsafe({...})`, so the
129
+ * nested call is only text inside it and the tool name says nothing about the browser.
130
+ */
131
+ function handoffCall(name, input) {
132
+ if (!input || typeof input !== 'object')
133
+ return null;
134
+ const args = input;
135
+ const code = codeOf(input);
136
+ const filename = typeof args.filename === 'string' ? args.filename : '';
137
+ // A code-mode call is a handoff only when the browser tool is inside it: it also runs everything else.
138
+ if (!BROWSER_RUN_CODE.test(name) && !BROWSER_RUN_CODE.test(code))
139
+ return null;
140
+ if (HANDOFF_CARD.test(filename) || (HANDOFF_CARD.test(code) && /filename\s*:/.test(code)))
141
+ return 'card';
142
+ return code.includes(HANDOFF_CFG) ? 'options' : null;
143
+ }
144
+ export class AiStreamFormatter {
145
+ tool;
146
+ buffer = '';
147
+ pending = new Map();
148
+ cardCalls = new Set();
149
+ waitingForUser = null;
150
+ /** The message of the last options call, kept so a card that runs after unrelated calls still carries it. */
151
+ lastHandoffMessage = null;
152
+ lastEventAt = Date.now();
153
+ lastWaitAt = 0;
154
+ /** The last call the agent finished, so a quiet spell can say what it is doing instead of only that nothing came. */
155
+ lastCall = '';
156
+ sandboxHinted = false;
157
+ constructor(tool) {
158
+ this.tool = tool;
159
+ }
160
+ /** Blocks for a piece of stdout. Incomplete trailing lines wait for the next piece. */
161
+ feed(text) {
162
+ this.buffer += text;
163
+ const lines = this.buffer.split('\n');
164
+ this.buffer = lines.pop() ?? '';
165
+ return lines.flatMap((line) => this.line(line));
166
+ }
167
+ /**
168
+ * Set while a handoff card waits for the person. OpenCode reports a tool call only when it has finished, so the card
169
+ * call itself is not visible while it waits: the options call before it (`__handoffCfg`) starts the state and the end
170
+ * of the card call, or a later call, ends it. The object changes on every new handoff, so `===` detects a change.
171
+ */
172
+ get awaiting() {
173
+ return this.waitingForUser;
174
+ }
175
+ /**
176
+ * Called once for every tool call the agent makes, with the id the end of that call will carry. Returns a line for the
177
+ * log when a card came back that nobody was told about (`MISSED_HANDOFF_HINT`).
178
+ */
179
+ watchHandoff(id, name, input, ended, output) {
180
+ // The card answers `{outcome: ...}` when it stops waiting: that ends the handoff whatever ran it.
181
+ if (ended && HANDOFF_RESULT.test(String(output ?? ''))) {
182
+ const announced = this.waitingForUser !== null;
183
+ this.waitingForUser = null;
184
+ return announced ? null : { kind: 'hint', at: Date.now(), data: MISSED_HANDOFF_HINT };
185
+ }
186
+ const kind = handoffCall(name, input);
187
+ if (kind === 'card') {
188
+ if (ended)
189
+ this.waitingForUser = null;
190
+ else {
191
+ this.cardCalls.add(id);
192
+ // `??=` on purpose: the options call before it already knows the message and when the wait started.
193
+ this.waitingForUser ??= { message: handoffMessage(codeOf(input)) ?? this.lastHandoffMessage, since: Date.now() };
194
+ }
195
+ return null;
196
+ }
197
+ if (kind === 'options') {
198
+ const message = handoffMessage(codeOf(input));
199
+ if (message)
200
+ this.lastHandoffMessage = message;
201
+ // Keep the start time when the wait is already set: a second report of the same handoff must not announce it twice.
202
+ const since = this.waitingForUser?.since ?? Date.now();
203
+ this.waitingForUser = { message: message ?? this.lastHandoffMessage, since };
204
+ return null;
205
+ }
206
+ // Anything else ends the wait, unless it was set too recently to be an abandoned one: the card runs right after it.
207
+ if (this.waitingForUser && Date.now() - this.waitingForUser.since > HANDOFF_CFG_TTL_MS)
208
+ this.waitingForUser = null;
209
+ return null;
210
+ }
211
+ /** What is left when the process ends. */
212
+ end() {
213
+ const rest = this.buffer;
214
+ this.buffer = '';
215
+ return rest.trim() ? this.line(rest) : [];
216
+ }
217
+ /**
218
+ * A block about the oldest tool call that has not returned, when nothing has happened for `quietMs` (and again every
219
+ * `quietMs` after that). It names the last call that did finish, so a long run never looks stuck.
220
+ */
221
+ waiting(quietMs) {
222
+ const now = Date.now();
223
+ if (now - Math.max(this.lastEventAt, this.lastWaitAt) < quietMs)
224
+ return null;
225
+ this.lastWaitAt = now;
226
+ const [first] = [...this.pending.values()].sort((a, b) => a.startedAt - b.startedAt);
227
+ const last = this.lastCall ? ` (last: ${this.lastCall})` : '';
228
+ const data = first
229
+ ? `... still waiting for ${labelText(first)} (running ${seconds(now - first.startedAt)})`
230
+ : `... no output from the agent for ${seconds(now - this.lastEventAt)}${last}`;
231
+ return { kind: 'meta', at: now, data };
232
+ }
233
+ line(raw) {
234
+ const trimmed = raw.trim();
235
+ if (!trimmed)
236
+ return [];
237
+ let event;
238
+ try {
239
+ event = JSON.parse(trimmed);
240
+ }
241
+ catch {
242
+ // Not one of the CLI's events: keep the line as it came, it is the only trace of it.
243
+ return [{ kind: 'meta', at: Date.now(), data: raw.trimEnd() }];
244
+ }
245
+ if (!event || typeof event !== 'object' || typeof event.type !== 'string') {
246
+ return [{ kind: 'meta', at: Date.now(), data: raw.trimEnd() }];
247
+ }
248
+ this.lastEventAt = Date.now();
249
+ return this.tool === 'claude' ? this.claude(event) : this.opencode(event);
250
+ }
251
+ /** A call the agent has started and not finished yet (Claude reports both ends). */
252
+ start(id, call) {
253
+ const now = Date.now();
254
+ this.pending.set(id, { name: call.name, detail: call.detail, startedAt: now });
255
+ return [{ kind: 'tool', at: now, data: `>> ${labelText(call)}`, tool: call }];
256
+ }
257
+ /**
258
+ * The end of a call, plus the first lines of what it returned. `fallback` is for a CLI that reports a call only when
259
+ * it is over (OpenCode), which still knows when it started.
260
+ */
261
+ finishCall(id, ok, output, fallback) {
262
+ const now = Date.now();
263
+ const call = this.pending.get(id);
264
+ this.pending.delete(id);
265
+ if (this.cardCalls.delete(id))
266
+ this.waitingForUser = null;
267
+ const name = call?.name ?? fallback?.name ?? 'tool';
268
+ const detail = call?.detail ?? fallback?.detail ?? '';
269
+ const startedAt = call?.startedAt ?? fallback?.startedAt;
270
+ const took = startedAt ? seconds(now - startedAt) : undefined;
271
+ const label = labelText({ name, detail });
272
+ this.lastCall = label;
273
+ const parts = [
274
+ {
275
+ kind: 'tool',
276
+ at: now,
277
+ data: `${ok ? 'ok' : 'FAILED'}: ${label}${took ? ` after ${took}` : ''}`,
278
+ tool: { name, detail, ok, ...(took && { took }) },
279
+ },
280
+ ];
281
+ if (!ok) {
282
+ const error = oneLine(output, MAX_ERROR);
283
+ if (error)
284
+ parts.push({ kind: 'error', at: now, data: error });
285
+ return parts;
286
+ }
287
+ // A saved login is a password: what a call read from `.auth` never goes into the log.
288
+ const preview = SECRET_PATH.test(detail) ? null : previewText(output);
289
+ if (preview)
290
+ parts.push({ kind: 'result', at: now, data: preview });
291
+ return parts;
292
+ }
293
+ /** Said once per run, after the first tool call that Claude Code's sandbox refused (nobody is there to approve it). */
294
+ sandboxHint(isError, body) {
295
+ if (this.sandboxHinted || !isError || !SANDBOX_ERROR.test(String(body ?? '')))
296
+ return null;
297
+ this.sandboxHinted = true;
298
+ return {
299
+ kind: 'hint',
300
+ at: Date.now(),
301
+ data: "HINT: Claude Code's sandbox blocked a command and a one-shot run cannot approve it. Options:\n" +
302
+ ' - Switch the AI agent to OpenCode in the top bar and run again (it does not use this sandbox).\n' +
303
+ ' - Or run the command yourself in a terminal and paste the output into the agent.\n' +
304
+ ' - Or check that the command matches "sandbox.excludedCommands" in .claude/settings.json (every command of a chained call must match).',
305
+ };
306
+ }
307
+ claude(event) {
308
+ const content = event.message?.content;
309
+ if (event.type === 'assistant' && Array.isArray(content)) {
310
+ return content.flatMap((block) => {
311
+ if (block?.type === 'text' && block.text?.trim()) {
312
+ return [{ kind: 'markdown', at: Date.now(), data: block.text.trimEnd() }];
313
+ }
314
+ // A thinking block: worth reading while it waits, but never mistaken for the answer (see `--thinking`).
315
+ if (block?.type === 'thinking' && block.thinking?.trim()) {
316
+ return [{ kind: 'reasoning', at: Date.now(), data: cap(block.thinking.trim(), MAX_REASONING) }];
317
+ }
318
+ if (block?.type === 'tool_use') {
319
+ this.watchHandoff(String(block.id), String(block.name), block.input, false);
320
+ return this.start(String(block.id), callOf(String(block.name), block.input));
321
+ }
322
+ return [];
323
+ });
324
+ }
325
+ if (event.type === 'user' && Array.isArray(content)) {
326
+ return content
327
+ .filter((block) => block?.type === 'tool_result')
328
+ .flatMap((block) => {
329
+ const body = Array.isArray(block.content)
330
+ ? block.content.map((part) => part?.text ?? '').join(' ')
331
+ : block.content;
332
+ const hint = this.sandboxHint(block.is_error, body);
333
+ return [...this.finishCall(String(block.tool_use_id), !block.is_error, body), ...(hint ? [hint] : [])];
334
+ });
335
+ }
336
+ if (event.type === 'result' && event.is_error) {
337
+ return [{ kind: 'error', at: Date.now(), data: oneLine(event.result ?? event.subtype, MAX_ERROR) }];
338
+ }
339
+ return [];
340
+ }
341
+ opencode(event) {
342
+ const part = event.part ?? {};
343
+ // The CLI reports a failed turn (bad model, no credentials, a usage limit, a provider outage) as an error event,
344
+ // not on stderr: without this branch the log of a run that died at once showed nothing but the exit code.
345
+ if (event.type === 'error')
346
+ return this.opencodeError(event.error);
347
+ // `--thinking` is what makes OpenCode ask the model to think; without it these blocks are never sent.
348
+ if (event.type === 'reasoning' && typeof part.text === 'string' && part.text.trim()) {
349
+ return [{ kind: 'reasoning', at: Date.now(), data: cap(part.text.trim(), MAX_REASONING) }];
350
+ }
351
+ if (event.type === 'text' && typeof part.text === 'string' && part.text.trim()) {
352
+ return [{ kind: 'markdown', at: Date.now(), data: part.text.trimEnd() }];
353
+ }
354
+ if (event.type === 'tool_use') {
355
+ const state = part.state ?? {};
356
+ const call = callOf(String(part.tool ?? 'tool'), state.input);
357
+ const id = String(part.callID ?? part.id ?? labelText(call));
358
+ const finished = state.status === 'completed' || state.status === 'error';
359
+ const hint = this.watchHandoff(id, String(part.tool ?? ''), state.input, finished, finished ? (state.error ?? state.output) : undefined);
360
+ if (finished) {
361
+ const time = state.time ?? {};
362
+ const parts = this.finishCall(id, state.status === 'completed', state.error ?? state.output, {
363
+ ...call,
364
+ ...(typeof time.start === 'number' && { startedAt: time.start }),
365
+ });
366
+ return hint ? [...parts, hint] : parts;
367
+ }
368
+ const parts = this.start(id, call);
369
+ return hint ? [...parts, hint] : parts;
370
+ }
371
+ return [];
372
+ }
373
+ /** `FAILED: <why>`, plus what to do about the two failures users actually hit. */
374
+ opencodeError(error) {
375
+ const body = (error ?? {});
376
+ const now = Date.now();
377
+ const message = oneLine(body.message ?? body.type ?? 'the agent failed', MAX_ERROR);
378
+ const type = String(body.type ?? '');
379
+ const parts = [{ kind: 'error', at: now, data: `FAILED: ${message}` }];
380
+ if (type === 'provider.free-limit') {
381
+ const wait = typeof body.retryAfterMs === 'number' ? ` (it comes back in about ${waitFor(body.retryAfterMs)})` : '';
382
+ parts.push({
383
+ kind: 'hint',
384
+ at: now,
385
+ data: 'HINT: this run used a free OpenCode model, and its free usage limit is spent' +
386
+ `${wait}. Pick a model in the top bar (it has to belong to a provider you are signed in to), ` +
387
+ 'or switch the AI agent to Claude. A model can also be set as "model" in opencode.jsonc.',
388
+ });
389
+ }
390
+ else if (type === 'provider.no-route' || type === 'provider.no-key' || type === 'provider.auth') {
391
+ parts.push({
392
+ kind: 'hint',
393
+ at: now,
394
+ data: 'HINT: the model is not available to this OpenCode install. Pick another one in the top bar, ' +
395
+ 'or sign the provider in with "opencode auth login".',
396
+ });
397
+ }
398
+ return parts;
399
+ }
400
+ }
401
+ /** A retry delay as a person reads it: `40 s`, `12.5 min`, `15.3 h`. */
402
+ function waitFor(ms) {
403
+ if (ms < 90_000)
404
+ return `${Math.max(1, Math.round(ms / 1000))} s`;
405
+ if (ms < 90 * 60_000)
406
+ return `${(ms / 60_000).toFixed(1)} min`;
407
+ return `${(ms / 3_600_000).toFixed(1)} h`;
408
+ }
@@ -0,0 +1,4 @@
1
+ import { Hono } from 'hono';
2
+ export declare function createApp({ port }: {
3
+ port: number;
4
+ }): Hono<import("hono/types").BlankEnv, import("hono/types").BlankSchema, "/">;