acrylblends 1.0.3 → 1.0.5

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 (256) hide show
  1. package/docs/framework/agents/index.md +6 -4
  2. package/docs/framework/workshop/README.md +3 -3
  3. package/docs/framework/workshop/facilitator.md +1 -0
  4. package/lib/accept-CNNHu4Cf.js +193 -0
  5. package/lib/{accepts-XBuZPVRI.js → accepts-Btq83xPd.js} +1 -1
  6. package/lib/accepts.js +1 -1
  7. package/lib/acryl-base.js +1 -331
  8. package/lib/adapter-CaZ6FZUU.js +37 -0
  9. package/lib/adapter.js +1 -36
  10. package/lib/{app-BM0rTCp7.js → app-CQcGWjrk.js} +4 -4
  11. package/lib/app-base-CpRMZL4v.js +332 -0
  12. package/lib/{audit-BXnYsLge.d.ts → audit-D5z9uwOP.d.ts} +14 -1
  13. package/lib/{base-NSN_SNv-.js → base-D_6kfOr-.js} +1 -1
  14. package/lib/basic-auth-C0HTRpXh.js +20 -0
  15. package/lib/basic-auth-DFLflXNe.js +95 -0
  16. package/lib/basic-auth.js +1 -94
  17. package/lib/bearer-auth-Cwxt-vgS.js +94 -0
  18. package/lib/bearer-auth.js +1 -93
  19. package/lib/body-D7f4e7w_.js +94 -0
  20. package/lib/body-limit-WmIjWRLt.js +66 -0
  21. package/lib/body-limit.js +1 -65
  22. package/lib/buffer-CkEOJCcY.js +51 -0
  23. package/lib/bun.js +3 -3
  24. package/lib/cache-B0bHH7s5.js +169 -0
  25. package/lib/cache.js +1 -168
  26. package/lib/{client-DDLXwhBo.js → client-BrMFnMEd.js} +1 -1
  27. package/lib/{client-modules-CLvWOjbd.js → client-modules-drj4mjhj.js} +4 -4
  28. package/lib/client.js +1 -1
  29. package/lib/color-COa3DplO.js +37 -0
  30. package/lib/combine-TWuGdcbC.js +137 -0
  31. package/lib/combine.js +1 -136
  32. package/lib/{components-cEW3ovK0.js → components-B2ka9Ax1.js} +4 -4
  33. package/lib/{components-JXVm45kK.js → components-PVXwaOaC.js} +2 -2
  34. package/lib/compress-BzWqsHF1.js +79 -0
  35. package/lib/compress-C-9vK4B2.d.ts +11 -0
  36. package/lib/compress-DCyrrQ5D.js +11 -0
  37. package/lib/compress.d.ts +1 -1
  38. package/lib/compress.js +2 -78
  39. package/lib/concurrent-DrF3d2c-.js +30 -0
  40. package/lib/conninfo-BCROmdnf.d.ts +11 -0
  41. package/lib/conninfo-CWJwieh4.js +19 -0
  42. package/lib/constants-CBSEudrb.js +7 -0
  43. package/lib/{context-DDO0cfuP.js → context-CkOh7Ygv.js} +2 -2
  44. package/lib/context-storage-iuPoI_CR.js +52 -0
  45. package/lib/context-storage.js +1 -51
  46. package/lib/cookie-B1LlkzOe.js +125 -0
  47. package/lib/cookie-BHIduy2T.js +82 -0
  48. package/lib/cookie.js +1 -81
  49. package/lib/cordis/db-sqlite.d.ts +1 -1
  50. package/lib/cordis/door.d.ts +17 -3
  51. package/lib/cordis/door.js +123 -16
  52. package/lib/cordis/inspect.js +4 -4
  53. package/lib/cordis/kv-db.d.ts +1 -1
  54. package/lib/cordis/kv-db.js +1 -1
  55. package/lib/cordis/kv-json.d.ts +1 -1
  56. package/lib/cordis/kv-json.js +1 -1
  57. package/lib/cordis/node-server.d.ts +1 -1
  58. package/lib/cordis/node-server.js +3 -116
  59. package/lib/cordis/router-http.d.ts +1 -1
  60. package/lib/cordis/router-http.js +2 -2
  61. package/lib/cordis/router.d.ts +1 -1
  62. package/lib/cordis/router.js +4 -4
  63. package/lib/cordis/state.js +7 -7
  64. package/lib/{core-CaY8XDZ6.js → core-BOu1jYua.js} +1 -1
  65. package/lib/cors-CbAQ-8qU.js +106 -0
  66. package/lib/cors.js +1 -105
  67. package/lib/crypto-iVP-N-tS.js +34 -0
  68. package/lib/csrf-DqqOZtgy.js +101 -0
  69. package/lib/csrf.js +1 -100
  70. package/lib/css-C5CPVMvM.js +134 -0
  71. package/lib/css.js +2 -133
  72. package/lib/db/sqlite.d.ts +1 -25
  73. package/lib/dev-CLgu-JRe.js +50 -0
  74. package/lib/dev.js +1 -49
  75. package/lib/dom-YjND9Jt3.js +74 -0
  76. package/lib/door/host.d.ts +2 -2
  77. package/lib/door/host.js +1 -1
  78. package/lib/door/page.d.ts +12 -1
  79. package/lib/door/page.js +22 -1
  80. package/lib/door/protocol.d.ts +2 -2
  81. package/lib/door/protocol.js +2 -197
  82. package/lib/encode-BhbNLrjp.js +33 -0
  83. package/lib/factory-CLdSf-xS.js +30 -0
  84. package/lib/factory.js +1 -29
  85. package/lib/handler-C0wZ0KwO.js +12 -0
  86. package/lib/helpers-BEjcdbgw.js +5 -0
  87. package/lib/{hooks-w72iLeFR.js → hooks-CfC07ri0.js} +1 -1
  88. package/lib/html-CgNa89xw.js +31 -0
  89. package/lib/html-DGExBFNg.js +103 -0
  90. package/lib/html.d.ts +2 -5
  91. package/lib/html.js +2 -30
  92. package/lib/http-exception-MjWkT89V.js +54 -0
  93. package/lib/http-exception.js +1 -53
  94. package/lib/{index-D4nVR3wu.d.ts → index-DMOtA-NJ.d.ts} +7 -1
  95. package/lib/index-dAzQsJG9.d.ts +6 -0
  96. package/lib/index.js +3 -3
  97. package/lib/ip-restriction-jKtKCRM_.js +166 -0
  98. package/lib/ip-restriction.js +1 -165
  99. package/lib/ipaddr-DCoIo528.js +238 -0
  100. package/lib/jsx/dom/client.js +1 -1
  101. package/lib/jsx/dom/jsx-dev-runtime.js +1 -1
  102. package/lib/jsx/dom/jsx-runtime.js +2 -1
  103. package/lib/jsx/dom/server.js +3 -3
  104. package/lib/jsx/dom.js +5 -73
  105. package/lib/jsx/jsx-dev-runtime.d.ts +2 -5
  106. package/lib/jsx/jsx-dev-runtime.js +2 -13
  107. package/lib/jsx/jsx-runtime.d.ts +2 -2
  108. package/lib/jsx/jsx-runtime.js +4 -4
  109. package/lib/jsx/streaming.js +1 -1
  110. package/lib/jsx-CUQgttxU.js +47 -0
  111. package/lib/jsx-dev-runtime-B4WTyq8r.js +14 -0
  112. package/lib/{jsx-dev-runtime-CWLRA2F3.js → jsx-dev-runtime-BLfA-EhI.js} +2 -2
  113. package/lib/jsx-dev-runtime-CGt4kNYr.d.ts +6 -0
  114. package/lib/jsx-renderer-CfifM4hT.js +99 -0
  115. package/lib/jsx-renderer.js +1 -98
  116. package/lib/jsx-runtime-COCRWBJ9.js +2 -0
  117. package/lib/jsx.js +5 -46
  118. package/lib/{jwk-LkzeoaU8.js → jwk-CDPpYQpD.js} +3 -3
  119. package/lib/jwk.js +1 -1
  120. package/lib/{jwt-BGJBw5AF.js → jwt-BXoIA2fQ.js} +2 -2
  121. package/lib/{jwt-CskdhEW9.js → jwt-lyS4Q3mT.js} +3 -3
  122. package/lib/jwt.js +2 -2
  123. package/lib/kv/db.d.ts +1 -10
  124. package/lib/kv/db.js +1 -56
  125. package/lib/kv/json.d.ts +1 -9
  126. package/lib/kv/json.js +1 -223
  127. package/lib/{language-D9a4zqvn.js → language-DeGRtLJt.js} +2 -2
  128. package/lib/language.js +1 -1
  129. package/lib/{linear-router-C3FPib6h.js → linear-router-CmZn_JLV.js} +2 -2
  130. package/lib/logger-Ckxu8diz.js +54 -0
  131. package/lib/logger.js +1 -53
  132. package/lib/loopback-only-BbasoF7R.js +90 -0
  133. package/lib/loopback-only.js +1 -89
  134. package/lib/method-not-allowed-TO2_hnfl.js +82 -0
  135. package/lib/method-not-allowed.js +1 -81
  136. package/lib/method-override-DnZKfJbV.js +103 -0
  137. package/lib/method-override.js +1 -102
  138. package/lib/middleware-BfG_fp8P.js +12 -0
  139. package/lib/mime-BnScXkQn.js +77 -0
  140. package/lib/mount-QkK_3NtJ.js +67 -0
  141. package/lib/mount.js +1 -66
  142. package/lib/node-server/conninfo.d.ts +1 -10
  143. package/lib/node-server/conninfo.js +1 -18
  144. package/lib/node-server/register.js +25 -2
  145. package/lib/node-server/serve-static.d.ts +1 -17
  146. package/lib/node-server/serve-static.js +1 -1
  147. package/lib/node-server.d.ts +2 -2
  148. package/lib/node-server.js +4 -4
  149. package/lib/open-BqK4D2Wn.d.ts +11 -0
  150. package/lib/open-But9E2rL.js +57 -0
  151. package/lib/open-CULVJgOc.d.ts +12 -0
  152. package/lib/open-Dp4sxspc.js +224 -0
  153. package/lib/open-T7kgbO2O.d.ts +27 -0
  154. package/lib/{pattern-router-HZxjDGEQ.js → pattern-router-CJBecJBd.js} +1 -1
  155. package/lib/powered-by-CXQNjNIX.js +24 -0
  156. package/lib/powered-by.js +1 -23
  157. package/lib/pretty-json-CeC1yvYt.js +39 -0
  158. package/lib/pretty-json.js +1 -38
  159. package/lib/{protocol-BizVt4J6.d.ts → protocol-C37gPePD.d.ts} +9 -2
  160. package/lib/protocol-Dt0AZBOz.js +213 -0
  161. package/lib/proxy-C2w_mH7k.js +121 -0
  162. package/lib/proxy.js +1 -120
  163. package/lib/quick.js +4 -4
  164. package/lib/{reg-exp-router-DC1R3qoY.js → reg-exp-router-C_WV84eV.js} +2 -2
  165. package/lib/request-DvMgGtlW.js +323 -0
  166. package/lib/request.js +1 -322
  167. package/lib/route-D8m-a2I1.js +126 -0
  168. package/lib/route.js +1 -125
  169. package/lib/router/linear-router.js +1 -1
  170. package/lib/router/pattern-router.js +1 -1
  171. package/lib/router/reg-exp-router.js +1 -1
  172. package/lib/router/smart-router.js +1 -1
  173. package/lib/router/trie-router.js +1 -1
  174. package/lib/router-Be_o6nFu.js +35 -0
  175. package/lib/router.js +1 -34
  176. package/lib/schema-validator-EK3cUUCt.js +22 -0
  177. package/lib/schema-validator.js +1 -21
  178. package/lib/{secure-headers-mP3j6Hm6.js → secure-headers-C5ZOcXqk.js} +1 -1
  179. package/lib/secure-headers.js +1 -1
  180. package/lib/serve-static-B7sqnftW.d.ts +18 -0
  181. package/lib/{serve-static-3uo_9rjF.js → serve-static-C1Nkj_xB.js} +2 -2
  182. package/lib/{serve-static-Cp5adzGo.js → serve-static-EiJf1zkO.js} +4 -4
  183. package/lib/serve-static.js +1 -1
  184. package/lib/{smart-router-C5CZ5XSM.js → smart-router-BjxkDQCE.js} +1 -1
  185. package/lib/{snapshot-text-DmE0YfeZ.js → snapshot-text-Bbx43mN0.js} +57 -9
  186. package/lib/{src-2Z4mJg0x.js → src-BTBm4OPD.js} +22 -6
  187. package/lib/src-LX0E4ec4.js +117 -0
  188. package/lib/{ssg-BwjcA-Ge.js → ssg-BEl7MHqg.js} +5 -5
  189. package/lib/ssg.js +1 -1
  190. package/lib/stream-CzM_SHkq.js +88 -0
  191. package/lib/{streaming-DHST3Scg.js → streaming-BlDaph8U.js} +3 -3
  192. package/lib/streaming.js +1 -1
  193. package/lib/{test-websocket-D8WWqE5G.js → test-websocket-BLeEvybj.js} +2 -2
  194. package/lib/testing-BQx5T_UX.js +17 -0
  195. package/lib/testing.js +1 -16
  196. package/lib/timeout-FeqZ9A-0.js +42 -0
  197. package/lib/timeout.js +1 -41
  198. package/lib/tiny.js +2 -2
  199. package/lib/trailing-slash-DwTx-1sk.js +85 -0
  200. package/lib/trailing-slash.js +1 -84
  201. package/lib/{trie-router-CfFbapzL.js → trie-router-JydLKUa2.js} +2 -2
  202. package/lib/url-BPh4d6j-.js +197 -0
  203. package/lib/utils/accept.js +1 -192
  204. package/lib/utils/basic-auth.js +1 -19
  205. package/lib/utils/body.js +1 -93
  206. package/lib/utils/buffer.js +1 -50
  207. package/lib/utils/color.js +1 -36
  208. package/lib/utils/compress.d.ts +1 -10
  209. package/lib/utils/compress.js +1 -10
  210. package/lib/utils/concurrent.js +1 -29
  211. package/lib/utils/constants.js +1 -6
  212. package/lib/utils/cookie.js +1 -124
  213. package/lib/utils/crypto.js +1 -33
  214. package/lib/utils/encode.js +1 -32
  215. package/lib/utils/handler.js +1 -11
  216. package/lib/utils/html.js +1 -102
  217. package/lib/utils/ipaddr.js +1 -237
  218. package/lib/utils/jwt.js +1 -1
  219. package/lib/utils/mime.js +1 -76
  220. package/lib/utils/stream.js +1 -87
  221. package/lib/utils/url.js +1 -196
  222. package/lib/{validator-D84mq-dO.js → validator-Btiyt5VJ.js} +3 -3
  223. package/lib/validator.js +1 -1
  224. package/lib/websocket-DsyBIKVA.js +56 -0
  225. package/lib/ws.js +1 -55
  226. package/package.json +1 -1
  227. package/plugins/acryl-door/src/cli/acryl.ts +5 -3
  228. package/plugins/acryl-door/src/host/channel.ts +36 -0
  229. package/plugins/acryl-door/src/host/chat-tools.ts +2 -0
  230. package/plugins/acryl-door/src/host/door.ts +44 -5
  231. package/plugins/acryl-door/src/host/shim.ts +23 -0
  232. package/plugins/acryl-door/src/host/verbs.ts +4 -3
  233. package/plugins/acryl-door/src/index.ts +39 -3
  234. package/plugins/acryl-door/src/page/driver/driver.ts +22 -1
  235. package/plugins/acryl-door/src/page/protocol.ts +13 -2
  236. package/plugins/acryl-node-server/src/register.ts +29 -2
  237. package/plugins/acryl-router/src/service.ts +19 -1
  238. package/plugins/agent/model.ts +6 -1
  239. package/plugins/agent/tools.ts +12 -2
  240. package/plugins/agent-panel/client/describe.ts +44 -0
  241. package/plugins/agent-panel/client/main.ts +67 -35
  242. package/plugins/agent-panel/index.ts +137 -35
  243. package/plugins/desktop/attach.ts +72 -0
  244. package/plugins/desktop/main.ts +17 -4
  245. package/plugins/extensions/index.ts +3 -4
  246. package/plugins/extensions/stage.ts +33 -5
  247. package/plugins/openai/index.ts +1 -1
  248. package/shared/DEFAULTS.md +4 -0
  249. package/templates/agent/AGENTS.md +2 -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
