@iowarp/clio-coder 0.5.0 → 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 (312) hide show
  1. package/.claude-plugin/marketplace.json +4 -4
  2. package/CHANGELOG.md +35 -0
  3. package/CONTRIBUTING.md +9 -0
  4. package/README.md +12 -9
  5. package/ROADMAP.md +30 -5
  6. package/dist/{acp-KS7ARGV6.js → acp-F6PGOFZ4.js} +2 -2
  7. package/dist/{agents-YOGJ2YEJ.js → agents-ZHP4R6EX.js} +22 -21
  8. package/dist/assets/codewiki.json +1 -1
  9. package/dist/{auth-CEDCO6O6.js → auth-N75U2H3Z.js} +4 -4
  10. package/dist/{chunk-D3KWFOUT.js → chunk-23UIXH6Z.js} +3 -3
  11. package/dist/{chunk-ZR5YA6ZF.js → chunk-36XVK5P5.js} +5 -5
  12. package/dist/{chunk-AVSR2HKI.js → chunk-3DZ4LPLS.js} +6 -6
  13. package/dist/{chunk-LYJJ6546.js → chunk-3GSPCWFX.js} +12 -12
  14. package/dist/{chunk-CRHPCPZP.js → chunk-4KC4CHKJ.js} +2 -2
  15. package/dist/{chunk-4VDHLCF5.js → chunk-4WJBWKMI.js} +2 -2
  16. package/dist/{chunk-KBUTSJSR.js → chunk-4XBYHXGK.js} +2 -2
  17. package/dist/{chunk-6W75D37F.js → chunk-53P775GE.js} +6 -3
  18. package/dist/{chunk-XZWSP67B.js → chunk-5APYAXCB.js} +2 -2
  19. package/dist/{chunk-D65E25JG.js → chunk-67HPYNTJ.js} +2 -2
  20. package/dist/{chunk-5JQR5B4D.js → chunk-6KV4KCQZ.js} +4 -4
  21. package/dist/{chunk-V4XJWDHR.js → chunk-6UKFRTXI.js} +2 -1
  22. package/dist/{chunk-NDPF2N2L.js → chunk-7DW7CJW2.js} +2 -2
  23. package/dist/{chunk-5KQEUIWM.js → chunk-7LUEQODV.js} +4 -4
  24. package/dist/{chunk-LNOXCGN2.js → chunk-7UFDIOBT.js} +3 -3
  25. package/dist/{chunk-DXSETN72.js → chunk-7WUGNN43.js} +2 -2
  26. package/dist/{chunk-V35KZDUZ.js → chunk-AA3GRZWS.js} +2 -2
  27. package/dist/{chunk-X4CEFOSG.js → chunk-ADO3OQIC.js} +4 -4
  28. package/dist/{chunk-UX2L5CUD.js → chunk-AI36V3ON.js} +3 -3
  29. package/dist/{chunk-B7GY6QER.js → chunk-B26OUMJW.js} +2 -2
  30. package/dist/{chunk-LAR7DX5E.js → chunk-B5TASTCK.js} +153 -62
  31. package/dist/{chunk-TGWZXJ7L.js → chunk-BEMQK5Q7.js} +92 -84
  32. package/dist/{chunk-RNDCROSG.js → chunk-CNXU7TR2.js} +3 -3
  33. package/dist/{chunk-3JLTRPN7.js → chunk-CVMUXT64.js} +35 -66
  34. package/dist/{chunk-733ZBT5P.js → chunk-E74MJF6G.js} +4 -4
  35. package/dist/{chunk-T2MDZES3.js → chunk-EBMTBE76.js} +3 -3
  36. package/dist/{chunk-OZ76TLGU.js → chunk-EEITU7S6.js} +2 -2
  37. package/dist/{chunk-GRHOXDKB.js → chunk-ELDJWJKM.js} +97 -14
  38. package/dist/{chunk-GCQFN7YI.js → chunk-EMYEQLWO.js} +2 -2
  39. package/dist/{chunk-LTWH2ACR.js → chunk-EQPFEDFV.js} +222 -127
  40. package/dist/{chunk-TDGYTWM6.js → chunk-F4I3H56H.js} +2 -2
  41. package/dist/chunk-F6KCJO3U.js +523 -0
  42. package/dist/{chunk-FGSQHUCK.js → chunk-F6VKWTTY.js} +5 -5
  43. package/dist/{chunk-UJD5MPP6.js → chunk-FLNXQQ5B.js} +2 -2
  44. package/dist/{chunk-EX57VVWW.js → chunk-FOSRJ2VZ.js} +4 -4
  45. package/dist/{chunk-ZARZ4POR.js → chunk-G5ZDQQE3.js} +3 -3
  46. package/dist/{chunk-HQVN7G4F.js → chunk-G62JBPXN.js} +7 -7
  47. package/dist/{chunk-ZJPORZTC.js → chunk-GIAI7A2K.js} +2 -2
  48. package/dist/chunk-GPTR6OTU.js +2551 -0
  49. package/dist/{chunk-EZD2BXGP.js → chunk-GUFQPTQK.js} +2 -2
  50. package/dist/{chunk-OPD7GL6F.js → chunk-H6FQVNZC.js} +8 -7
  51. package/dist/chunk-H7REJ7L5.js +80 -0
  52. package/dist/{chunk-EYTLV3W3.js → chunk-HTWTORF7.js} +2 -2
  53. package/dist/{chunk-DF6PI6GN.js → chunk-I6UKQPGS.js} +3 -3
  54. package/dist/{chunk-ISHUS7HC.js → chunk-JRW236JT.js} +3 -3
  55. package/dist/{chunk-DE2LU267.js → chunk-KKAJGT3B.js} +3 -2
  56. package/dist/{chunk-OV62D6KO.js → chunk-KQMYL5GR.js} +3 -3
  57. package/dist/{chunk-QDUUOZ3S.js → chunk-KWMFC7FN.js} +3 -3
  58. package/dist/{chunk-R7FGRZNF.js → chunk-LXYHCCIZ.js} +9 -9
  59. package/dist/{chunk-NFSK2VNU.js → chunk-LZMTVVN4.js} +2 -2
  60. package/dist/{chunk-62UK7AGW.js → chunk-M5PMRZSR.js} +397 -44
  61. package/dist/chunk-MMZMM6SW.js +131 -0
  62. package/dist/{chunk-FFYTC2KQ.js → chunk-MNABNEBX.js} +2 -2
  63. package/dist/{chunk-XMDBTYCH.js → chunk-N3CWOGVF.js} +3 -3
  64. package/dist/{chunk-VYRQHORQ.js → chunk-NMQSO6Z6.js} +4 -3
  65. package/dist/{chunk-R2UUNSNQ.js → chunk-O3ZAF7UY.js} +18 -5
  66. package/dist/{chunk-4SYHGVDE.js → chunk-OQNCLZI4.js} +4 -4
  67. package/dist/{chunk-EG4ARXPC.js → chunk-OX6QTA4N.js} +3 -3
  68. package/dist/{chunk-P4GNZPXO.js → chunk-OYUS3UZP.js} +2 -2
  69. package/dist/{chunk-OQ2EOAHA.js → chunk-PF36KOGI.js} +291 -112
  70. package/dist/{chunk-55RMDDFM.js → chunk-PGQG7ZMW.js} +2 -2
  71. package/dist/{chunk-CBAQPTDU.js → chunk-PIU6BXXW.js} +2 -2
  72. package/dist/{chunk-EKV4UBCM.js → chunk-PZBQUJ2F.js} +2 -2
  73. package/dist/{chunk-7RET77VB.js → chunk-Q2HKY32Y.js} +2 -2
  74. package/dist/{chunk-U6YTUVOX.js → chunk-Q4RMNWMZ.js} +4 -4
  75. package/dist/chunk-Q6FRR3AQ.js +48 -0
  76. package/dist/{chunk-6RJMYYIE.js → chunk-QJSIAJBY.js} +3 -3
  77. package/dist/{chunk-DOUISWEE.js → chunk-QOJ4ERMU.js} +5 -5
  78. package/dist/{chunk-IFPDAD7R.js → chunk-RWSRKXLT.js} +3 -3
  79. package/dist/{chunk-T33IYTZM.js → chunk-SAASVMIY.js} +3 -3
  80. package/dist/{chunk-2DTIWSAW.js → chunk-SEQDH6JC.js} +3 -3
  81. package/dist/{chunk-BRCOSE7O.js → chunk-SZUO5BSE.js} +2 -2
  82. package/dist/{chunk-HOEDA42N.js → chunk-T7HZ7KJF.js} +2 -2
  83. package/dist/{chunk-OJXOA6YU.js → chunk-TL6LNERH.js} +2 -2
  84. package/dist/{chunk-UCINIRHW.js → chunk-TPCZXWLT.js} +3 -3
  85. package/dist/{chunk-ZUKUCZYZ.js → chunk-TQ2KTH4A.js} +2 -2
  86. package/dist/{chunk-4WOOTKFD.js → chunk-U3FETNQB.js} +341 -20
  87. package/dist/{chunk-5B3RTYAK.js → chunk-UGDSB4AN.js} +2 -2
  88. package/dist/{chunk-VKHLUZNO.js → chunk-UZ7YBL43.js} +3 -3
  89. package/dist/{chunk-QNUQ7K7D.js → chunk-VKMSEO7Y.js} +4 -4
  90. package/dist/{chunk-IN7DGBVS.js → chunk-VLX5VZ35.js} +5 -5
  91. package/dist/{chunk-SJGS3GDI.js → chunk-WAGBMMNX.js} +5 -5
  92. package/dist/{chunk-BFOSV5EZ.js → chunk-WX2YCH7F.js} +2 -2
  93. package/dist/{chunk-3LT34CAM.js → chunk-XBIGUILU.js} +32 -17
  94. package/dist/{chunk-XDHUDE5K.js → chunk-XKYBFRWR.js} +5 -5
  95. package/dist/{chunk-U5QU5ZOD.js → chunk-XYPWFSU5.js} +3 -3
  96. package/dist/{chunk-YBUECLAF.js → chunk-YEJQDODA.js} +2 -2
  97. package/dist/{chunk-YQ6XEFVK.js → chunk-Z74OGONW.js} +39 -7
  98. package/dist/{chunk-I4ELN5BX.js → chunk-ZTNOEJRI.js} +2 -2
  99. package/dist/cli/index.js +26 -26
  100. package/dist/{clio-K2PBVCEM.js → clio-4IKFE25F.js} +2 -2
  101. package/dist/{clio-context-tools-GR4OHFSO.js → clio-context-tools-OHJXYX4R.js} +25 -23
  102. package/dist/{code-nav-4X3OI6DK.js → code-nav-XPM3MY5I.js} +7 -7
  103. package/dist/{config-Q5J7UBAB.js → config-343YS25T.js} +43 -41
  104. package/dist/{config-graph-P3555574.js → config-graph-WI263KDX.js} +43 -41
  105. package/dist/{configure-KIJ32ZO6.js → configure-GZDG4HER.js} +21 -21
  106. package/dist/{context-LFBQOSXT.js → context-4OFO7N7Y.js} +12 -12
  107. package/dist/{context-WBH3KSVR.js → context-JLK2RLOG.js} +42 -40
  108. package/dist/{context-NNLBES2Y.js → context-O6DIWCF6.js} +23 -21
  109. package/dist/{context-clear-YOHMQ6SQ.js → context-clear-MR7KOGQI.js} +42 -40
  110. package/dist/{context-working-set-MRAEVDNV.js → context-working-set-YZAD3KWO.js} +14 -13
  111. package/dist/{data-tool-JJPBGMQJ.js → data-tool-EKD7DFHG.js} +7 -7
  112. package/dist/{detail-RHJOTGN4.js → detail-YZPHFEVJ.js} +43 -41
  113. package/dist/{dispatch-runner-NC7PYNIO.js → dispatch-runner-VKVXVTWJ.js} +44 -42
  114. package/dist/{doctor-S5KZ2GTN.js → doctor-53AVQXAO.js} +17 -16
  115. package/dist/{doctor-deep-2EJQSHIZ.js → doctor-deep-BLRB2NCG.js} +5 -5
  116. package/dist/{eval-3D6M36N7.js → eval-P3BQSLQE.js} +21 -20
  117. package/dist/{evidence-XNVPPLVK.js → evidence-6LLI6SZH.js} +44 -42
  118. package/dist/{evidence-6IYN52ET.js → evidence-HTN3JLUO.js} +42 -40
  119. package/dist/{evidence-JVECPI3F.js → evidence-NJRE3R5H.js} +42 -40
  120. package/dist/{evolve-TLPUDJDM.js → evolve-WWZHJVJP.js} +42 -40
  121. package/dist/{fleet-WKVBF5SZ.js → fleet-GGZ4BVJE.js} +64 -62
  122. package/dist/{fleet-6RJWIOEO.js → fleet-QG3IIWWM.js} +44 -42
  123. package/dist/{fleet-commands-L6WFT7KZ.js → fleet-commands-J2ZIKISQ.js} +7 -7
  124. package/dist/{fleet-decisions-BTQMWU5A.js → fleet-decisions-Y2Q7375Q.js} +8 -8
  125. package/dist/{fleet-graph-553DZ67Q.js → fleet-graph-TCBCCCBP.js} +11 -11
  126. package/dist/{fleet-inspect-236FZPRB.js → fleet-inspect-KOLKBC56.js} +44 -42
  127. package/dist/{fleet-preflight-NZQHABWO.js → fleet-preflight-44UNCC4Y.js} +27 -25
  128. package/dist/{fleet-validate-7NLAE26R.js → fleet-validate-K2FXRZVA.js} +13 -13
  129. package/dist/{fleet-verify-E3RO6A5W.js → fleet-verify-AS444WDW.js} +42 -40
  130. package/dist/{fleet-view-KMZQY7YJ.js → fleet-view-X4JU4J6O.js} +43 -41
  131. package/dist/gui/ops-worker.js +9 -9
  132. package/dist/gui/reads-worker.js +9 -8
  133. package/dist/{init-DV5B6HQG.js → init-VSOWFHVL.js} +54 -52
  134. package/dist/{interactive-OXGYZYAS.js → interactive-JRCBENLZ.js} +1733 -577
  135. package/dist/{interop-J7UOBOSF.js → interop-JR6BCY7R.js} +10 -10
  136. package/dist/{inventory-OEGIL6PY.js → inventory-3JUGLVJI.js} +43 -41
  137. package/dist/{library-ROZAMUIL.js → library-7BOH3Y7N.js} +15 -15
  138. package/dist/{library-B3UPGY4V.js → library-7PFONXUI.js} +9 -9
  139. package/dist/{library-MXQA5DLT.js → library-TUYZMFIA.js} +7 -7
  140. package/dist/{library-import-2LTWOVBD.js → library-import-TJVXKLDK.js} +10 -10
  141. package/dist/{library-inventory-LJ3XHLWO.js → library-inventory-MD3AYTP4.js} +9 -9
  142. package/dist/{library-validation-DARHHUNW.js → library-validation-ZDLITEA3.js} +7 -7
  143. package/dist/{mcp-2GSPYQ54.js → mcp-EFCWUPUR.js} +3 -3
  144. package/dist/{memory-J3STUUIK.js → memory-Q2YVOKK5.js} +42 -40
  145. package/dist/{models-Y3GMFJ2V.js → models-M7BOCW7F.js} +17 -16
  146. package/dist/{monitor-LHNH577J.js → monitor-2HD3MC67.js} +47 -45
  147. package/dist/{orchestrator-DTAPXULA.js → orchestrator-OUW3ZN6Y.js} +1894 -226
  148. package/dist/{panes-A743ONZ5.js → panes-TTPPSYCL.js} +3 -3
  149. package/dist/{preload-OJBMAIXF.js → preload-A47C2NUF.js} +42 -40
  150. package/dist/{providers-CMETPZNS.js → providers-T43W7EL4.js} +4 -4
  151. package/dist/{resources-FY6XYFN2.js → resources-TP6X2V6W.js} +20 -11
  152. package/dist/{run-H64MEVTA.js → run-LDSBPMCS.js} +61 -59
  153. package/dist/{share-CAFMMQLM.js → share-URHKKMHR.js} +9 -9
  154. package/dist/{skills-ERKQUC7L.js → skills-E477MOOO.js} +13 -11
  155. package/dist/{skills-eval-CQ23MYQK.js → skills-eval-AIBR2L7H.js} +583 -93
  156. package/dist/{skills-inventory-TVUYJDVN.js → skills-inventory-HLMKY3R5.js} +13 -11
  157. package/dist/{slash-commands-ROG3KIFJ.js → slash-commands-LIIZKA5Y.js} +28 -26
  158. package/dist/{startup-background-KDWCQMWT.js → startup-background-JR2KDK6R.js} +43 -41
  159. package/dist/{steer-KKYXJSH4.js → steer-D7RTAPCM.js} +3 -3
  160. package/dist/{system-ZDW44ZGX.js → system-X2P5N4IY.js} +14 -13
  161. package/dist/{targets-AJWA6E7Z.js → targets-VFFAKUFM.js} +25 -24
  162. package/dist/{tasks-6L2JJRC5.js → tasks-ERAWZA22.js} +6 -6
  163. package/dist/{terminal-lease-YFUSDPAD.js → terminal-lease-V2N2BIKQ.js} +2 -2
  164. package/dist/{trace-LA5MVR4Y.js → trace-MA5BT7XY.js} +3 -3
  165. package/dist/{usage-XSILDHKF.js → usage-WYFY2L6M.js} +50 -48
  166. package/dist/{verifiers-ECA5GIHL.js → verifiers-TYIW2XKF.js} +7 -7
  167. package/dist/{verify-R6QQCCEB.js → verify-242UFYTP.js} +6 -6
  168. package/dist/{web-fetch-PVZNMBOB.js → web-fetch-LRAGYU6Y.js} +3 -3
  169. package/dist/{wiki-generate-RP22WP5K.js → wiki-generate-OFKOX2T6.js} +53 -51
  170. package/dist/worker/entry.js +31 -29
  171. package/docs/architecture/architecture.md +1 -0
  172. package/docs/architecture/context-engine.md +4 -2
  173. package/docs/architecture/observability.md +6 -4
  174. package/docs/architecture/prompt-envelope-and-tools.md +1 -1
  175. package/docs/architecture/session-lifecycle.md +1 -1
  176. package/docs/architecture/tui-design.md +12 -9
  177. package/docs/gui/parity/02-slash-and-surfaces.md +1 -1
  178. package/docs/guide/commands-and-modes.md +91 -5
  179. package/docs/guide/configuration-and-targets.md +2 -2
  180. package/docs/guide/context-continuity.md +49 -0
  181. package/docs/guide/proactive-memory.md +6 -3
  182. package/library/registry.yaml +292 -32
  183. package/library/skills/README.md +27 -2
  184. package/library/skills/context/context-handoff/SKILL.md +10 -4
  185. package/library/skills/meta/clio-coder-dev/SKILL.md +92 -88
  186. package/library/skills/meta/clio-coder-dev/evals.md +51 -46
  187. package/library/skills/meta/clio-coder-dev/plugin.json +2 -2
  188. package/library/skills/meta/clio-coder-dev/references/change-map.md +49 -0
  189. package/library/skills/meta/clio-coder-test/SKILL.md +89 -142
  190. package/library/skills/meta/clio-coder-test/evals.md +58 -62
  191. package/library/skills/meta/clio-coder-test/plugin.json +2 -2
  192. package/library/skills/meta/clio-coder-test/references/harness.md +3 -3
  193. package/library/skills/meta/clio-coder-test/references/lifecycle-validation.md +30 -0
  194. package/library/skills/meta/clio-coder-test/references/test-map.md +60 -89
  195. package/library/skills/registry.yaml +5 -5
  196. package/library/skills/skill-marketplace.json +12 -14
  197. package/package.json +1 -1
  198. package/src/cli/skills-eval.ts +1064 -54
  199. package/src/cli/usage.ts +2 -2
  200. package/src/core/clio-repo.ts +3 -0
  201. package/src/core/tool-names.ts +1 -0
  202. package/src/domains/context/budget/inspection.ts +9 -0
  203. package/src/domains/context/budget/live-view.ts +418 -0
  204. package/src/domains/context/budget/pressure.ts +393 -0
  205. package/src/domains/context/budget/request-fit.ts +15 -0
  206. package/src/domains/evidence/build.ts +24 -0
  207. package/src/domains/gateway/mcp/client.ts +1 -1
  208. package/src/domains/memory/commit-state.ts +160 -0
  209. package/src/domains/memory/operations.ts +18 -10
  210. package/src/domains/memory/prompt-cache.ts +95 -0
  211. package/src/domains/memory/prompt-section.ts +50 -7
  212. package/src/domains/memory/relevance.ts +93 -0
  213. package/src/domains/memory/restoration.ts +78 -0
  214. package/src/domains/memory/store.ts +39 -1
  215. package/src/domains/middleware/memory-intervention.ts +116 -24
  216. package/src/domains/observability/background-memory-usage.ts +1 -1
  217. package/src/domains/observability/cost.ts +3 -3
  218. package/src/domains/observability/extension.ts +1 -1
  219. package/src/domains/observability/metrics.ts +1 -1
  220. package/src/domains/prompts/compiler.ts +1 -1
  221. package/src/domains/prompts/extension.ts +38 -1
  222. package/src/domains/quota/anthropic-max-provider.ts +134 -0
  223. package/src/domains/quota/anthropic-usage.ts +220 -0
  224. package/src/domains/quota/antigravity-provider.ts +339 -0
  225. package/src/domains/quota/cache.ts +87 -0
  226. package/src/domains/quota/claude-code-provider.ts +169 -0
  227. package/src/domains/quota/codex-provider.ts +237 -0
  228. package/src/domains/quota/presentation.ts +240 -0
  229. package/src/domains/quota/registry.ts +23 -0
  230. package/src/domains/quota/service.ts +93 -0
  231. package/src/domains/quota/summary-feed.ts +86 -0
  232. package/src/domains/quota/types.ts +84 -0
  233. package/src/domains/resources/index.ts +11 -0
  234. package/src/domains/resources/skills/catalog-view.ts +571 -0
  235. package/src/domains/resources/skills/lexical-match.ts +136 -0
  236. package/src/domains/resources/skills/loader.ts +38 -0
  237. package/src/domains/resources/skills/promotion.ts +1 -55
  238. package/src/domains/resources/skills/provenance-pin.ts +50 -20
  239. package/src/domains/safety/action-classifier.ts +1 -0
  240. package/src/domains/session/compaction/branch-summary.ts +4 -1
  241. package/src/domains/session/compaction/compact.ts +18 -2
  242. package/src/domains/session/compaction/cut-point.ts +20 -1
  243. package/src/domains/session/compaction/tokens.ts +48 -3
  244. package/src/domains/session/context-accounting.ts +8 -1
  245. package/src/domains/session/continuity/carry.ts +59 -0
  246. package/src/domains/session/continuity/contract.ts +592 -0
  247. package/src/domains/session/continuity/evidence.ts +290 -0
  248. package/src/domains/session/continuity/fold.ts +1075 -0
  249. package/src/domains/session/continuity/note.ts +79 -0
  250. package/src/domains/session/continuity/operator-request.ts +104 -0
  251. package/src/domains/session/continuity/persistence.ts +408 -0
  252. package/src/domains/session/continuity/ports.ts +231 -0
  253. package/src/domains/session/continuity/projection.ts +538 -0
  254. package/src/domains/session/continuity/validate.ts +352 -0
  255. package/src/domains/session/entries.ts +50 -3
  256. package/src/domains/session/index.ts +48 -0
  257. package/src/domains/session/migrations/index.ts +7 -3
  258. package/src/domains/session/tree/fork.ts +15 -1
  259. package/src/domains/session/usage.ts +2 -2
  260. package/src/engine/acp/commands.ts +1 -1
  261. package/src/engine/agent.ts +133 -9
  262. package/src/engine/session.ts +13 -6
  263. package/src/entry/orchestrator.ts +116 -33
  264. package/src/interactive/chat-loop-messages.ts +2 -2
  265. package/src/interactive/chat-loop.ts +219 -63
  266. package/src/interactive/chat-renderer.ts +99 -8
  267. package/src/interactive/context-overlay.ts +24 -3
  268. package/src/interactive/continuity-controller.ts +526 -0
  269. package/src/interactive/dispatch-board.ts +21 -3
  270. package/src/interactive/footer/dashboard.ts +25 -1
  271. package/src/interactive/footer/key-hints.ts +2 -2
  272. package/src/interactive/footer/pages.ts +70 -27
  273. package/src/interactive/footer/widgets.ts +32 -7
  274. package/src/interactive/footer-panel.ts +1 -1
  275. package/src/interactive/interactive-application.ts +7 -2
  276. package/src/interactive/interactive-input-runtime.ts +2 -2
  277. package/src/interactive/interactive-presentation.ts +15 -0
  278. package/src/interactive/interactive-slash-runtime.ts +45 -8
  279. package/src/interactive/interactive-tickers.ts +7 -1
  280. package/src/interactive/model-session-replay.ts +130 -3
  281. package/src/interactive/output-reserve.ts +35 -0
  282. package/src/interactive/overlay-general-openers.ts +12 -8
  283. package/src/interactive/overlay-key-routing.ts +6 -9
  284. package/src/interactive/overlay-lifecycle.ts +10 -6
  285. package/src/interactive/overlay-session-lifecycle.ts +21 -14
  286. package/src/interactive/quota-view.ts +229 -0
  287. package/src/interactive/session-last-turn.ts +1 -1
  288. package/src/interactive/session-transcript.ts +2 -1
  289. package/src/interactive/session-usage-reseed.ts +2 -2
  290. package/src/interactive/side-question.ts +2 -2
  291. package/src/interactive/slash-commands.ts +28 -9
  292. package/src/interactive/turn-context.ts +686 -63
  293. package/src/interactive/turn-middleware.ts +39 -6
  294. package/src/interactive/turn-persistence.ts +15 -2
  295. package/src/interactive/turn-prewarm.ts +1 -1
  296. package/src/interactive/turn-runtime.ts +48 -5
  297. package/src/interactive/{cost-overlay.ts → usage-overlay.ts} +121 -33
  298. package/src/interactive/welcome-dashboard.ts +26 -3
  299. package/src/tools/agent-tools.ts +3 -1
  300. package/src/tools/bootstrap.ts +6 -0
  301. package/src/tools/builtin-tool-catalog.ts +9 -0
  302. package/src/tools/context/index.ts +148 -113
  303. package/src/tools/context/surface.ts +11 -7
  304. package/src/tools/core-bootstrap.ts +4 -0
  305. package/src/tools/observation.ts +8 -2
  306. package/src/tools/policy.ts +3 -0
  307. package/src/tools/self-compact.ts +31 -0
  308. package/src/tools/surface.ts +1 -0
  309. package/src/tools/tasks.ts +1 -1
  310. package/dist/chunk-CTFPFW3H.js +0 -44
  311. package/dist/chunk-EOJPDNUP.js +0 -1325
  312. package/dist/chunk-I4Y4WKZR.js +0 -386
