acrylblends 1.0.4 → 1.0.6

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 (351) hide show
  1. package/docs/framework/agents/index.md +7 -5
  2. package/lib/accept-CNNHu4Cf.js +193 -0
  3. package/lib/{accepts-XBuZPVRI.js → accepts-Btq83xPd.js} +1 -1
  4. package/lib/accepts.js +1 -1
  5. package/lib/acryl-base.js +1 -331
  6. package/lib/adapter-CaZ6FZUU.js +37 -0
  7. package/lib/adapter.js +1 -36
  8. package/lib/{app-BM0rTCp7.js → app-CQcGWjrk.js} +4 -4
  9. package/lib/app-base-CpRMZL4v.js +332 -0
  10. package/lib/{audit-D5z9uwOP.d.ts → audit-GcLtvNLV.d.ts} +4 -2
  11. package/lib/{base-NSN_SNv-.js → base-D_6kfOr-.js} +1 -1
  12. package/lib/basic-auth-C0HTRpXh.js +20 -0
  13. package/lib/basic-auth-DFLflXNe.js +95 -0
  14. package/lib/basic-auth.js +1 -94
  15. package/lib/bearer-auth-Cwxt-vgS.js +94 -0
  16. package/lib/bearer-auth.js +1 -93
  17. package/lib/body-D7f4e7w_.js +94 -0
  18. package/lib/body-limit-WmIjWRLt.js +66 -0
  19. package/lib/body-limit.js +1 -65
  20. package/lib/buffer-CkEOJCcY.js +51 -0
  21. package/lib/bun.js +3 -3
  22. package/lib/cache-B0bHH7s5.js +169 -0
  23. package/lib/cache.js +1 -168
  24. package/lib/{client-DDLXwhBo.js → client-BrMFnMEd.js} +1 -1
  25. package/lib/{client-modules-CLvWOjbd.js → client-modules-drj4mjhj.js} +4 -4
  26. package/lib/client.js +1 -1
  27. package/lib/color-COa3DplO.js +37 -0
  28. package/lib/combine-TWuGdcbC.js +137 -0
  29. package/lib/combine.js +1 -136
  30. package/lib/{components-cEW3ovK0.js → components-B2ka9Ax1.js} +4 -4
  31. package/lib/{components-JXVm45kK.js → components-PVXwaOaC.js} +2 -2
  32. package/lib/compress-BzWqsHF1.js +79 -0
  33. package/lib/compress-C-9vK4B2.d.ts +11 -0
  34. package/lib/compress-DCyrrQ5D.js +11 -0
  35. package/lib/compress.d.ts +1 -1
  36. package/lib/compress.js +2 -78
  37. package/lib/concurrent-DrF3d2c-.js +30 -0
  38. package/lib/conninfo-BCROmdnf.d.ts +11 -0
  39. package/lib/conninfo-CWJwieh4.js +19 -0
  40. package/lib/constants-CBSEudrb.js +7 -0
  41. package/lib/{context-DDO0cfuP.js → context-CkOh7Ygv.js} +2 -2
  42. package/lib/context-storage-iuPoI_CR.js +52 -0
  43. package/lib/context-storage.js +1 -51
  44. package/lib/cookie-B1LlkzOe.js +125 -0
  45. package/lib/cookie-BHIduy2T.js +82 -0
  46. package/lib/cookie.js +1 -81
  47. package/lib/cordis/db-sqlite.d.ts +1 -1
  48. package/lib/cordis/door.d.ts +99 -5
  49. package/lib/cordis/door.js +562 -20
  50. package/lib/cordis/inspect.js +4 -4
  51. package/lib/cordis/kv-db.d.ts +1 -1
  52. package/lib/cordis/kv-db.js +1 -1
  53. package/lib/cordis/kv-json.d.ts +1 -1
  54. package/lib/cordis/kv-json.js +1 -1
  55. package/lib/cordis/node-server.d.ts +1 -1
  56. package/lib/cordis/node-server.js +3 -116
  57. package/lib/cordis/router-http.js +2 -2
  58. package/lib/cordis/router.js +4 -4
  59. package/lib/cordis/state.js +7 -7
  60. package/lib/{core-CaY8XDZ6.js → core-BOu1jYua.js} +1 -1
  61. package/lib/cors-CbAQ-8qU.js +106 -0
  62. package/lib/cors.js +1 -105
  63. package/lib/crypto-iVP-N-tS.js +34 -0
  64. package/lib/csrf-DqqOZtgy.js +101 -0
  65. package/lib/csrf.js +1 -100
  66. package/lib/css-C5CPVMvM.js +134 -0
  67. package/lib/css.js +2 -133
  68. package/lib/db/sqlite.d.ts +1 -25
  69. package/lib/dev-CLgu-JRe.js +50 -0
  70. package/lib/dev.js +1 -49
  71. package/lib/dom-YjND9Jt3.js +74 -0
  72. package/lib/door/host.d.ts +2 -2
  73. package/lib/door/host.js +1 -1
  74. package/lib/door/page.d.ts +3 -1
  75. package/lib/door/page.js +2 -2
  76. package/lib/door/protocol.d.ts +1 -1
  77. package/lib/door/protocol.js +1 -212
  78. package/lib/encode-BhbNLrjp.js +33 -0
  79. package/lib/factory-CLdSf-xS.js +30 -0
  80. package/lib/factory.js +1 -29
  81. package/lib/handler-C0wZ0KwO.js +12 -0
  82. package/lib/helpers-BEjcdbgw.js +5 -0
  83. package/lib/{hooks-w72iLeFR.js → hooks-CfC07ri0.js} +1 -1
  84. package/lib/html-CgNa89xw.js +31 -0
  85. package/lib/html-DGExBFNg.js +103 -0
  86. package/lib/html.d.ts +2 -5
  87. package/lib/html.js +2 -30
  88. package/lib/http-exception-MjWkT89V.js +54 -0
  89. package/lib/http-exception.js +1 -53
  90. package/lib/index-dAzQsJG9.d.ts +6 -0
  91. package/lib/index.js +3 -3
  92. package/lib/ip-restriction-jKtKCRM_.js +166 -0
  93. package/lib/ip-restriction.js +1 -165
  94. package/lib/ipaddr-DCoIo528.js +238 -0
  95. package/lib/jsx/dom/client.js +1 -1
  96. package/lib/jsx/dom/jsx-dev-runtime.js +1 -1
  97. package/lib/jsx/dom/jsx-runtime.js +2 -1
  98. package/lib/jsx/dom/server.js +3 -3
  99. package/lib/jsx/dom.js +5 -73
  100. package/lib/jsx/jsx-dev-runtime.d.ts +2 -5
  101. package/lib/jsx/jsx-dev-runtime.js +2 -13
  102. package/lib/jsx/jsx-runtime.d.ts +2 -2
  103. package/lib/jsx/jsx-runtime.js +4 -4
  104. package/lib/jsx/streaming.js +1 -1
  105. package/lib/jsx-CUQgttxU.js +47 -0
  106. package/lib/jsx-dev-runtime-B4WTyq8r.js +14 -0
  107. package/lib/{jsx-dev-runtime-CWLRA2F3.js → jsx-dev-runtime-BLfA-EhI.js} +2 -2
  108. package/lib/jsx-dev-runtime-CGt4kNYr.d.ts +6 -0
  109. package/lib/jsx-renderer-CfifM4hT.js +99 -0
  110. package/lib/jsx-renderer.js +1 -98
  111. package/lib/jsx-runtime-COCRWBJ9.js +2 -0
  112. package/lib/jsx.js +5 -46
  113. package/lib/{jwk-LkzeoaU8.js → jwk-CDPpYQpD.js} +3 -3
  114. package/lib/jwk.js +1 -1
  115. package/lib/{jwt-BGJBw5AF.js → jwt-BXoIA2fQ.js} +2 -2
  116. package/lib/{jwt-CskdhEW9.js → jwt-lyS4Q3mT.js} +3 -3
  117. package/lib/jwt.js +2 -2
  118. package/lib/kv/db.d.ts +1 -10
  119. package/lib/kv/db.js +1 -56
  120. package/lib/kv/json.d.ts +1 -9
  121. package/lib/kv/json.js +1 -223
  122. package/lib/{language-D9a4zqvn.js → language-DeGRtLJt.js} +2 -2
  123. package/lib/language.js +1 -1
  124. package/lib/{linear-router-C3FPib6h.js → linear-router-CmZn_JLV.js} +2 -2
  125. package/lib/logger-Ckxu8diz.js +54 -0
  126. package/lib/logger.js +1 -53
  127. package/lib/loopback-only-BbasoF7R.js +90 -0
  128. package/lib/loopback-only.js +1 -89
  129. package/lib/method-not-allowed-TO2_hnfl.js +82 -0
  130. package/lib/method-not-allowed.js +1 -81
  131. package/lib/method-override-DnZKfJbV.js +103 -0
  132. package/lib/method-override.js +1 -102
  133. package/lib/middleware-BfG_fp8P.js +12 -0
  134. package/lib/mime-BnScXkQn.js +77 -0
  135. package/lib/mount-QkK_3NtJ.js +67 -0
  136. package/lib/mount.js +1 -66
  137. package/lib/node-server/conninfo.d.ts +1 -10
  138. package/lib/node-server/conninfo.js +1 -18
  139. package/lib/node-server/register.js +25 -2
  140. package/lib/node-server/serve-static.d.ts +1 -17
  141. package/lib/node-server/serve-static.js +1 -1
  142. package/lib/node-server.d.ts +2 -2
  143. package/lib/node-server.js +4 -4
  144. package/lib/open-BqK4D2Wn.d.ts +11 -0
  145. package/lib/open-But9E2rL.js +57 -0
  146. package/lib/open-CULVJgOc.d.ts +12 -0
  147. package/lib/open-Dp4sxspc.js +224 -0
  148. package/lib/open-T7kgbO2O.d.ts +27 -0
  149. package/lib/{pattern-router-HZxjDGEQ.js → pattern-router-CJBecJBd.js} +1 -1
  150. package/lib/powered-by-CXQNjNIX.js +24 -0
  151. package/lib/powered-by.js +1 -23
  152. package/lib/pretty-json-CeC1yvYt.js +39 -0
  153. package/lib/pretty-json.js +1 -38
  154. package/lib/{protocol-C37gPePD.d.ts → protocol-CR31M7K_.d.ts} +2 -0
  155. package/lib/protocol-Dt0AZBOz.js +213 -0
  156. package/lib/proxy-C2w_mH7k.js +121 -0
  157. package/lib/proxy.js +1 -120
  158. package/lib/quick.js +4 -4
  159. package/lib/{reg-exp-router-DC1R3qoY.js → reg-exp-router-C_WV84eV.js} +2 -2
  160. package/lib/request-DvMgGtlW.js +323 -0
  161. package/lib/request.js +1 -322
  162. package/lib/route-D8m-a2I1.js +126 -0
  163. package/lib/route.js +1 -125
  164. package/lib/router/linear-router.js +1 -1
  165. package/lib/router/pattern-router.js +1 -1
  166. package/lib/router/reg-exp-router.js +1 -1
  167. package/lib/router/smart-router.js +1 -1
  168. package/lib/router/trie-router.js +1 -1
  169. package/lib/router-Be_o6nFu.js +35 -0
  170. package/lib/router.js +1 -34
  171. package/lib/schema-validator-EK3cUUCt.js +22 -0
  172. package/lib/schema-validator.js +1 -21
  173. package/lib/{secure-headers-mP3j6Hm6.js → secure-headers-C5ZOcXqk.js} +1 -1
  174. package/lib/secure-headers.js +1 -1
  175. package/lib/serve-static-B7sqnftW.d.ts +18 -0
  176. package/lib/{serve-static-3uo_9rjF.js → serve-static-C1Nkj_xB.js} +2 -2
  177. package/lib/{serve-static-Cp5adzGo.js → serve-static-EiJf1zkO.js} +4 -4
  178. package/lib/serve-static.js +1 -1
  179. package/lib/{smart-router-C5CZ5XSM.js → smart-router-BjxkDQCE.js} +1 -1
  180. package/lib/{snapshot-text-BLz8nlSc.js → snapshot-text-CtQm3n8n.js} +23 -14
  181. package/lib/{src-dy3Fk9tA.js → src-BTBm4OPD.js} +6 -6
  182. package/lib/src-LX0E4ec4.js +117 -0
  183. package/lib/{ssg-BwjcA-Ge.js → ssg-BEl7MHqg.js} +5 -5
  184. package/lib/ssg.js +1 -1
  185. package/lib/stream-CzM_SHkq.js +88 -0
  186. package/lib/{streaming-DHST3Scg.js → streaming-BlDaph8U.js} +3 -3
  187. package/lib/streaming.js +1 -1
  188. package/lib/{test-websocket-D8WWqE5G.js → test-websocket-BLeEvybj.js} +2 -2
  189. package/lib/testing-BQx5T_UX.js +17 -0
  190. package/lib/testing.js +1 -16
  191. package/lib/timeout-FeqZ9A-0.js +42 -0
  192. package/lib/timeout.js +1 -41
  193. package/lib/tiny.js +2 -2
  194. package/lib/trailing-slash-DwTx-1sk.js +85 -0
  195. package/lib/trailing-slash.js +1 -84
  196. package/lib/{trie-router-CfFbapzL.js → trie-router-JydLKUa2.js} +2 -2
  197. package/lib/url-BPh4d6j-.js +197 -0
  198. package/lib/utils/accept.js +1 -192
  199. package/lib/utils/basic-auth.js +1 -19
  200. package/lib/utils/body.js +1 -93
  201. package/lib/utils/buffer.js +1 -50
  202. package/lib/utils/color.js +1 -36
  203. package/lib/utils/compress.d.ts +1 -10
  204. package/lib/utils/compress.js +1 -10
  205. package/lib/utils/concurrent.js +1 -29
  206. package/lib/utils/constants.js +1 -6
  207. package/lib/utils/cookie.js +1 -124
  208. package/lib/utils/crypto.js +1 -33
  209. package/lib/utils/encode.js +1 -32
  210. package/lib/utils/handler.js +1 -11
  211. package/lib/utils/html.js +1 -102
  212. package/lib/utils/ipaddr.js +1 -237
  213. package/lib/utils/jwt.js +1 -1
  214. package/lib/utils/mime.js +1 -76
  215. package/lib/utils/stream.js +1 -87
  216. package/lib/utils/url.js +1 -196
  217. package/lib/{validator-D84mq-dO.js → validator-Btiyt5VJ.js} +3 -3
  218. package/lib/validator.js +1 -1
  219. package/lib/websocket-DsyBIKVA.js +56 -0
  220. package/lib/ws.js +1 -55
  221. package/package.json +1 -1
  222. package/plugins/acryl-door/src/cli/acryl.ts +36 -5
  223. package/plugins/acryl-door/src/host/channel.ts +14 -6
  224. package/plugins/acryl-door/src/host/chat-tools.ts +2 -0
  225. package/plugins/acryl-door/src/host/door.ts +97 -12
  226. package/plugins/acryl-door/src/host/page-routes.ts +3 -1
  227. package/plugins/acryl-door/src/host/shell.ts +147 -0
  228. package/plugins/acryl-door/src/host/shim.ts +39 -3
  229. package/plugins/acryl-door/src/host/verbs.ts +39 -2
  230. package/plugins/acryl-door/src/index.ts +22 -3
  231. package/plugins/acryl-door/src/page/channel.ts +3 -1
  232. package/plugins/acryl-door/src/page/index.ts +2 -1
  233. package/plugins/acryl-door/src/page/protocol.ts +2 -0
  234. package/plugins/acryl-door/src/shell/protocol.ts +130 -0
  235. package/plugins/acryl-node-server/src/register.ts +29 -2
  236. package/plugins/agent/model.ts +6 -1
  237. package/plugins/agent/tools.ts +12 -2
  238. package/plugins/agent-panel/client/describe.ts +44 -0
  239. package/plugins/agent-panel/client/main.ts +4 -1
  240. package/plugins/agent-panel/headless.ts +29 -8
  241. package/plugins/agent-panel/index.ts +22 -10
  242. package/plugins/agent-panel/launch.ts +67 -0
  243. package/plugins/desktop/attach.ts +72 -0
  244. package/plugins/desktop/door-shell.ts +379 -0
  245. package/plugins/desktop/main.ts +82 -15
  246. package/plugins/extensions/index.ts +3 -4
  247. package/plugins/extensions/stage.ts +33 -5
  248. package/plugins/openai/index.ts +1 -1
  249. package/templates/agent/AGENTS.md +4 -0
  250. package/templates/agent/blend.yaml +4 -1
  251. package/templates/agent/src/app.ts +2 -0
  252. package/templates/agent/src/routes.ts +1 -1
  253. package/templates/agent/template.json +1 -0
  254. package/templates/agent/test/builder.test.ts +26 -21
  255. package/lib/helpers-CvTauQT5.js +0 -5
  256. package/lib/middleware-CPQ8himN.js +0 -12
  257. package/plugins/acryl-router/src/connection.d.ts +0 -25
  258. package/plugins/acryl-router/src/core/app-base.d.ts +0 -215
  259. package/plugins/acryl-router/src/core/app.d.ts +0 -19
  260. package/plugins/acryl-router/src/core/client/client.d.ts +0 -4
  261. package/plugins/acryl-router/src/core/client/fetch-result-please.d.ts +0 -35
  262. package/plugins/acryl-router/src/core/client/index.d.ts +0 -7
  263. package/plugins/acryl-router/src/core/client/types.d.ts +0 -229
  264. package/plugins/acryl-router/src/core/client/utils.d.ts +0 -18
  265. package/plugins/acryl-router/src/core/compose.d.ts +0 -14
  266. package/plugins/acryl-router/src/core/context.d.ts +0 -443
  267. package/plugins/acryl-router/src/core/helper/accepts/accepts.d.ts +0 -30
  268. package/plugins/acryl-router/src/core/helper/accepts/index.d.ts +0 -5
  269. package/plugins/acryl-router/src/core/helper/conninfo/index.d.ts +0 -5
  270. package/plugins/acryl-router/src/core/helper/conninfo/types.d.ts +0 -36
  271. package/plugins/acryl-router/src/core/helper/cookie/index.d.ts +0 -25
  272. package/plugins/acryl-router/src/core/helper/css/common.d.ts +0 -59
  273. package/plugins/acryl-router/src/core/helper/css/index.d.ts +0 -80
  274. package/plugins/acryl-router/src/core/helper/dev/index.d.ts +0 -20
  275. package/plugins/acryl-router/src/core/helper/factory/index.d.ts +0 -82
  276. package/plugins/acryl-router/src/core/helper/html/index.d.ts +0 -8
  277. package/plugins/acryl-router/src/core/helper/proxy/index.d.ts +0 -73
  278. package/plugins/acryl-router/src/core/helper/route/index.d.ts +0 -75
  279. package/plugins/acryl-router/src/core/helper/ssg/index.d.ts +0 -7
  280. package/plugins/acryl-router/src/core/helper/ssg/middleware.d.ts +0 -44
  281. package/plugins/acryl-router/src/core/helper/ssg/plugins.d.ts +0 -27
  282. package/plugins/acryl-router/src/core/helper/ssg/ssg.d.ts +0 -91
  283. package/plugins/acryl-router/src/core/helper/ssg/utils.d.ts +0 -16
  284. package/plugins/acryl-router/src/core/helper/streaming/index.d.ts +0 -8
  285. package/plugins/acryl-router/src/core/helper/streaming/sse.d.ts +0 -13
  286. package/plugins/acryl-router/src/core/helper/streaming/stream.d.ts +0 -3
  287. package/plugins/acryl-router/src/core/helper/streaming/text.d.ts +0 -3
  288. package/plugins/acryl-router/src/core/helper/streaming/utils.d.ts +0 -1
  289. package/plugins/acryl-router/src/core/helper/testing/index.d.ts +0 -13
  290. package/plugins/acryl-router/src/core/helper/websocket/index.d.ts +0 -70
  291. package/plugins/acryl-router/src/core/http-exception.d.ts +0 -58
  292. package/plugins/acryl-router/src/core/index.d.ts +0 -37
  293. package/plugins/acryl-router/src/core/jsx/base.d.ts +0 -61
  294. package/plugins/acryl-router/src/core/jsx/children.d.ts +0 -9
  295. package/plugins/acryl-router/src/core/jsx/components.d.ts +0 -15
  296. package/plugins/acryl-router/src/core/jsx/constants.d.ts +0 -6
  297. package/plugins/acryl-router/src/core/jsx/context.d.ts +0 -51
  298. package/plugins/acryl-router/src/core/jsx/dom/components.d.ts +0 -10
  299. package/plugins/acryl-router/src/core/jsx/dom/context.d.ts +0 -3
  300. package/plugins/acryl-router/src/core/jsx/dom/css.d.ts +0 -83
  301. package/plugins/acryl-router/src/core/jsx/dom/hooks/index.d.ts +0 -39
  302. package/plugins/acryl-router/src/core/jsx/dom/intrinsic-element/components.d.ts +0 -16
  303. package/plugins/acryl-router/src/core/jsx/dom/jsx-dev-runtime.d.ts +0 -8
  304. package/plugins/acryl-router/src/core/jsx/dom/jsx-runtime.d.ts +0 -7
  305. package/plugins/acryl-router/src/core/jsx/dom/render.d.ts +0 -73
  306. package/plugins/acryl-router/src/core/jsx/dom/utils.d.ts +0 -1
  307. package/plugins/acryl-router/src/core/jsx/hooks/index.d.ts +0 -47
  308. package/plugins/acryl-router/src/core/jsx/index.d.ts +0 -76
  309. package/plugins/acryl-router/src/core/jsx/intrinsic-element/common.d.ts +0 -6
  310. package/plugins/acryl-router/src/core/jsx/intrinsic-element/components.d.ts +0 -13
  311. package/plugins/acryl-router/src/core/jsx/intrinsic-elements.d.ts +0 -735
  312. package/plugins/acryl-router/src/core/jsx/streaming.d.ts +0 -36
  313. package/plugins/acryl-router/src/core/jsx/types.d.ts +0 -36
  314. package/plugins/acryl-router/src/core/jsx/utils.d.ts +0 -4
  315. package/plugins/acryl-router/src/core/request/constants.d.ts +0 -1
  316. package/plugins/acryl-router/src/core/request.d.ts +0 -306
  317. package/plugins/acryl-router/src/core/router/reg-exp-router/index.d.ts +0 -6
  318. package/plugins/acryl-router/src/core/router/reg-exp-router/matcher.d.ts +0 -7
  319. package/plugins/acryl-router/src/core/router/reg-exp-router/node.d.ts +0 -13
  320. package/plugins/acryl-router/src/core/router/reg-exp-router/prepared-router.d.ts +0 -17
  321. package/plugins/acryl-router/src/core/router/reg-exp-router/router.d.ts +0 -11
  322. package/plugins/acryl-router/src/core/router/reg-exp-router/trie.d.ts +0 -8
  323. package/plugins/acryl-router/src/core/router/smart-router/index.d.ts +0 -5
  324. package/plugins/acryl-router/src/core/router/smart-router/router.d.ts +0 -11
  325. package/plugins/acryl-router/src/core/router/trie-router/index.d.ts +0 -5
  326. package/plugins/acryl-router/src/core/router/trie-router/node.d.ts +0 -6
  327. package/plugins/acryl-router/src/core/router/trie-router/router.d.ts +0 -7
  328. package/plugins/acryl-router/src/core/router/utils.d.ts +0 -1
  329. package/plugins/acryl-router/src/core/router.d.ts +0 -97
  330. package/plugins/acryl-router/src/core/types.d.ts +0 -573
  331. package/plugins/acryl-router/src/core/utils/accept.d.ts +0 -6
  332. package/plugins/acryl-router/src/core/utils/body.d.ts +0 -79
  333. package/plugins/acryl-router/src/core/utils/buffer.d.ts +0 -17
  334. package/plugins/acryl-router/src/core/utils/color.d.ts +0 -21
  335. package/plugins/acryl-router/src/core/utils/concurrent.d.ts +0 -11
  336. package/plugins/acryl-router/src/core/utils/constants.d.ts +0 -4
  337. package/plugins/acryl-router/src/core/utils/cookie.d.ts +0 -41
  338. package/plugins/acryl-router/src/core/utils/crypto.d.ts +0 -15
  339. package/plugins/acryl-router/src/core/utils/handler.d.ts +0 -6
  340. package/plugins/acryl-router/src/core/utils/headers.d.ts +0 -8
  341. package/plugins/acryl-router/src/core/utils/html.d.ts +0 -44
  342. package/plugins/acryl-router/src/core/utils/http-status.d.ts +0 -32
  343. package/plugins/acryl-router/src/core/utils/mime.d.ts +0 -70
  344. package/plugins/acryl-router/src/core/utils/stream.d.ts +0 -31
  345. package/plugins/acryl-router/src/core/utils/types.d.ts +0 -128
  346. package/plugins/acryl-router/src/core/utils/url.d.ts +0 -44
  347. package/plugins/acryl-router/src/core/validator/index.d.ts +0 -7
  348. package/plugins/acryl-router/src/core/validator/utils.d.ts +0 -29
  349. package/plugins/acryl-router/src/core/validator/validator.d.ts +0 -16
  350. package/plugins/acryl-router/src/helpers/index.d.ts +0 -17
  351. package/plugins/acryl-router/src/helpers/schema-validator.d.ts +0 -54