@@ -18,6 +18,7 @@ import { streamSSE } from '../acryl-router/src/helpers/index.ts'
18
18
  import { Acryl } from '../acryl-router/src/index.ts'
19
19
  import type {} from '../agent/index.ts'
20
20
  import type {} from '../extensions/index.ts'
21
+ import type { ChatDriver } from './drivers.ts'
21
22
  import { agentDriver, availableHeadless, type HeadlessName, headlessDriver } from './headless.ts'
22
23
  import { provideDrivers } from './registry.ts'
23
24
 
@@ -70,21 +71,32 @@ export function apply(ctx: Context, config: PanelConfig = {}): void {
70
71
  ctx.effect(() => registry.register(agentDriver(ctx)))
71
72
  for (const program of availableHeadless(config.drivers))
72
73
  ctx.effect(() => registry.register(headlessDriver(program, root)))
73
- let running = false
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, {
@@ -121,32 +133,122 @@ export function apply(ctx: Context, config: PanelConfig = {}): void {
121
133
  },
122
134
  400,
123
135
  )
124
- if (running) return c.json({ error: 'a run is already going; wait for it to end' }, 409)
125
- running = true
126
- return streamSSE(c, async (stream) => {
127
- const send = (event: string, data: unknown): Promise<void> =>
128
- stream.writeSSE({ event, data: JSON.stringify(data) })
129
- let changed = false
130
- const off = ctx.on('extensions/changed', (extension: string, action: string) => {
131
- changed = true
132
- void send('change', { extension, action })
136
+ if (current !== undefined && !current.ended)
137
+ return c.json({ error: 'a run is already going; wait for it to end' }, 409)
138
+ const run = startRun(driverName, message, driver)
139
+ return follow(c, run, false)
140
+ })
141
+ // Stop: the only thing that cancels a run. A page that goes away (a reload, a move to another page, a closed tab) does not: the agent goes on, and the page picks it up again.
142
+ .post(`${PREFIX}/stop`, (c) => {
143
+ if (!fromOwnPage(c.req.raw.headers) || c.req.header('x-agent-panel-token') !== token)
144
+ return c.json({ error: "only the app's own page may stop a run" }, 403)
145
+ current?.cancel.abort()
146
+ return c.json({ ok: true })
147
+ })
148
+ // The latest run, for a page that just loaded: what was asked, then every event so far, then the rest as it happens (until the run ends).
149
+ .get(`${PREFIX}/run`, (c) => {
150
+ if (!fromOwnPage(c.req.raw.headers) || c.req.header('x-agent-panel-token') !== token)
151
+ return c.json({ error: "only the app's own page may read the run" }, 403)
152
+ return current === undefined ? c.json({ run: null }) : follow(c, current, true)
153
+ })
154
+ /** One run of an agent: its events as they came, and who is following. It lives in the app, not in a page. */
155
+ interface Run {
156
+ readonly id: number
157
+ readonly driver: string
158
+ readonly message: string
159
+ readonly events: Array<{ event: string; data: unknown }>
160
+ readonly followers: Set<(item: { event: string; data: unknown }) => void>
161
+ readonly cancel: AbortController
162
+ ended: boolean
163
+ endedAt?: number
164
+ }
165
+ let current: Run | undefined
166
+ let runs = 0
167
+ const startRun = (driverName: string, message: string, driver: ChatDriver): Run => {
168
+ runs += 1
169
+ const run: Run = {
170
+ id: runs,
171
+ driver: driverName,
172
+ message,
173
+ events: [],
174
+ followers: new Set(),
175
+ cancel: new AbortController(),
176
+ ended: false,
177
+ }
178
+ current = run
179
+ const emit = (event: string, data: unknown): void => {
180
+ const item = { event, data }
181
+ run.events.push(item)
182
+ for (const follower of [...run.followers]) follower(item)
183
+ }
184
+ let changed = false
185
+ const off = ctx.on('extensions/changed', (extension: string, action: string) => {
186
+ changed = true
187
+ emit('change', { extension, action })
188
+ })
189
+ ctx.effect(() => () => run.cancel.abort(), `agent-panel: run ${runs}`)
190
+ audit({ ask: 'start', driver: driverName, characters: message.length })
191
+ void (async () => {
192
+ try {
193
+ for await (const { event, data } of driver.start({ message, signal: run.cancel.signal }))
194
+ emit(event, data)
195
+ emit('done', { changed })
196
+ } catch (error) {
197
+ emit('error', { message: error instanceof Error ? error.message : String(error) })
198
+ } finally {
199
+ off()
200
+ run.ended = true
201
+ run.endedAt = Date.now()
202
+ audit({ ask: 'end', driver: driverName, changed })
203
+ for (const follower of [...run.followers]) follower({ event: 'end', data: {} })
204
+ }
205
+ })()
206
+ return run
207
+ }
208
+ /** Follow a run as server-sent events: a `run` event (what it is, whether it already ended), the events so far, then the new ones until it ends. Following does not keep it alive. */
209
+ const follow = (c: Parameters<typeof streamSSE>[0], run: Run, announce: boolean): Response =>
210
+ streamSSE(c, async (stream) => {
211
+ if (announce)
212
+ await stream.writeSSE({
213
+ event: 'run',
214
+ data: JSON.stringify({
215
+ id: run.id,
216
+ driver: run.driver,
217
+ message: run.message,
218
+ ended: run.ended,
219
+ endedAt: run.endedAt ?? null,
220
+ }),
133
221
  })
134
- audit({ ask: 'start', driver: driverName, characters: message.length })
135
- // Stop in the page closes the stream; that cancels the run (the model call, the tool or the program in flight is dropped).
136
- const cancel = new AbortController()
137
- stream.onAbort(() => cancel.abort())
138
- try {
139
- for await (const { event, data } of driver.start({ message, signal: cancel.signal }))
140
- await send(event, data)
141
- await send('done', { changed })
142
- } catch (error) {
143
- await send('error', { message: error instanceof Error ? error.message : String(error) })
144
- } finally {
145
- off()
146
- running = false
147
- audit({ ask: 'end', driver: driverName, changed })
148
- }
222
+ const queue: Array<{ event: string; data: unknown }> = [...run.events]
223
+ let wake: (() => void) | undefined
224
+ const follower = (item: { event: string; data: unknown }): void => {
225
+ queue.push(item)
226
+ wake?.()
227
+ }
228
+ if (!run.ended) run.followers.add(follower)
229
+ let open = true
230
+ stream.onAbort(() => {
231
+ open = false
232
+ run.followers.delete(follower)
233
+ wake?.()
149
234
  })
235
+ try {
236
+ while (open) {
237
+ const item = queue.shift()
238
+ if (item === undefined) {
239
+ if (run.ended) break
240
+ await new Promise<void>((resolve) => {
241
+ wake = resolve
242
+ })
243
+ wake = undefined
244
+ continue
245
+ }
246
+ if (item.event === 'end') break
247
+ await stream.writeSSE({ event: item.event, data: JSON.stringify(item.data) })
248
+ }
249
+ } finally {
250
+ run.followers.delete(follower)
251
+ }
150
252
  })
151
253
  ctx.router.route('/', app)
152
254
  }
@@ -0,0 +1,72 @@
1
+ /**
2
+ * `npm run desktop` while `npm run dev` runs: the door allows one process per app folder, so a second server cannot start. Instead of failing, the window attaches to the dev server
3
+ * that is running, and its door (the same pages, the same agent, the same data); closing the window then leaves that server running.
4
+ *
5
+ * The running server is found where the door says it is: `.acryl/door.json` (its loopback address and pid). Only a live process on a loopback address that answers is attached to.
6
+ */
7
+ import { readFileSync } from 'node:fs'
8
+ import { join } from 'node:path'
9
+
10
+ export interface RunningApp {
11
+ readonly url: string
12
+ readonly pid: number
13
+ }
14
+
15
+ export interface AttachDeps {
16
+ readonly read?: (file: string) => string
17
+ /** True when a process with this pid is alive. */
18
+ readonly alive?: (pid: number) => boolean
19
+ /** Whether something answers HTTP at the address. */
20
+ readonly answers?: (url: string) => Promise<boolean>
21
+ }
22
+
23
+ const LOOPBACK = new Set(['127.0.0.1', 'localhost', '[::1]'])
24
+
25
+ const processAlive = (pid: number): boolean => {
26
+ try {
27
+ process.kill(pid, 0)
28
+ return true
29
+ } catch (error) {
30
+ return (error as NodeJS.ErrnoException).code === 'EPERM'
31
+ }
32
+ }
33
+
34
+ const httpAnswers = async (url: string): Promise<boolean> => {
35
+ try {
36
+ await fetch(url, { signal: AbortSignal.timeout(1500), redirect: 'manual' })
37
+ return true
38
+ } catch {
39
+ return false
40
+ }
41
+ }
42
+
43
+ /** @returns the dev server running from `root`, or `undefined` when there is none (no door file, a dead process, a stranger's address, nothing answering). */
44
+ export async function runningApp(
45
+ root: string,
46
+ deps: AttachDeps = {},
47
+ ): Promise<RunningApp | undefined> {
48
+ const read = deps.read ?? ((file: string) => readFileSync(file, 'utf8'))
49
+ let found: { url?: unknown; pid?: unknown }
50
+ try {
51
+ found = JSON.parse(read(join(root, '.acryl', 'door.json'))) as { url?: unknown; pid?: unknown }
52
+ } catch {
53
+ return undefined
54
+ }
55
+ if (
56
+ typeof found.url !== 'string' ||
57
+ typeof found.pid !== 'number' ||
58
+ !Number.isInteger(found.pid) ||
59
+ found.pid <= 0
60
+ )
61
+ return undefined
62
+ let parsed: URL
63
+ try {
64
+ parsed = new URL(found.url)
65
+ } catch {
66
+ return undefined
67
+ }
68
+ if (parsed.protocol !== 'http:' || !LOOPBACK.has(parsed.hostname)) return undefined
69
+ if (!(deps.alive ?? processAlive)(found.pid)) return undefined
70
+ if (!(await (deps.answers ?? httpAnswers)(parsed.origin))) return undefined
71
+ return { url: parsed.origin, pid: found.pid }
72
+ }
@@ -13,6 +13,7 @@ import { app, BrowserWindow, shell } from 'electron'
13
13
  import type { App } from '../../src/app.ts'
14
14
  import '../acryl-node-server/src/register.ts'
15
15
  import { selectInstance } from '../instance/index.ts'
16
+ import { runningApp } from './attach.ts'
16
17
  import { createDesktopShutdown, installShutdownRequests } from './shutdown.ts'
17
18
  import { resolveDesktopUserDataOverride } from './user-data.ts'
18
19
  import { windowOptions } from './window.ts'
@@ -65,9 +66,21 @@ if (!app.requestSingleInstanceLock()) {
65
66
 
66
67
  void app.whenReady().then(async () => {
67
68
  // The hook above is registered before the app's modules are loaded, so the app may have .tsx pages: they are loaded here, not as a static import.
68
- const { createApp } = await import('../../src/app.ts')
69
- running = await createApp(chosen)
70
- const origin = new URL(running.url).origin
69
+ // A dev server is already running from this folder (`npm run dev`): the door allows one process per folder, so the window opens on that server instead of starting a second one,
70
+ // and closing the window leaves it running.
71
+ const attached = await runningApp(projectRoot)
72
+ let url: string
73
+ if (attached !== undefined) {
74
+ console.log(
75
+ `desktop: attached to the app already running from this folder at ${attached.url} (its server keeps running when the window closes)`,
76
+ )
77
+ url = attached.url
78
+ } else {
79
+ const { createApp } = await import('../../src/app.ts')
80
+ running = await createApp(chosen)
81
+ url = running.url
82
+ }
83
+ const origin = new URL(url).origin
71
84
  window = new BrowserWindow(windowOptions({ title: chosen.name }))
72
85
  window.once('ready-to-show', () => window?.show())
73
86
  // Navigation stays on the app's own server; any other address opens in the system browser.
@@ -80,6 +93,6 @@ if (!app.requestSingleInstanceLock()) {
80
93
  event.preventDefault()
81
94
  void shell.openExternal(url)
82
95
  })
83
- await window.loadURL(new URL(startPath(), running.url).href)
96
+ await window.loadURL(new URL(startPath(), url).href)
84
97
  })
85
98
  }
@@ -21,7 +21,6 @@ import {
21
21
  readdirSync,
22
22
  readFileSync,
23
23
  renameSync,
24
- rmSync,
25
24
  statSync,
26
25
  writeFileSync,
27
26
  } from 'node:fs'
@@ -33,7 +32,7 @@ import type {} from '../instance/index.ts'
33
32
  import type { Sandbox } from '../sandbox/index.ts'
34
33
  import { appendLedger, type LedgerEntry, readLedger } from './ledger.ts'
35
34
  import { resolveFromApp, routerAliases } from './resolve.ts'
36
- import { hashFolder, prune, stage } from './stage.ts'
35
+ import { discard, hashFolder, prune, stage } from './stage.ts'
37
36
 
38
37
  /** A fiber's lifecycle state, by name (Cordis's own enum is a `const enum`, which does not exist at run time). */
39
38
  export type ExtensionState = 'pending' | 'loading' | 'active' | 'failed' | 'disposed' | 'unloading'
@@ -611,7 +610,7 @@ export class Extensions extends Service {
611
610
  try {
612
611
  module = await this.load(staged.dir, name)
613
612
  } catch (error) {
614
- rmSync(staged.dir, { recursive: true, force: true })
613
+ discard(staged.dir)
615
614
  this.record({ kind: 'refused', name, version: staged.version, error: message(error) })
616
615
  return {
617
616
  ok: false,
@@ -638,7 +637,7 @@ export class Extensions extends Service {
638
637
  : undefined
639
638
  if (failure) {
640
639
  await mounted.fiber.dispose().catch(() => undefined)
641
- rmSync(staged.dir, { recursive: true, force: true })
640
+ discard(staged.dir)
642
641
  const error = failure.error
643
642
  this.record({ kind: 'refused', name, version: staged.version, error })
644
643
  let restored = false
@@ -5,32 +5,60 @@
5
5
  * code; a copy at a new path has fresh URLs for the entry and everything it imports, so it is evaluated anew. The author's source folder
6
6
  * is never modified. The hash makes an unchanged source recognisable; the stamp makes "newest" well defined.
7
7
  */
8
- import { cpSync, existsSync, mkdirSync, readdirSync, rmSync } from 'node:fs'
8
+ import { cpSync, existsSync, mkdirSync, readdirSync, renameSync, rmSync } from 'node:fs'
9
9
  import { basename, join } from 'node:path'
10
10
  import { digestFolder, FOLDER_DIGEST_SKIPPED } from '@webboxes/blends-core'
11
11
 
12
12
  /** A short content digest of a folder, so an unchanged source keeps its version. */
13
13
  export const hashFolder = (dir: string, length = 12): string => digestFolder(dir, length)
14
14
 
15
- /** Copy `source` into a new version folder under `stageRoot/<name>/` and return its path and version. */
15
+ /**
16
+ * Whether `node --watch` runs this process. It watches every module the process has imported, and keeps watching the paths of the runs before; a staged copy that is deleted
17
+ * while it is watched restarts the app, and the restart stages and prunes again: a loop. So under the watcher a staged copy is never deleted (a later start without it prunes).
18
+ */
19
+ export const watched = (): boolean => process.env.WATCH_REPORT_DEPENDENCIES === '1'
20
+
21
+ /** Delete a staged folder, unless the watcher would take the deletion for a change of the app. */
22
+ export function discard(dir: string): void {
23
+ if (!watched()) rmSync(dir, { recursive: true, force: true })
24
+ }
25
+
26
+ /** The newest complete staged copy of `version` (copies are finished under a temporary name and renamed, so a folder with the final name is whole). */
27
+ function staged(stageRoot: string, name: string, version: string): string | undefined {
28
+ const folder = join(stageRoot, name)
29
+ if (!existsSync(folder)) return undefined
30
+ const found = readdirSync(folder)
31
+ .filter((entry) => !entry.startsWith('.') && entry.endsWith(`-${version}`))
32
+ .sort()
33
+ .pop()
34
+ return found === undefined ? undefined : join(folder, found)
35
+ }
36
+
37
+ /** Copy `source` into a version folder under `stageRoot/<name>/` and return its path and version. An unchanged source reuses its copy: a restart stages nothing new. */
16
38
  export function stage(
17
39
  source: string,
18
40
  stageRoot: string,
19
41
  name: string,
20
42
  ): { dir: string; version: string } {
21
43
  const version = hashFolder(source)
44
+ const existing = staged(stageRoot, name, version)
45
+ if (existing !== undefined) return { dir: existing, version }
22
46
  const stamp = String(Date.now()).padStart(15, '0')
23
47
  const dir = join(stageRoot, name, `${stamp}-${version}`)
24
- mkdirSync(dir, { recursive: true })
25
- cpSync(source, dir, {
48
+ const partial = join(stageRoot, name, `.${stamp}-${version}.partial`)
49
+ rmSync(partial, { recursive: true, force: true })
50
+ mkdirSync(partial, { recursive: true })
51
+ cpSync(source, partial, {
26
52
  recursive: true,
27
53
  filter: (path) => !FOLDER_DIGEST_SKIPPED.has(basename(path)),
28
54
  })
55
+ renameSync(partial, dir)
29
56
  return { dir, version }
30
57
  }
31
58
 
32
- /** Delete staged versions of `name` except the ones to keep (best effort). Modules already loaded stay in memory until the app restarts. */
59
+ /** Delete staged versions of `name` except the ones to keep (best effort; none under the watcher, see `watched`). Modules already loaded stay in memory until the app restarts. */
33
60
  export function prune(stageRoot: string, name: string, keep: readonly string[]): void {
61
+ if (watched()) return
34
62
  const folder = join(stageRoot, name)
35
63
  if (!existsSync(folder)) return
36
64
  if (keep.length === 0) {
@@ -102,7 +102,7 @@ export async function complete(
102
102
  function: {
103
103
  name: tool.name,
104
104
  description: tool.description,
105
- parameters: { type: 'object', additionalProperties: true },
105
+ parameters: tool.parameters ?? { type: 'object', additionalProperties: true },
106
106
  },
107
107
  })),
108
108
  }),
@@ -34,11 +34,15 @@ The door is a command in this folder, `.acryl/bin/acryl`, that lets you look at
34
34
  .acryl/bin/acryl act press --key <Enter|Escape|...>
35
35
  .acryl/bin/acryl act scroll [--ref <r>] --direction up|down [--amount <n>]
36
36
  .acryl/bin/acryl act wait (--text <t> | --role <role> [--name <n>]) [--gone] [--timeout-ms <ms>]
37
+ .acryl/bin/acryl act reload reload the page; answers once the new page is back
38
+ .acryl/bin/acryl act navigate --path <path> go to another path of the app, e.g. /settings (never another site)
37
39
  .acryl/bin/acryl plugin list | add <folder> | remove <name> | reload
38
40
  .acryl/bin/acryl tool list | call --tool <name> --args '<json>'
39
41
  .acryl/bin/acryl verbs the verb list
40
42
  ```
41
43
 
44
+ - **Check your own UI work with `look` and `act`.** After you build or change something people see, open the page and use it the way a person would: look, click through the new feature, look again, and say what you saw. A plugin you add, remove or reload with `plugin add|remove|reload` reloads the open pages by itself and the verb answers once they are back; after you change any other file (`src/`, a page) run `act reload`, then `look`. A page is a path of the app: `act navigate --path /demo`.
45
+ - The door's page script is added to every HTML page in development, so a page you write needs no tag for `look` and `act` to see it.
42
46
  - `look` and `act` need the app's page open in a browser (`brief` says `Page: not open` when it is not). Look again before each `act`: refs belong to the latest `look`.
43
47
  - A password field is not shown and cannot be typed into through the door; sign in by hand, or test the sign-in with a test.
44
48
  - Every verb takes `--json`. The exit code is 0 for done, 1 when the verb failed, 2 for a usage error and 3 when no app is running in this folder.
@@ -5,4 +5,6 @@ Besides `src/routes.ts`, the app runs an agent (`POST /ask`) that can write plug
5
5
  rollback; `GET /extensions` and `GET /tasks` show what it did. A plugin there is a folder with an `index.ts` exporting `name`, `inject`, `apply(ctx)`
6
6
  and an optional `check(ctx)` that receives the plugin's own context (so `ctx.router` reads as in `apply`). It may import `acryl-router`.
7
7
 
8
+ **Write an extension, not a row.** A feature you build while the app runs is a folder `plugins/<name>/` installed with `.acryl/bin/acryl plugin add plugins/<name>` (the `extensions` plugin loads it again at every start). Add a row to `blend.yaml` and an entry to the `registry` of `src/app.ts` only when the person asks for a plugin that is part of the app itself. Both kinds may import the framework by name (`acrylblends`, `acrylblends/body-limit`, `acryl-router`); a row's relative imports of its sibling plugins also work.
9
+
8
10
  **Production.** The agent, the builder and the extensions are development tools: `POST /ask` spends the model key and runs tools, a plugin installed live runs in this process, and none of it is behind a login. With `NODE_ENV=production` `src/app.ts` leaves those three rows out, so `/ask`, `/extensions*` and `/tasks*` are not mounted; the page and `GET /health` stay. `ACRYL_PRODUCTION_KEEP=agent,builder,extensions` keeps the named rows, and each one kept prints a `WARNING` line at start.
@@ -14,7 +14,7 @@ spec:
14
14
  # The address the server listens on.
15
15
  hostname: { type: string, default: 127.0.0.1 }
16
16
  # Steps an agent run may take.
17
- steps: { type: number, default: 6 }
17
+ steps: { type: number, default: 12 }
18
18
  # Where task records are kept: jsonl (a text file) or sqlite.
19
19
  taskStore: { type: string, default: jsonl }
20
20
  # When true, a plugin the agent installs waits for POST /extensions/approve.
@@ -48,6 +48,9 @@ spec:
48
48
  name: builder
49
49
  - id: save
50
50
  name: save
51
+ # The panel: a chat button on every page in development (the app's own agent, or Codex, Claude or pi started in this folder). It starts nothing in production.
52
+ - id: agent-panel
53
+ name: agent-panel
51
54
  - id: routes
52
55
  name: routes
53
56
  - id: server
@@ -7,6 +7,7 @@ import * as nodeServer from '../plugins/acryl-node-server/src/index.ts'
7
7
  import * as router from '../plugins/acryl-router/src/index.ts'
8
8
  import * as state from '../plugins/acryl-state/src/index.ts'
9
9
  import { agent, memory, model, tools } from '../plugins/agent/index.ts'
10
+ import * as panel from '../plugins/agent-panel/index.ts'
10
11
  import { builder } from '../plugins/builder/index.ts'
11
12
  import { extensions } from '../plugins/extensions/index.ts'
12
13
  import { type AppInstance, instance } from '../plugins/instance/index.ts'
@@ -37,6 +38,7 @@ export const registry = {
37
38
  sandbox: sandboxPlugin,
38
39
  routes,
39
40
  'acryl-node-server': nodeServer,
41
+ 'agent-panel': panel,
40
42
  'acryl-door': door,
41
43
  }
42
44
 
@@ -37,7 +37,7 @@ export const routes = {
37
37
  .get('/', (c) =>
38
38
  (c.req.header('accept') ?? '').includes('text/html')
39
39
  ? c.html(
40
- `<!doctype html><html lang="en"><head><meta charset="utf-8"><title>${APP_NAME}</title></head><body><h1>${APP_NAME}</h1><p>This app runs an agent unless it is switched off (production switches it off, see ACRYL_PRODUCTION_KEEP in AGENTS.md). Ask it for a feature (<code>POST /ask</code>), or add the chat panel with <code>npx acrylblends add agent-panel</code>.</p>${DOOR_TAG}</body></html>`,
40
+ `<!doctype html><html lang="en"><head><meta charset="utf-8"><title>${APP_NAME}</title></head><body><h1>${APP_NAME}</h1><p>This app runs an agent unless it is switched off (production switches it off, see ACRYL_PRODUCTION_KEEP in AGENTS.md). Ask it for a feature in the agent panel on this page (or with <code>POST /ask</code>).</p>${DOOR_TAG}</body></html>`,
41
41
  )
42
42
  : c.json({ app: APP_NAME, ok: true }),
43
43
  )
@@ -10,6 +10,7 @@
10
10
  "sandbox",
11
11
  "extensions",
12
12
  "builder",
13
+ "agent-panel",
13
14
  "manifest",
14
15
  "save",
15
16
  "acryl-door"
@@ -1,4 +1,5 @@
1
1
  import assert from 'node:assert/strict'
2
+ import { randomBytes } from 'node:crypto'
2
3
  import { mkdirSync } from 'node:fs'
3
4
  import { mkdtemp, rm } from 'node:fs/promises'
4
5
  import { join } from 'node:path'
@@ -7,6 +8,9 @@ import { fileURLToPath } from 'node:url'
7
8
  import { pinnedInstance, withPort } from '../plugins/instance/index.ts'
8
9
  import { createApp } from '../src/app.ts'
9
10
 
11
+ /** The plugin this test has the agent build: a name of its own, so a route the app has (a person's /hello) can never shadow it and fail the template's tests. */
12
+ const NAME = `builder-probe-${randomBytes(3).toString('hex')}`
13
+
10
14
  /** Throwaway homes live inside the project (`.acrylblends/` is ignored by git): a sandboxed agent may not write to the system temp folder. */
11
15
  const scratch = join(fileURLToPath(new URL('..', import.meta.url)), '.acrylblends', 'test')
12
16
  mkdirSync(scratch, { recursive: true })
@@ -24,28 +28,28 @@ test('the agent builds, updates, protects and removes a plugin in the running ap
24
28
  })
25
29
  return ((await response.json()) as { output: string }).output
26
30
  }
27
- const hello = async () => {
28
- const response = await app.root.router.request(`/hello`)
31
+ const served = async () => {
32
+ const response = await app.root.router.request(`/${NAME}`)
29
33
  return response.status === 200
30
34
  ? ((await response.json()) as { text: string }).text
31
35
  : response.status
32
36
  }
33
37
  try {
34
- assert.equal(await ask('create route hello /hello Hello'), 'hello: installed, active.')
35
- assert.equal(await hello(), 'Hello')
36
- assert.equal(await ask('update route hello /hello Bonjour'), 'hello: updated, active.')
37
- assert.equal(await hello(), 'Bonjour')
38
- assert.match(await ask('break plugin hello'), /Previous version restored: true/)
39
- assert.equal(await hello(), 'Bonjour')
38
+ assert.equal(await ask(`create route ${NAME} /${NAME} Hello`), `${NAME}: installed, active.`)
39
+ assert.equal(await served(), 'Hello')
40
+ assert.equal(await ask(`update route ${NAME} /${NAME} Bonjour`), `${NAME}: updated, active.`)
41
+ assert.equal(await served(), 'Bonjour')
42
+ assert.match(await ask(`break plugin ${NAME}`), /Previous version restored: true/)
43
+ assert.equal(await served(), 'Bonjour')
40
44
 
41
45
  // The broken version stays on disk, so put the working one back before restarting.
42
- await ask('update route hello /hello Bonjour')
46
+ await ask(`update route ${NAME} /${NAME} Bonjour`)
43
47
  await app.stop()
44
48
  app = await createApp(chosen, { listen: false })
45
- assert.equal(await hello(), 'Bonjour')
49
+ assert.equal(await served(), 'Bonjour')
46
50
 
47
- assert.equal(await ask('remove plugin hello'), 'hello: done.')
48
- assert.equal(await hello(), 404)
51
+ assert.equal(await ask(`remove plugin ${NAME}`), `${NAME}: done.`)
52
+ assert.equal(await served(), 404)
49
53
 
50
54
  // Every run is a durable task that owns its tool calls, and the records survived the restart.
51
55
  const records = (await (await app.root.router.request(`/tasks`)).json()) as Array<{
@@ -81,22 +85,22 @@ test('with approval on, an install waits for the owner; approving it makes it li
81
85
  ).json()) as Record<string, unknown>
82
86
  try {
83
87
  assert.equal(
84
- (await post('/ask', { question: 'create route hello /hello Hello' })).output,
85
- "hello: waiting for the owner's approval.",
88
+ (await post('/ask', { question: `create route ${NAME} /${NAME} Hello` })).output,
89
+ `${NAME}: waiting for the owner's approval.`,
86
90
  )
87
- assert.equal((await app.root.router.request(`/hello`)).status, 404)
91
+ assert.equal((await app.root.router.request(`/${NAME}`)).status, 404)
88
92
  const listed = (await (await app.root.router.request(`/extensions`)).json()) as {
89
93
  waitingForApproval: Array<{ name: string }>
90
94
  }
91
95
  assert.deepEqual(
92
96
  listed.waitingForApproval.map((request) => request.name),
93
- ['hello'],
97
+ [NAME],
94
98
  )
95
99
  assert.equal(
96
- ((await post('/extensions/approve', { name: 'hello' })) as { state?: string }).state,
100
+ ((await post('/extensions/approve', { name: NAME })) as { state?: string }).state,
97
101
  'active',
98
102
  )
99
- assert.deepEqual(await (await app.root.router.request(`/hello`)).json(), { text: 'Hello' })
103
+ assert.deepEqual(await (await app.root.router.request(`/${NAME}`)).json(), { text: 'Hello' })
100
104
  const tasks = (await (await app.root.router.request(`/tasks`)).json()) as Array<{
101
105
  manifest?: string
102
106
  }>
@@ -168,12 +172,13 @@ test('what happens is streamed from the state cells: the run as a task, and the
168
172
  await app.root.router.request(`/ask`, {
169
173
  method: 'POST',
170
174
  headers: { 'content-type': 'application/json' },
171
- body: JSON.stringify({ question: 'create route hello /hello Hello' }),
175
+ body: JSON.stringify({ question: `create route ${NAME} /${NAME} Hello` }),
172
176
  })
173
177
  const arrived = (stream: { text: string }, what: RegExp) => what.test(stream.text)
174
178
  for (
175
179
  let waited = 0;
176
- waited < 3000 && !arrived(extensionsStream.seen, /"name":"hello"[^\n]*"state":"active"/);
180
+ waited < 3000 &&
181
+ !arrived(extensionsStream.seen, new RegExp(`"name":"${NAME}"[^\\n]*"state":"active"`));
177
182
  waited += 25
178
183
  ) {
179
184
  await new Promise((done) => setTimeout(done, 25))
@@ -189,7 +194,7 @@ test('what happens is streamed from the state cells: the run as a task, and the
189
194
  )
190
195
  assert.ok(arrived(tasksStream.seen, /"kind":"tool:install_plugin"/), 'the install is a task')
191
196
  assert.ok(
192
- arrived(extensionsStream.seen, /"name":"hello"[^\n]*"state":"active"/),
197
+ arrived(extensionsStream.seen, new RegExp(`"name":"${NAME}"[^\\n]*"state":"active"`)),
193
198
  'the extension is reported',
194
199
  )
195
200
  // In process there is no socket to close: the readers are cancelled, which ends the streams and lets the pumps finish.
@@ -1,5 +0,0 @@
1
- import "./factory.js";
2
- import "./ssg-BwjcA-Ge.js";
3
- import "./css.js";
4
- import "./connection-C7xgP1hP.js";
5
- export {};
@@ -1,12 +0,0 @@
1
- import "./basic-auth.js";
2
- import "./combine.js";
3
- import "./context-storage.js";
4
- import "./ip-restriction.js";
5
- import "./jsx-renderer.js";
6
- import "./jwk-LkzeoaU8.js";
7
- import "./jwt-CskdhEW9.js";
8
- import "./method-not-allowed.js";
9
- import "./serve-static-Cp5adzGo.js";
10
- import "./timeout.js";
11
- import "./loopback-only.js";
12
- export {};