@@ -10,7 +10,7 @@
10
10
  *
11
11
  * The two writes are the same pair the side-question path makes: the
12
12
  * in-process cost tracker under the `background-memory` label, which is what
13
- * `/cost` folds, and one durable row in the out-of-turn usage store, which is
13
+ * `/usage` folds, and one durable row in the out-of-turn usage store, which is
14
14
  * what an archive reader folds after the process is gone.
15
15
  */
16
16
 
@@ -7,7 +7,7 @@
7
7
  *
8
8
  * Per-entry token breakdown (input/output/cacheRead/cacheWrite/reasoning)
9
9
  * matches the shape of pi-ai's `Usage` plus provider-specific reasoning detail
10
- * fields. The /cost overlay aggregates it via `aggregateCostEntries`; the TUI
10
+ * fields. The /usage overlay aggregates it via `aggregateCostEntries`; the TUI
11
11
  * footer consumes the session sum through `ObservabilityContract.sessionTokens()`.
12
12
  */
13
13
 
@@ -92,7 +92,7 @@ export function formatCostAggregate(cost: CostAggregate | null | undefined): str
92
92
 
93
93
  /**
94
94
  * What a fixed-width surface says when there is no cost claim. The footer and
95
- * `/cost` drop their cost field instead; a table cell that owns a column cannot,
95
+ * `/usage` drop their cost field instead; a table cell that owns a column cannot,
96
96
  * so it says the same thing in words rather than inventing `$0.00`.
97
97
  */