@@ -6,7 +6,7 @@
6
6
  import { MAX_SNAPSHOT_NODES, MAX_WAIT_MS } from '../page/protocol.ts'
7
7
 
8
8
  /** Every verb, in the order `brief` lists them. */
9
- export const VERBS = ['brief', 'look', 'act', 'plugin-list', 'plugin-add', 'plugin-remove', 'plugin-reload', 'tool-list', 'tool-call'] as const
9
+ export const VERBS = ['brief', 'look', 'act', 'plugin-list', 'plugin-add', 'plugin-remove', 'plugin-reload', 'tool-list', 'tool-call', 'window-list', 'window-open', 'window-focus', 'window-close', 'window-resize', 'menu-list', 'menu-click'] as const
10
10
  export type Verb = (typeof VERBS)[number]
11
11
 
12
12
  /** The JSON Schema of a verb's arguments, as MCP lists it. */
@@ -35,13 +35,14 @@ export const VERB_SPECS: Readonly<Record<Verb, VerbSpec>> = {
35
35
  arguments: none,
36
36
  },
37
37
  'look': {
38
- usage: 'look [--cursor <n>] [--max-nodes <n>]: the page as text, with refs',
38
+ usage: 'look [--window <n>] [--cursor <n>] [--max-nodes <n>]: the page as text, with refs',
39
39
  description: 'The open page as text: its roles, names, values and visible text, each with a ref to act on. A long page comes in parts (cursor).',
40
40
  arguments: {
41
41
  type: 'object',
42
42
  properties: {
43
43
  cursor: { type: 'integer', minimum: 0, description: 'where the previous part ended' },
44
44
  maxNodes: { type: 'integer', minimum: 1, maximum: MAX_SNAPSHOT_NODES },
45
+ window: { type: 'integer', minimum: 1, description: 'a desktop window (the id window-list prints); default: the page used last' },
45
46
  },
46
47
  additionalProperties: false,
47
48
  },
@@ -66,6 +67,7 @@ export const VERB_SPECS: Readonly<Record<Verb, VerbSpec>> = {
66
67
  name: { type: 'string', description: 'wait: its name' },
67
68
  gone: { type: 'boolean', description: 'wait: until it is gone instead' },
68
69
  timeoutMs: { type: 'integer', minimum: 0, maximum: MAX_WAIT_MS },
70
+ window: { type: 'integer', minimum: 1, description: 'a desktop window (the id window-list prints); default: the page used last' },
69
71
  },
70
72
  required: ['op'],
71
73
  additionalProperties: false,
@@ -101,6 +103,41 @@ export const VERB_SPECS: Readonly<Record<Verb, VerbSpec>> = {
101
103
  description: 'Run one of the app tools that tool-list names.',
102
104
  arguments: { type: 'object', properties: { tool: { type: 'string' }, args: { description: 'the tool\'s input' } }, required: ['tool'], additionalProperties: false },
103
105
  },
106
+ 'window-list': {
107
+ usage: 'window list: the windows of the desktop app',
108
+ description: 'The windows of the desktop app: id, title, path, size, and which one is focused. Needs the desktop app (npm run desktop).',
109
+ arguments: none,
110
+ },
111
+ 'window-open': {
112
+ usage: 'window open [--path <path of the app>]: open a new window',
113
+ description: 'Open a new window of the desktop app on a path of the app (default: its start page). Answers with the new window\'s id; look and act take it as window.',
114
+ arguments: { type: 'object', properties: { path: { type: 'string', description: 'a path of this app, e.g. /settings' } }, additionalProperties: false },
115
+ },
116
+ 'window-focus': {
117
+ usage: 'window focus --id <n>: bring a window to the front',
118
+ description: 'Bring a window of the desktop app to the front. Look and act without a window then drive it.',
119
+ arguments: { type: 'object', properties: { id: { type: 'integer', minimum: 1, description: 'the id window-list prints' } }, required: ['id'], additionalProperties: false },
120
+ },
121
+ 'window-close': {
122
+ usage: 'window close --id <n> [--force]: close a window',
123
+ description: 'Close a window of the desktop app. The last window cannot be closed without force, because that ends the desktop app.',
124
+ arguments: { type: 'object', properties: { id: { type: 'integer', minimum: 1 }, force: { type: 'boolean', description: 'allow closing the last window, which ends the desktop app' } }, required: ['id'], additionalProperties: false },
125
+ },
126
+ 'window-resize': {
127
+ usage: 'window resize --id <n> --width <px> --height <px>',
128
+ description: 'Resize a window of the desktop app.',
129
+ arguments: { type: 'object', properties: { id: { type: 'integer', minimum: 1 }, width: { type: 'integer', minimum: 200, maximum: 10_000 }, height: { type: 'integer', minimum: 200, maximum: 10_000 } }, required: ['id', 'width', 'height'], additionalProperties: false },
130
+ },
131
+ 'menu-list': {
132
+ usage: 'menu list: the application menu',
133
+ description: 'The desktop app\'s application menu as paths such as File>New Note, with the disabled items marked.',
134
+ arguments: none,
135
+ },
136
+ 'menu-click': {
137
+ usage: 'menu click --path "File>New Note" [--force]: press a menu item',
138
+ description: 'Press an item of the desktop app\'s application menu by its path (labels joined with >, as menu-list prints them). A disabled item is refused, and so is one that ends the app (Quit, Close Window on the last window) unless force is set.',
139
+ arguments: { type: 'object', properties: { path: { type: 'string', description: 'e.g. File>New Note' }, force: { type: 'boolean', description: 'allow an item that ends the desktop app' } }, required: ['path'], additionalProperties: false },
140
+ },
104
141
  }
105
142
 
106
143
  /** A verb's name in the `tools` registry the built-in chat reads: prefixed, so it never meets an app tool's name. */
@@ -23,6 +23,7 @@ import { DOOR_PREFIX, privateApi } from './host/http.ts'
23
23
  import { mcpRoutes } from './host/mcp-routes.ts'
24
24
  import { operatorRoutes } from './host/operator-routes.ts'
25
25
  import { pageChannelRoutes } from './host/page-routes.ts'
26
+ import { ShellChannel, shellRoutes } from './host/shell.ts'
26
27
  import { realNode, writeShims, type ShimTarget } from './host/shim.ts'
27
28
 
28
29
  /** Stable Cordis plugin name, equal to the package name and the Loader row id. */
@@ -94,6 +95,13 @@ function loopbackUrl(url: string): string {
94
95
  return parsed.origin
95
96
  }
96
97
 
98
+ const escapeHtml = (text: string): string => text.replace(/[&<>"']/gu, char => `&#${char.charCodeAt(0)};`)
99
+
100
+ /** The page of a path the app does not serve, development only: what is missing, a way back, and the door's script. */
101
+ export function notFoundPage(path: string, scriptTag: string): string {
102
+ return `<!doctype html><html lang="en"><head><meta charset="utf-8"><title>404 Not Found</title></head><body><main><h1>404 Not Found</h1><p>This app has no page at <code>${escapeHtml(path)}</code>.</p><p><a href="/">Back to the app</a></p></main>${scriptTag}</body></html>`
103
+ }
104
+
97
105
  export function apply(ctx: Context, rawConfig: Config = {}): void {
98
106
  const config = Config(rawConfig)
99
107
  // Development only: nothing is started, mounted or written in production, or when switched off.
@@ -106,9 +114,11 @@ export function apply(ctx: Context, rawConfig: Config = {}): void {
106
114
  }
107
115
  const secret = newSecret()
108
116
  const channel = new UiChannel()
117
+ const shell = new ShellChannel()
109
118
  const audit = new AuditLog(auditPath(stateDir(app.root)))
110
119
  const door = new Door({
111
120
  channel,
121
+ shell,
112
122
  audit,
113
123
  app: { name: app.name, root: app.root, url: loopbackUrl(server.url) },
114
124
  extensions: () => ctx.get('extensions') as unknown as ExtensionsPort | undefined,
@@ -117,6 +127,7 @@ export function apply(ctx: Context, rawConfig: Config = {}): void {
117
127
  reloadOnChange: config.reloadOnChange !== false,
118
128
  })
119
129
  ctx.effect(() => () => { channel.close() }, 'acryl-door: page channel')
130
+ ctx.effect(() => () => { shell.close() }, 'acryl-door: desktop shell channel')
120
131
  // One script tag for the page (`page.js`): it imports the module tree served below it.
121
132
  const pageModule = new Acryl()
122
133
  .get(`${DOOR_PREFIX}/page.js`, c => c.body(`import '${DOOR_PREFIX}/page/main.ts'\n`, 200, { 'content-type': 'text/javascript; charset=utf-8' }))
@@ -124,9 +135,15 @@ export function apply(ctx: Context, rawConfig: Config = {}): void {
124
135
  // The page script on every HTML page the app answers, whenever its plugin was mounted (the pages of a plugin that loaded at boot, before this one, included): a page an agent writes
125
136
  // can be looked at and driven without anyone adding the tag. A page that already carries it (the templates' own pages) is left as it is. Development only, like the rest of the door.
126
137
  const tag = `<script type="module" src="${DOOR_PREFIX}/page.js"></script>`
127
- const addScript = async (c: { res: Response }, next: () => Promise<void>): Promise<void> => {
138
+ const addScript = async (c: { res: Response; req: { path: string; header(name: string): string | undefined } }, next: () => Promise<void>): Promise<void> => {
128
139
  await next()
129
- if (!(c.res.headers.get('content-type') ?? '').includes('text/html')) return
140
+ if (!(c.res.headers.get('content-type') ?? '').includes('text/html')) {
141
+ // A path the app does not serve, opened as a page: the browser would show a bare "404 Not Found" with no script, and the window would be left where nothing can look at it or
142
+ // act on it (an agent that mistypes a path would need a person to bring the window back). The page says what is missing, carries the script, and keeps the 404.
143
+ if (c.res.status === 404 && (c.req.header('accept') ?? '').includes('text/html') && !(c.res.headers.get('content-type') ?? '').includes('json'))
144
+ c.res = new Response(notFoundPage(c.req.path, tag), { status: 404, headers: { 'content-type': 'text/html; charset=utf-8' } })
145
+ return
146
+ }
130
147
  const html = await c.res.text()
131
148
  const headers = new Headers(c.res.headers)
132
149
  headers.delete('content-length')
@@ -141,6 +158,7 @@ export function apply(ctx: Context, rawConfig: Config = {}): void {
141
158
  DOOR_PREFIX,
142
159
  pageModule,
143
160
  pageChannelRoutes(channel, { prefix: DOOR_PREFIX }),
161
+ shellRoutes(DOOR_PREFIX, secret, shell),
144
162
  operatorRoutes(DOOR_PREFIX, secret, door, reportError),
145
163
  mcpRoutes(DOOR_PREFIX, secret, door, reportError),
146
164
  ))
@@ -151,7 +169,8 @@ export function apply(ctx: Context, rawConfig: Config = {}): void {
151
169
  ctx.effect(function* () {
152
170
  yield ctx.reflect.provide('door', door)
153
171
  writeDoorFile(app.root, loopbackUrl(server.url), secret)
154
- writeShims(app.root, shimTarget())
172
+ // A shim that cannot be replaced (Windows, held open by a running `acryl`) leaves the old one in place; it must not stop the app from starting.
173
+ try { writeShims(app.root, shimTarget()) } catch (cause) { reportError('write .acryl/bin', cause) }
155
174
  yield () => { removeDoorFile(app.root, secret) }
156
175
  }, 'acryl-door: the door service, .acryl/door.json and .acryl/bin/acryl')
157
176
  }
@@ -26,6 +26,8 @@ const BACKOFF_MAX_MS = 10_000
26
26
 
27
27
  export interface PageChannelOptions {
28
28
  readonly windowId: string
29
+ /** The desktop window this page is in (`AcrylWindow/<n>` in the user agent, set by the Electron shell), so the Host can drive it by the number `window list` prints. */
30
+ readonly shellWindow?: number
29
31
  readonly focused: () => boolean
30
32
  /** Where the door's routes are, e.g. `/api/acryl-door`. */
31
33
  readonly base: string
@@ -56,7 +58,7 @@ export class PageChannel {
56
58
  connect(): void {
57
59
  if (this.disposed || this.source !== undefined) return
58
60
  const create = this.options.createSource ?? ((url: string): ChannelSource => new EventSource(url))
59
- const query = `window=${encodeURIComponent(this.options.windowId)}&focused=${this.options.focused() ? '1' : '0'}`
61
+ const query = `window=${encodeURIComponent(this.options.windowId)}&focused=${this.options.focused() ? '1' : '0'}${this.options.shellWindow === undefined ? '' : `&shell=${String(this.options.shellWindow)}`}`
60
62
  const source = create(`${this.options.base}/channel?${query}`)
61
63
  this.source = source
62
64
  this.lastHeard = this.now()
@@ -21,7 +21,8 @@ export function startDoor(document: Document, base: string = DOOR_BASE): () => v
21
21
  const focused = (): boolean => document.visibilityState === 'visible' && document.hasFocus()
22
22
  const detachInput = clock.attach(document)
23
23
  const removeIndicator = mountIndicator(driver, document)
24
- const channel = new PageChannel(driver, { windowId, focused, base })
24
+ const shellWindow = Number(/\bAcrylWindow\/(\d{1,9})\b/u.exec(view.navigator?.userAgent ?? '')?.[1])
25
+ const channel = new PageChannel(driver, { windowId, focused, base, ...(Number.isSafeInteger(shellWindow) && shellWindow > 0 ? { shellWindow } : {}) })
25
26
  const reportFocus = (): void => { channel.reportFocus(focused()) }
26
27
  view.addEventListener('focus', reportFocus)
27
28
  view.addEventListener('blur', reportFocus)
@@ -212,6 +212,8 @@ export interface AuditEntry {
212
212
  readonly tool: string
213
213
  /** The control touched, when the call had one. */
214
214
  readonly target?: { readonly role: string; readonly name: string }
215
+ /** The desktop window the call was about (the number `window list` prints), when it had one. */
216
+ readonly window?: number
215
217
  readonly outcome: 'ok' | 'refused' | 'failed'
216
218
  /** The error code for a refusal or failure, or a short note for a success. */
217
219
  readonly detail?: string
@@ -0,0 +1,130 @@
1
+ /**
2
+ * What the door and the desktop shell say to each other. The shell is the Electron main process of `npm run desktop`; it reaches the door as a page does (an event
3
+ * stream down, a POST up) and answers calls about the app's windows and menu. This file is plain data and checks, so the shell side can follow it without importing the door.
4
+ */
5
+
6
+ /** The shell's operations, one per door verb (`window-list` is `acryl window list`). */
7
+ export const SHELL_OPS = ['window-list', 'window-open', 'window-focus', 'window-close', 'window-resize', 'menu-list', 'menu-click'] as const
8
+ export type ShellOp = (typeof SHELL_OPS)[number]
9
+
10
+ export type ShellRequest =
11
+ | { readonly op: 'window-list' }
12
+ | { readonly op: 'window-open'; readonly path?: string }
13
+ | { readonly op: 'window-focus'; readonly id: number }
14
+ /** `force`: the window may be the last, and closing the last one ends the desktop app (and with it the server it started). */
15
+ | { readonly op: 'window-close'; readonly id: number; readonly force?: boolean }
16
+ | { readonly op: 'window-resize'; readonly id: number; readonly width: number; readonly height: number }
17
+ | { readonly op: 'menu-list' }
18
+ /** `force`: the item may end the desktop app (Quit, or Close Window on the last window). */
19
+ | { readonly op: 'menu-click'; readonly path: string; readonly force?: boolean }
20
+
21
+ export interface ShellWindow {
22
+ /** Electron's own window id: the number `window list` prints and `--window` takes. */
23
+ readonly id: number
24
+ readonly title: string
25
+ /** The path (with query) the window shows, never the origin. */
26
+ readonly path: string
27
+ readonly focused: boolean
28
+ readonly width: number
29
+ readonly height: number
30
+ }
31
+
32
+ export interface ShellMenuItem {
33
+ /** Labels from the top of the menu, joined with `>`: `File>New Note`. */
34
+ readonly path: string
35
+ readonly label: string
36
+ readonly enabled: boolean
37
+ /** True for an item that ends the desktop app (Quit, or Close Window): `menu click` presses it only with `force`. */
38
+ readonly ends?: boolean
39
+ }
40
+
41
+ export type ShellResult =
42
+ | { readonly windows: readonly ShellWindow[] }
43
+ | { readonly items: readonly ShellMenuItem[] }
44
+ | { readonly ok: true; readonly detail: string; readonly id?: number }
45
+
46
+ /** Door to shell. */
47
+ export interface ShellCall {
48
+ readonly t: 'call'
49
+ readonly id: number
50
+ readonly request: ShellRequest
51
+ }
52
+
53
+ /** Shell to door. */
54
+ export type ShellAnswer =
55
+ | { readonly t: 'result'; readonly id: number; readonly value: ShellResult }
56
+ | { readonly t: 'error'; readonly id: number; readonly code: string; readonly message: string }
57
+
58
+ export type ShellErrorCode = 'invalid' | 'no-desktop' | 'no-such-window' | 'last-window' | 'no-such-menu-item' | 'disabled' | 'quits-app' | 'timeout' | 'disconnected' | 'unloaded' | 'aborted' | 'failed'
59
+
60
+ export class ShellError extends Error {
61
+ readonly code: ShellErrorCode
62
+
63
+ constructor(code: ShellErrorCode, message: string) {
64
+ super(message)
65
+ this.code = code
66
+ this.name = 'ShellError'
67
+ }
68
+ }
69
+
70
+ /** A path of the app, as `act navigate` takes one: one leading `/`, and no other site. */
71
+ const APP_PATH = /^\/(?![/\\])[^\s\\]*$/u
72
+
73
+ const isRecord = (value: unknown): value is Record<string, unknown> => typeof value === 'object' && value !== null && !Array.isArray(value)
74
+
75
+ function only(value: Record<string, unknown>, allowed: readonly string[]): void {
76
+ const extra = Object.keys(value).filter(key => key !== 'op' && !allowed.includes(key))
77
+ if (extra.length > 0) throw new ShellError('invalid', `unknown field ${extra.join(', ')}`)
78
+ }
79
+
80
+ function windowId(value: Record<string, unknown>): number {
81
+ if (typeof value.id !== 'number' || !Number.isSafeInteger(value.id) || value.id < 1) throw new ShellError('invalid', 'id must be the number `window list` prints for the window')
82
+ return value.id
83
+ }
84
+
85
+ function size(value: Record<string, unknown>, key: 'width' | 'height'): number {
86
+ const number = value[key]
87
+ if (typeof number !== 'number' || !Number.isInteger(number) || number < 200 || number > 10_000) throw new ShellError('invalid', `${key} must be a whole number of pixels from 200 to 10000`)
88
+ return number
89
+ }
90
+
91
+ /** @throws ShellError `invalid`. */
92
+ export function parseShellRequest(op: ShellOp, input: unknown): ShellRequest {
93
+ const value = isRecord(input) ? input : {}
94
+ switch (op) {
95
+ case 'window-list': only(value, []); return { op }
96
+ case 'window-open': {
97
+ only(value, ['path'])
98
+ if (value.path === undefined) return { op }
99
+ if (typeof value.path !== 'string' || !APP_PATH.test(value.path)) throw new ShellError('invalid', 'path must be a path of this app, such as /settings (it starts with one /, and names no other site)')
100
+ return { op, path: value.path }
101
+ }
102
+ case 'window-focus': only(value, ['id']); return { op, id: windowId(value) }
103
+ case 'window-close': {
104
+ only(value, ['id', 'force'])
105
+ if (value.force !== undefined && typeof value.force !== 'boolean') throw new ShellError('invalid', 'force must be true or false')
106
+ return { op, id: windowId(value), ...(value.force === true ? { force: true } : {}) }
107
+ }
108
+ case 'window-resize': only(value, ['id', 'width', 'height']); return { op, id: windowId(value), width: size(value, 'width'), height: size(value, 'height') }
109
+ case 'menu-list': only(value, []); return { op }
110
+ case 'menu-click': {
111
+ only(value, ['path', 'force'])
112
+ if (typeof value.path !== 'string' || value.path.trim() === '' || value.path.length > 300) throw new ShellError('invalid', 'path is the labels from the top of the menu joined with >, e.g. "File>New Note" (`menu list` prints them)')
113
+ if (value.force !== undefined && typeof value.force !== 'boolean') throw new ShellError('invalid', 'force must be true or false')
114
+ return { op, path: value.path, ...(value.force === true ? { force: true } : {}) }
115
+ }
116
+ }
117
+ }
118
+
119
+ /** @param value - one parsed POST body from the shell; null when it is not a well-formed answer. */
120
+ export function parseShellAnswer(value: unknown): ShellAnswer | null {
121
+ if (!isRecord(value) || typeof value.id !== 'number' || !Number.isSafeInteger(value.id)) return null
122
+ if (value.t === 'result' && isRecord(value.value)) return { t: 'result', id: value.id, value: value.value as unknown as ShellResult }
123
+ if (value.t === 'error' && typeof value.code === 'string' && typeof value.message === 'string') return { t: 'error', id: value.id, code: value.code.slice(0, 40), message: value.message.slice(0, 500) }
124
+ return null
125
+ }
126
+
127
+ /** Whatever code the shell used, as one the door knows (an unknown code is `failed`). */
128
+ export function shellErrorCode(code: string): ShellErrorCode {
129
+ return (['invalid', 'no-such-window', 'last-window', 'no-such-menu-item', 'disabled', 'quits-app'] as const).find(known => known === code) ?? 'failed'
130
+ }
@@ -7,9 +7,9 @@
7
7
  * `typescript` is loaded from the app, on the first JSX file only; the framework itself has no dependencies.
8
8
  */
9
9
 
10
+ import { existsSync, readFileSync } from 'node:fs'
10
11
  import { registerHooks } from 'node:module'
11
- import { readFileSync } from 'node:fs'
12
- import { fileURLToPath } from 'node:url'
12
+ import { fileURLToPath, pathToFileURL } from 'node:url'
13
13
  import { compileModule } from './transpile.ts'
14
14
 
15
15
  /** The JSX runtime the docs set up; an app's tsconfig.json (or a file's own `@jsxImportSource` pragma) chooses another. */
@@ -27,3 +27,30 @@ registerHooks({
27
27
  return { format: 'module', source, shortCircuit: true }
28
28
  },
29
29
  })
30
+
31
+ /**
32
+ * The framework's names for a file of the app: in an app the framework plugins are source under `plugins/`, not packages, so `acrylblends`, `acrylblends/body-limit` or `acryl-router`
33
+ * have nowhere to resolve from. A plugin installed while the app runs gets them from the extensions plugin; a plugin that is a ROW of `blend.yaml` is imported by `src/app.ts` before
34
+ * any of that exists, so the same names are resolved here, in the preload every `dev`, `start` and `test` script runs. Only a name that Node cannot resolve itself, only for the
35
+ * app's own files (never for something in `node_modules`), and only in an app that has the framework as vendored source: the published package resolves its own names.
36
+ */
37
+ const plugins = new URL('../../', import.meta.url)
38
+ const appRoot = new URL('../', plugins).href
39
+ const resolver = new URL('extensions/resolve.ts', plugins)
40
+ if (existsSync(fileURLToPath(new URL('acryl-router/src/index.ts', plugins))) && existsSync(fileURLToPath(resolver))) {
41
+ const { routerAliases } = (await import(resolver.href)) as { routerAliases(): Record<string, string> }
42
+ const aliases = routerAliases()
43
+ registerHooks({
44
+ resolve(specifier, context, nextResolve) {
45
+ try {
46
+ return nextResolve(specifier, context)
47
+ } catch (error) {
48
+ const alias = aliases[specifier]
49
+ const parent = context.parentURL
50
+ if (alias !== undefined && parent?.startsWith(appRoot) && !parent.includes('/node_modules/'))
51
+ return nextResolve(pathToFileURL(alias).href, context)
52
+ throw error
53
+ }
54
+ },
55
+ })
56
+ }
@@ -21,7 +21,12 @@ export interface ModelInput {
21
21
  readonly reasoning?: string
22
22
  }>
23
23
  /** The tools the model may call. */
24
- readonly tools?: ReadonlyArray<{ readonly name: string; readonly description: string }>
24
+ readonly tools?: ReadonlyArray<{
25
+ readonly name: string
26
+ readonly description: string
27
+ /** The JSON Schema of the tool's input, when it has one. */
28
+ readonly parameters?: Readonly<Record<string, unknown>>
29
+ }>
25
30
  /** Aborts when the run is cancelled: a provider that can stop its request passes this on. */
26
31
  readonly signal?: AbortSignal
27
32
  }
@@ -3,6 +3,8 @@ import type { Context } from '@deepseek-ai/cordis'
3
3
  /** One tool: what it does (for the model) and how to run it. */
4
4
  export interface Tool {
5
5
  readonly description: string
6
+ /** The JSON Schema of the input (an object schema), sent to a real model so it knows which fields the tool takes. A tool without one takes any object. */
7
+ readonly parameters?: Readonly<Record<string, unknown>>
6
8
  /** `options.signal` aborts when the run is cancelled; a tool that can stop its work listens to it. */
7
9
  run(input: unknown, options?: { signal?: AbortSignal }): unknown | Promise<unknown>
8
10
  }
@@ -11,7 +13,11 @@ export interface Tool {
11
13
  export interface Tools {
12
14
  /** Register a tool. Returns the function that removes it. */
13
15
  register(name: string, tool: Tool): () => void
14
- list(): Array<{ name: string; description: string }>
16
+ list(): Array<{
17
+ name: string
18
+ description: string
19
+ parameters?: Readonly<Record<string, unknown>>
20
+ }>
15
21
  call(name: string, input: unknown, options?: { signal?: AbortSignal }): Promise<unknown>
16
22
  }
17
23
 
@@ -31,7 +37,11 @@ export function tools(ctx: Context) {
31
37
  return () => void table.delete(name)
32
38
  },
33
39
  list: () =>
34
- [...table.entries()].map(([name, tool]) => ({ name, description: tool.description })),
40
+ [...table.entries()].map(([name, tool]) => ({
41
+ name,
42
+ description: tool.description,
43
+ ...(tool.parameters === undefined ? {} : { parameters: tool.parameters }),
44
+ })),
35
45
  async call(name, input, options) {
36
46
  const tool = table.get(name)
37
47
  if (!tool) throw new Error(`unknown tool "${name}"`)
@@ -0,0 +1,44 @@
1
+ // One line for a tool event of the panel's conversation: what the tool is doing, not only its name. A coding agent started in the folder reports its own log (`{ tool: 'codex', info }`):
2
+ // its bookkeeping is dropped and its command or sentence is kept. The app's own agent reports a call (`{ tool, input, result | error }`): the name and a short form of the arguments.
3
+
4
+ const MAX = 160
5
+
6
+ const clip = (text: string): string => (text.length > MAX ? `${text.slice(0, MAX - 1)}…` : text)
7
+
8
+ /** A shell command as the agent's log writes it: `/bin/zsh -lc 'npm test' in /path/to/app` becomes `npm test`. */
9
+ function commandOf(line: string): string {
10
+ const withoutCwd = line.replace(/ in \/\S+\s*$/u, '')
11
+ const wrapped = /^(?:\/\S*\/)?(?:ba|z|da)?sh -l?c (['"])([\s\S]*)\1$/u.exec(withoutCwd.trim())
12
+ return wrapped?.[2] ?? withoutCwd.trim()
13
+ }
14
+
15
+ /** @returns the line to show, or `undefined` when the event says nothing worth a line (a hook ran, a token count). */
16
+ export function describeTool(data: Record<string, unknown>): string | undefined {
17
+ const tool = String(data.tool ?? '')
18
+ if (typeof data.info === 'string') {
19
+ const lines = data.info
20
+ .split('\n')
21
+ .map((line) => line.trim())
22
+ .filter(
23
+ (line) =>
24
+ line !== '' &&
25
+ !/^hook:/u.test(line) &&
26
+ !/^tokens used/u.test(line) &&
27
+ !/^[\d,]+$/u.test(line),
28
+ )
29
+ if (lines.length === 0) return undefined
30
+ const [first = '', second = ''] = lines
31
+ if (first === 'exec') return second === '' ? undefined : `${tool}: $ ${clip(commandOf(second))}`
32
+ // The output of a command that ran: only a failure is worth a line.
33
+ if (/^succeeded in /u.test(first)) return undefined
34
+ if (/^exited \d+/u.test(first)) return `${tool}: ${clip(first.replace(/:$/u, ''))}`
35
+ return `${tool}: ${clip(lines.join(' '))}`
36
+ }
37
+ const input =
38
+ data.input === undefined ||
39
+ (typeof data.input === 'object' && data.input !== null && Object.keys(data.input).length === 0)
40
+ ? ''
41
+ : ` ${clip(JSON.stringify(data.input))}`
42
+ const failed = data.error ? ` failed: ${clip(String(data.error))}` : ''
43
+ return `${tool}${input}${failed}`
44
+ }
@@ -1,5 +1,7 @@
1
1
  // The panel in the page: a button at the corner opens a side panel (a shadow root, so the app's CSS cannot touch it and it cannot touch the app's). It asks the app's
2
2
  // agent through POST /api/agent-panel/ask, shows the answer as it arrives, and reloads the page after a run that installed or removed a plugin.
3
+ import { describeTool } from './describe.ts'
4
+
3
5
  const PREFIX = '/api/agent-panel'
4
6
 
5
7
  interface Session {
@@ -181,7 +183,8 @@ async function start(): Promise<void> {
181
183
  answer ??= line('')
182
184
  answer.textContent += String(data.text ?? '')
183
185
  } else if (event === 'tool') {
184
- line(`${String(data.tool)}${data.error ? ` failed: ${String(data.error)}` : ''}`, 'tool')
186
+ const described = describeTool(data)
187
+ if (described !== undefined) line(described, 'tool')
185
188
  } else if (event === 'change') {
186
189
  changed = true
187
190
  line(`plugin ${String(data.extension)}: ${String(data.action)}`, 'tool')
@@ -1,10 +1,11 @@
1
1
  /** The panel's own drivers, all `chat`: the app's agent, and the coding agents (`claude`, `codex`, `pi`) started headless in the app folder. */
2
- import { spawn, spawnSync } from 'node:child_process'
2
+ import { type ChildProcess, spawn, spawnSync } from 'node:child_process'
3
3
  import { readFileSync } from 'node:fs'
4
4
  import { homedir } from 'node:os'
5
5
  import { join } from 'node:path'
6
6
  import type { Context } from '@deepseek-ai/cordis'
7
7
  import type { ChatDriver, ChatEvent } from './drivers.ts'
8
+ import { resolveLaunch } from './launch.ts'
8
9
 
9
10
  export type HeadlessName = 'claude' | 'codex' | 'pi'
10
11
 
@@ -58,6 +59,15 @@ export function command(
58
59
  }
59
60
  }
60
61
 
62
+ /** Stop a program and what it started: Windows has no process groups, so the tree goes by its root (`SIGTERM` there ends only the one process). */
63
+ function stopProgram(child: ChildProcess): void {
64
+ if (process.platform === 'win32' && child.pid !== undefined) {
65
+ spawnSync('taskkill', ['/PID', String(child.pid), '/T', '/F'], { stdio: 'ignore' })
66
+ return
67
+ }
68
+ child.kill('SIGTERM')
69
+ }
70
+
61
71
  export function onPath(program: string): boolean {
62
72
  const found = spawnSync(process.platform === 'win32' ? 'where' : 'which', [program], {
63
73
  encoding: 'utf8',
@@ -89,7 +99,10 @@ export function availableHeadless(wanted?: readonly HeadlessName[]): HeadlessNam
89
99
  const found: HeadlessName[] = []
90
100
  for (const driver of wanted ?? (['claude', 'codex', 'pi'] as const)) {
91
101
  if (driver === 'pi' && !piHasDeepseekKey()) continue
92
- if (onPath(driver)) found.push(driver)
102
+ if (!onPath(driver)) continue
103
+ // On Windows an npm-installed program is offered only when its script can be started directly: a driver that is listed must start.
104
+ if ('error' in resolveLaunch(driver, [])) continue
105
+ found.push(driver)
93
106
  }
94
107
  return found
95
108
  }
@@ -131,8 +144,13 @@ export function headlessDriver(name: HeadlessName, root: string): ChatDriver {
131
144
  kind: 'chat',
132
145
  name,
133
146
  async *start({ message, signal }) {
134
- const { file, args } = command(name, message, root)
135
- const child = spawn(file, args, {
147
+ const wanted = command(name, message, root)
148
+ const launch = resolveLaunch(wanted.file, wanted.args)
149
+ if ('error' in launch) {
150
+ yield { event: 'error', data: { message: launch.error } }
151
+ return
152
+ }
153
+ const child = spawn(launch.file, launch.args, {
136
154
  cwd: root,
137
155
  env: process.env,
138
156
  stdio: ['ignore', 'pipe', 'pipe'],
@@ -144,16 +162,19 @@ export function headlessDriver(name: HeadlessName, root: string): ChatDriver {
144
162
  queue.push(event)
145
163
  wake?.()
146
164
  }
147
- signal.addEventListener('abort', () => child.kill('SIGTERM'), { once: true })
165
+ signal.addEventListener('abort', () => stopProgram(child), { once: true })
148
166
  child.stdout.on('data', (chunk: Buffer) =>
149
167
  push({ event: 'text', data: { text: chunk.toString() } }),
150
168
  )
151
169
  child.stderr.on('data', (chunk: Buffer) =>
152
170
  push({ event: 'tool', data: { tool: name, info: chunk.toString().slice(0, 500) } }),
153
171
  )
154
- child.on('error', () => {
172
+ child.on('error', (cause) => {
155
173
  ended = true
156
- push({ event: 'error', data: { message: `${name} could not be started` } })
174
+ push({
175
+ event: 'error',
176
+ data: { message: `${name} could not be started: ${cause.message}` },
177
+ })
157
178
  })
158
179
  child.on('exit', (code) => {
159
180
  ended = true
@@ -169,7 +190,7 @@ export function headlessDriver(name: HeadlessName, root: string): ChatDriver {
169
190
  else await new Promise<void>((resolve) => (wake = resolve))
170
191
  }
171
192
  } finally {
172
- if (child.exitCode === null) child.kill('SIGTERM')
193
+ if (child.exitCode === null) stopProgram(child)
173
194
  }
174
195
  },
175
196
  }
@@ -73,18 +73,30 @@ export function apply(ctx: Context, config: PanelConfig = {}): void {
73
73
  ctx.effect(() => registry.register(headlessDriver(program, root)))
74
74
 
75
75
  const tag = `<script type="module" src="${PREFIX}/panel.js"></script>`
76
+ const addScript = async (c: { res: Response }, next: () => Promise<void>): Promise<void> => {
77
+ await next()
78
+ if (!(c.res.headers.get('content-type') ?? '').includes('text/html')) return
79
+ const html = await c.res.text()
80
+ if (html.includes(`${PREFIX}/panel.js`)) {
81
+ c.res = new Response(html, c.res)
82
+ return
83
+ }
84
+ const headers = new Headers(c.res.headers)
85
+ headers.delete('content-length')
86
+ c.res = new Response(
87
+ html.includes('</body>') ? html.replace('</body>', `${tag}</body>`) : html + tag,
88
+ { status: c.res.status, headers },
89
+ )
90
+ }
91
+ // The script tag on every HTML page the app answers, whenever the plugin that serves the page was mounted (a plugin an agent built loads at boot, before this one, after a restart).
92
+ // An app whose router copy is older has no `wrap`: the tag is then added to the pages of the routes mounted after this plugin only.
93
+ const router = ctx.router as unknown as { wrap?: (middleware: typeof addScript) => unknown }
94
+ const wrapped = typeof router.wrap === 'function'
95
+ if (wrapped) router.wrap?.(addScript)
76
96
  const app = new Acryl()
77
- // The script tag, on every HTML page the routes mounted after this plugin answer.
78
97
  .use('*', async (c, next) => {
79
- await next()
80
- if (!(c.res.headers.get('content-type') ?? '').includes('text/html')) return
81
- const html = await c.res.text()
82
- const headers = new Headers(c.res.headers)
83
- headers.delete('content-length')
84
- c.res = new Response(
85
- html.includes('</body>') ? html.replace('</body>', `${tag}</body>`) : html + tag,
86
- { status: c.res.status, headers },
87
- )
98
+ if (wrapped) return next()
99
+ await addScript(c, next)
88
100
  })
89
101
  .get(`${PREFIX}/panel.js`, (c) =>
90
102
  c.body(`import '${PREFIX}/client/main.ts'\n`, 200, {