98
98
  export const COST_NOT_MEASURED = "not measured";
@@ -134,7 +134,7 @@ export interface UsageBreakdown {
134
134
  }
135
135
 
136
136
  /**
137
- * What produced a priced call, when it was not an ordinary turn. `/cost` and
137
+ * What produced a priced call, when it was not an ordinary turn. `/usage` and
138
138
  * the usage surfaces separate these out so an operator can see that money was
139
139
  * spent beside the session rather than inside it.
140
140
  */
@@ -53,7 +53,7 @@ function recordDispatchCost(
53
53
  }
54
54
  telemetry.record("counter", "tokens.total", payload.tokenCount);
55
55
  // Dispatch terminal payloads carry the same full split as receipts. Preserve
56
- // it so /cost and the footer agree with the fleet board instead of showing
56
+ // it so /usage and the footer agree with the fleet board instead of showing
57
57
  // zero input/output/cache for worker-only sessions.
58
58
  cost.accumulate(
59
59
  payload.targetId,
@@ -1,5 +1,5 @@
1
1
  /**
2
- * Thin reducer over TelemetrySnapshot. Produces a view the TUI + /cost overlay
2
+ * Thin reducer over TelemetrySnapshot. Produces a view the TUI + /usage overlay
3
3
  * consume. Stays pure so tests can hand-roll snapshots and assert the shape.
4
4
  */
5
5
 
@@ -256,7 +256,7 @@ function sessionHasContext(inputs: SessionPromptInputs): boolean {
256
256
  return toolSurfaceHasTool(inputs.toolNames, "context") && turnAllowsTool(inputs.turnConstraints, "context");
257
257
  }
258
258
 
259
- function sessionCanUseSkills(inputs: SessionPromptInputs): boolean {
259
+ export function sessionCanUseSkills(inputs: SessionPromptInputs): boolean {
260
260
  return (
261
261
  sessionHasContext(inputs) &&
262
262
  inputs.skillDiscoveryEnabled !== false &&
@@ -18,7 +18,14 @@ import {
18
18
  selectActiveRules,
19
19
  } from "../context/index.js";
20
20
  import { detectRunIdentity } from "../dispatch/run-identity.js";
21
- import { compile, compileWorker, type RenderedPromptFragment } from "./compiler.js";
21
+ import { isAutonomyLevel, modelMayActivateSkills } from "../safety/autonomy.js";
22
+ import {
23
+ compile,
24
+ compileWorker,
25
+ type RenderedPromptFragment,
26
+ type SessionPromptInputs,
27
+ sessionCanUseSkills,
28
+ } from "./compiler.js";
22
29
  import type { CompileSessionPromptInput, CompileWorkerPromptInput, PromptsContract } from "./contract.js";
23
30
  import { type FragmentTable, loadFragments } from "./fragment-loader.js";
24
31
  import { sha256 } from "./hash.js";
@@ -182,6 +189,7 @@ export function createPromptsBundle(
182
189
  additionalFragments: [
183
190
  ...sources.workspaceRoot,
184
191
  ...sources.clioRepoAwareness,
192
+ ...selfDevelopmentSkillFragments(sources.clioRepoAwareness.length > 0, sessionInputs, safety),
185
193
  ...renderCustomizationFragments(sources.customization, cwd, input.workingContextPaths ?? []).fragments,
186
194
  ],
187
195
  });
@@ -372,6 +380,7 @@ function clioRepoAwarenessFragments(cwd: string): RenderedPromptFragment[] {
372
380
  const body = [
373
381
  "# Clio Source Tree",
374
382
  "This workspace is Clio Coder's own source tree.",
383
+ `Source repository root: ${JSON.stringify(awareness.repoRoot)}.`,
375
384
  "When running inside this repo, Clio can modify her own TUI, skills, agents, tools, prompts, context/bootstrap, and harness as ordinary local source work when the user asks.",
376
385
  "Shared contribution/publishing/push/PR/release requires explicit user intent and normal Git/GitHub etiquette. Do not imply autonomous publishing.",
377
386
  ].join("\n");
@@ -385,3 +394,31 @@ function clioRepoAwarenessFragments(cwd: string): RenderedPromptFragment[] {
385
394
  },
386
395
  ];
387
396
  }
397
+
398
+ /** A small task-aware nudge; actual loading still uses normal skill admission. */
399
+ function selfDevelopmentSkillFragments(
400
+ selfRepo: boolean,
401
+ inputs: SessionPromptInputs,
402
+ autonomy: string,
403
+ ): RenderedPromptFragment[] {
404
+ if (!selfRepo || !sessionCanUseSkills(inputs) || inputs.turnConstraints?.mode === "proposal") return [];
405
+ const activation = isAutonomyLevel(autonomy) && modelMayActivateSkills(autonomy);
406
+ const body = [
407
+ "# Self-development skills",
408
+ "For a task that changes Clio's source, harness, prompts, or library, use clio-coder-dev before editing and clio-coder-test when choosing validation. Skip this workflow for unrelated or self-contained questions.",
409
+ 'These two skills are discoverable from this checkout\'s library/skills/meta when no installed package owns their names. Check context(scope="skills") for current readiness; disabled, damaged, or hidden skills stay unavailable.',
410
+ activation
411
+ ? 'Load each relevant ready skill with context(scope="skills", name="clio-coder-dev") or context(scope="skills", name="clio-coder-test") as needed, without waiting for a separate skill request. Reuse already loaded guidance; do not load every reference or the whole catalog.'
412
+ : "Only the operator activates skills at this autonomy level. Suggest /skill clio-coder-dev or /skill clio-coder-test when relevant, then continue permitted work without waiting or bypassing the activation gate.",
413
+ "Read CONTRIBUTING.md and the assigned sprint packet from the detected repository root. A plan or skill does not launch an unapproved implementation sprint or authorize publication.",
414
+ ].join("\n");
415
+ return [
416
+ {
417
+ id: "context.self-development-skills",
418
+ relPath: "inline/self-development-skills",
419
+ body,
420
+ contentHash: sha256(body),
421
+ dynamic: true,
422
+ },
423
+ ];
424
+ }
@@ -0,0 +1,134 @@
1
+ /**
2
+ * Quota adapter for the `anthropic-max` runtime, the account Clio itself
3
+ * spends when it runs a native turn.
4
+ *
5
+ * Unlike `claude-code`, this credential belongs to Clio: it is the OAuth
6
+ * record her own auth storage persists under the `anthropic` provider id.
7
+ * The adapter reads the stored credential without triggering a refresh,
8
+ * because a quota read must never mutate an authentication record.
9
+ *
10
+ * Both adapters can point at the same Anthropic account. That is a display
11
+ * concern rather than a correctness one: each reports what its own
12
+ * credential sees, and a consumer that shows both should say which
13
+ * credential produced which row.
14
+ */
15
+
16
+ import { openAuthStorage } from "../providers/auth/index.js";
17
+ import { fetchAnthropicUsage } from "./anthropic-usage.js";
18
+ import type { QuotaProvider, UsageSnapshot } from "./types.js";
19
+
20
+ export const ANTHROPIC_MAX_QUOTA_PROVIDER_ID = "anthropic-max";
21
+
22
+ /** The provider id Clio's auth storage files an Anthropic OAuth credential under. */
23
+ const AUTH_PROVIDER_ID = "anthropic";
24
+ const DISPLAY_NAME = "Anthropic Max";
25
+ const REQUEST_TIMEOUT_MS = 15_000;
26
+
27
+ /** The subset of Clio's stored OAuth credential this adapter needs. */
28
+ export interface AnthropicMaxCredentials {
29
+ accessToken: string;
30
+ /** Epoch milliseconds the access token expires, or null when the record omits it. */
31
+ expiresAtMs: number | null;
32
+ hasRefreshToken: boolean;
33
+ }
34
+
35
+ export interface AnthropicMaxQuotaOptions {
36
+ readCredentials?: () => Promise<AnthropicMaxCredentials | null>;
37
+ fetch?: typeof globalThis.fetch;
38
+ timeoutMs?: number;
39
+ now?: () => Date;
40
+ }
41
+
42
+ /** Shape a stored auth credential into the fields a usage read needs. */
43
+ export function parseStoredAnthropicCredential(raw: unknown): AnthropicMaxCredentials | null {
44
+ if (!isRecord(raw) || raw.type !== "oauth") return null;
45
+ const access = raw.access;
46
+ if (typeof access !== "string" || access.length === 0) return null;
47
+ const refresh = raw.refresh;
48
+ return {
49
+ accessToken: access,
50
+ expiresAtMs: asNumber(raw.expires),
51
+ hasRefreshToken: typeof refresh === "string" && refresh.length > 0,
52
+ };
53
+ }
54
+
55
+ /**
56
+ * Read Clio's stored Anthropic credential.
57
+ *
58
+ * `AuthStorage.get` is the accessor that does no refresh, unlike
59
+ * `resolveApiKey`, which renews an expiring credential as a side effect.
60
+ */
61
+ async function readStoredCredential(): Promise<AnthropicMaxCredentials | null> {
62
+ try {
63
+ return parseStoredAnthropicCredential(openAuthStorage().get(AUTH_PROVIDER_ID));
64
+ } catch {
65
+ return null;
66
+ }
67
+ }
68
+
69
+ /** Build the read-only `anthropic-max` quota adapter. */
70
+ export function createAnthropicMaxQuotaProvider(options: AnthropicMaxQuotaOptions = {}): QuotaProvider {
71
+ const readCredentials = options.readCredentials ?? readStoredCredential;
72
+ const doFetch = options.fetch ?? globalThis.fetch;
73
+ const timeoutMs = options.timeoutMs ?? REQUEST_TIMEOUT_MS;
74
+ const now = options.now ?? (() => new Date());
75
+
76
+ const snapshot = (
77
+ status: UsageSnapshot["status"],
78
+ message: string | null,
79
+ extra: Partial<UsageSnapshot> = {},
80
+ ): UsageSnapshot => ({
81
+ providerId: ANTHROPIC_MAX_QUOTA_PROVIDER_ID,
82
+ displayName: DISPLAY_NAME,
83
+ status,
84
+ windows: [],
85
+ message,
86
+ fetchedAt: now().toISOString(),
87
+ ...extra,
88
+ });
89
+
90
+ return {
91
+ id: ANTHROPIC_MAX_QUOTA_PROVIDER_ID,
92
+ displayName: DISPLAY_NAME,
93
+
94
+ async detect(): Promise<boolean> {
95
+ return (await readCredentials()) !== null;
96
+ },
97
+
98
+ async fetch(): Promise<UsageSnapshot> {
99
+ const credentials = await readCredentials();
100
+ if (credentials === null) {
101
+ return snapshot("no_credentials", "Clio holds no Anthropic OAuth credential");
102
+ }
103
+ if (credentials.expiresAtMs !== null && credentials.expiresAtMs <= now().getTime()) {
104
+ return snapshot(
105
+ "expired",
106
+ credentials.hasRefreshToken
107
+ ? "Clio's Anthropic token expired; the next turn refreshes it"
108
+ : "Run clio-coder auth login anthropic again",
109
+ );
110
+ }
111
+
112
+ const usage = await fetchAnthropicUsage(credentials.accessToken, { fetch: doFetch, timeoutMs });
113
+ if (usage.status !== "ok") {
114
+ return snapshot(usage.status, usage.message ?? "Run clio-coder auth login anthropic again", {
115
+ retryAfterSeconds: usage.retryAfterSeconds,
116
+ });
117
+ }
118
+ return snapshot("ok", null, { windows: usage.windows, credits: usage.credits });
119
+ },
120
+ };
121
+ }
122
+
123
+ function isRecord(value: unknown): value is Record<string, unknown> {
124
+ return typeof value === "object" && value !== null && !Array.isArray(value);
125
+ }
126
+
127
+ function asNumber(value: unknown): number | null {
128
+ if (typeof value === "number") return Number.isFinite(value) ? value : null;
129
+ if (typeof value === "string") {
130
+ const parsed = Number.parseFloat(value.trim());
131
+ return Number.isFinite(parsed) ? parsed : null;
132
+ }
133
+ return null;
134
+ }
@@ -0,0 +1,220 @@
1
+ /**
2
+ * Shared reader for Anthropic's OAuth usage endpoint.
3
+ *
4
+ * Two runtimes spend an Anthropic subscription: the `claude-code` CLI with
5
+ * its own stored login, and `anthropic-max` with the OAuth credential Clio
6
+ * itself holds. The account surface is the same endpoint and the same two
7
+ * response shapes, so only the token source differs. This module owns the
8
+ * request and the parsing; each adapter owns its credential.
9
+ *
10
+ * The endpoint has shipped both a `limits[]` array and flat
11
+ * `five_hour`/`seven_day` buckets, and has been observed serving both at
12
+ * once. The array wins when present because only it carries severity,
13
+ * the active flag, and per-model scope.
14
+ */
15
+
16
+ import { parseRetryAfterSeconds, type UsageCredits, type UsageSnapshot, type UsageWindow } from "./types.js";
17
+
18
+ export const ANTHROPIC_USAGE_URL = "https://api.anthropic.com/api/oauth/usage";
19
+ export const ANTHROPIC_OAUTH_BETA = "oauth-2025-04-20";
20
+
21
+ const WINDOW_ORDER: Record<string, number> = { session: 0, weekly: 1 };
22
+
23
+ /** What one usage read produced, before an adapter stamps its own identity on it. */
24
+ export interface AnthropicUsageOutcome {
25
+ status: UsageSnapshot["status"];
26
+ message: string | null;
27
+ windows: UsageWindow[];
28
+ credits: UsageCredits | null;
29
+ retryAfterSeconds: number | null;
30
+ }
31
+
32
+ export interface AnthropicUsageDependencies {
33
+ fetch: typeof globalThis.fetch;
34
+ timeoutMs: number;
35
+ }
36
+
37
+ function outcome(
38
+ status: UsageSnapshot["status"],
39
+ message: string | null,
40
+ extra: Partial<AnthropicUsageOutcome> = {},
41
+ ): AnthropicUsageOutcome {
42
+ return { status, message, windows: [], credits: null, retryAfterSeconds: null, ...extra };
43
+ }
44
+
45
+ /** Read current Anthropic usage with a bearer token, reporting failure as a status. */
46
+ export async function fetchAnthropicUsage(
47
+ token: string,
48
+ dependencies: AnthropicUsageDependencies,
49
+ ): Promise<AnthropicUsageOutcome> {
50
+ const controller = new AbortController();
51
+ const timer = setTimeout(() => controller.abort(), dependencies.timeoutMs);
52
+ let response: Response;
53
+ try {
54
+ response = await dependencies.fetch(ANTHROPIC_USAGE_URL, {
55
+ method: "GET",
56
+ headers: {
57
+ Authorization: `Bearer ${token}`,
58
+ "anthropic-beta": ANTHROPIC_OAUTH_BETA,
59
+ },
60
+ signal: controller.signal,
61
+ });
62
+ } catch (error) {
63
+ return outcome("error", describeError(error, dependencies.timeoutMs));
64
+ } finally {
65
+ clearTimeout(timer);
66
+ }
67
+
68
+ if (response.status === 401 || response.status === 403) {
69
+ await discardBody(response);
70
+ return outcome("expired", null);
71
+ }
72
+ if (response.status === 429) {
73
+ await discardBody(response);
74
+ return outcome("error", "HTTP 429", {
75
+ retryAfterSeconds: parseRetryAfterSeconds(response.headers.get("Retry-After")),
76
+ });
77
+ }
78
+ if (response.status !== 200) {
79
+ await discardBody(response);
80
+ return outcome("error", `HTTP ${response.status}`);
81
+ }
82
+
83
+ let body: unknown;
84
+ try {
85
+ body = await response.json();
86
+ } catch {
87
+ return outcome("error", "Unexpected usage response");
88
+ }
89
+ if (!isRecord(body)) {
90
+ return outcome("error", "Unexpected usage response");
91
+ }
92
+
93
+ return outcome("ok", null, { windows: usageWindows(body), credits: usageCredits(body.spend) });
94
+ }
95
+
96
+ /** Windows from `limits[]` when the endpoint sends it, otherwise the flat buckets. */
97
+ function usageWindows(body: Record<string, unknown>): UsageWindow[] {
98
+ const fromLimits = windowsFromLimits(body.limits);
99
+ const windows = fromLimits.length > 0 ? fromLimits : windowsFromFlat(body);
100
+ return windows.sort((left, right) => {
101
+ const order = (WINDOW_ORDER[left.key] ?? 2) - (WINDOW_ORDER[right.key] ?? 2);
102
+ return order !== 0 ? order : left.label.localeCompare(right.label);
103
+ });
104
+ }
105
+
106
+ function windowsFromLimits(raw: unknown): UsageWindow[] {
107
+ if (!Array.isArray(raw)) return [];
108
+ const windows: UsageWindow[] = [];
109
+ for (const entry of raw) {
110
+ if (!isRecord(entry)) continue;
111
+ const percent = asNumber(entry.percent);
112
+ if (percent === null) continue;
113
+ const common: Pick<UsageWindow, "usedPct" | "resetsAt" | "severity" | "active"> = {
114
+ usedPct: percent,
115
+ resetsAt: asIsoString(entry.resets_at),
116
+ };
117
+ if (typeof entry.severity === "string") common.severity = entry.severity;
118
+ if (entry.is_active === true) common.active = true;
119
+ if (entry.kind === "session") {
120
+ windows.push({ key: "session", label: "5h", short: "5h", ...common });
121
+ } else if (entry.kind === "weekly_all") {
122
+ windows.push({ key: "weekly", label: "Weekly", short: "wk", ...common });
123
+ } else if (entry.kind === "weekly_scoped") {
124
+ const name = scopeName(entry.scope);
125
+ if (name === null) continue;
126
+ windows.push({ key: `weekly_scoped.${slug(name)}`, label: name, scope: name, ...common });
127
+ }
128
+ }
129
+ return windows;
130
+ }
131
+
132
+ function windowsFromFlat(body: Record<string, unknown>): UsageWindow[] {
133
+ const windows: UsageWindow[] = [];
134
+ const fiveHour = bucketWindow(body.five_hour, "session", "5h", "5h");
135
+ if (fiveHour !== null) windows.push(fiveHour);
136
+ const sevenDay = bucketWindow(body.seven_day, "weekly", "Weekly", "wk");
137
+ if (sevenDay !== null) windows.push(sevenDay);
138
+ return windows;
139
+ }
140
+
141
+ function bucketWindow(raw: unknown, key: string, label: string, short: string): UsageWindow | null {
142
+ if (!isRecord(raw)) return null;
143
+ const usedPct = asNumber(raw.utilization);
144
+ if (usedPct === null) return null;
145
+ return { key, label, short, usedPct, resetsAt: asIsoString(raw.resets_at) };
146
+ }
147
+
148
+ function scopeName(raw: unknown): string | null {
149
+ if (!isRecord(raw)) return null;
150
+ const model = raw.model;
151
+ if (!isRecord(model)) return null;
152
+ const name = typeof model.display_name === "string" ? model.display_name.trim() : "";
153
+ if (name.length === 0) return null;
154
+ return slug(name) === "all-models" || slug(name) === "all" ? null : name;
155
+ }
156
+
157
+ /** Credit balance from the `spend` block, present only for credit-billed plans. */
158
+ function usageCredits(raw: unknown): UsageCredits | null {
159
+ if (!isRecord(raw) || raw.enabled !== true) return null;
160
+ const percent = asNumber(raw.percent);
161
+ const limit = majorAmount(raw.limit);
162
+ if (limit === null) {
163
+ return percent === null ? null : { display: `${percent.toFixed(0)}% used`, usedPct: percent };
164
+ }
165
+ const used = majorAmount(raw.used);
166
+ if (used === null) return null;
167
+ const currency = isRecord(raw.limit) && typeof raw.limit.currency === "string" ? raw.limit.currency : "USD";
168
+ const symbol = currency === "USD" ? "$" : "";
169
+ const usedPct = percent ?? (limit > 0 ? (used / limit) * 100 : null);
170
+ return { display: `${symbol}${used.toFixed(2)} / ${symbol}${limit.toFixed(2)}`, usedPct };
171
+ }
172
+
173
+ function majorAmount(raw: unknown): number | null {
174
+ if (!isRecord(raw)) return null;
175
+ const minor = asNumber(raw.amount_minor);
176
+ if (minor === null) return null;
177
+ const exponent = asNumber(raw.exponent) ?? 0;
178
+ return minor / 10 ** exponent;
179
+ }
180
+
181
+ function isRecord(value: unknown): value is Record<string, unknown> {
182
+ return typeof value === "object" && value !== null && !Array.isArray(value);
183
+ }
184
+
185
+ function asNumber(value: unknown): number | null {
186
+ if (typeof value === "number") return Number.isFinite(value) ? value : null;
187
+ if (typeof value === "string") {
188
+ const parsed = Number.parseFloat(value.trim());
189
+ return Number.isFinite(parsed) ? parsed : null;
190
+ }
191
+ return null;
192
+ }
193
+
194
+ function asIsoString(value: unknown): string | null {
195
+ if (typeof value !== "string" || value.trim().length === 0) return null;
196
+ const parsed = new Date(value);
197
+ return Number.isNaN(parsed.getTime()) ? null : parsed.toISOString();
198
+ }
199
+
200
+ function slug(value: string): string {
201
+ return value
202
+ .toLowerCase()
203
+ .replace(/[^a-z0-9]+/g, "-")
204
+ .replace(/^-+|-+$/g, "");
205
+ }
206
+
207
+ function describeError(error: unknown, timeoutMs: number): string {
208
+ if (error instanceof Error) {
209
+ return error.name === "AbortError" ? `timeout after ${timeoutMs}ms` : error.message;
210
+ }
211
+ return String(error);
212
+ }
213
+
214
+ async function discardBody(response: Response): Promise<void> {
215
+ try {
216
+ await response.body?.cancel();
217
+ } catch {
218
+ return;
219
+ }
220
+ }