acrylblends 1.1.0 → 1.3.0

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 (263) hide show
  1. package/README.md +9 -0
  2. package/dist/index.d.ts +47 -2
  3. package/dist/index.js +2017 -32
  4. package/dist/index.js.map +1 -1
  5. package/docs/framework/agents/index.md +1 -1
  6. package/docs/framework/getting-started/desktop.md +23 -4
  7. package/docs/framework/guides/create-acrylblends.md +5 -3
  8. package/docs/framework/workshop/README.md +139 -153
  9. package/docs/framework/workshop/facilitator.md +69 -101
  10. package/legacy/README.md +9 -0
  11. package/legacy/agent/157ce3db1112/src/app.ts +98 -0
  12. package/legacy/agent/157ce3db1112/src/routes.ts +63 -0
  13. package/legacy/agent/24c681a00fca/src/app.ts +160 -0
  14. package/legacy/agent/24c681a00fca/src/routes.ts +83 -0
  15. package/legacy/agent/2e790da977b2/src/app.ts +98 -0
  16. package/legacy/agent/2e790da977b2/src/routes.ts +63 -0
  17. package/legacy/agent/6307045545dc/src/app.ts +168 -0
  18. package/legacy/agent/6307045545dc/src/routes.ts +83 -0
  19. package/legacy/agent/6a5fb9c15c53/src/app.ts +158 -0
  20. package/legacy/agent/6a5fb9c15c53/src/routes.ts +83 -0
  21. package/legacy/agent/71fb54a5a942/src/app.ts +111 -0
  22. package/legacy/agent/71fb54a5a942/src/routes.ts +63 -0
  23. package/legacy/agent/74ece148269c/src/app.ts +168 -0
  24. package/legacy/agent/74ece148269c/src/routes.ts +93 -0
  25. package/legacy/agent/855359304707/src/app.ts +168 -0
  26. package/legacy/agent/855359304707/src/routes.ts +126 -0
  27. package/legacy/agent/b5ef3c786ae4/src/app.ts +127 -0
  28. package/legacy/agent/b5ef3c786ae4/src/routes.ts +76 -0
  29. package/legacy/agent/c2f30958043b/src/app.ts +98 -0
  30. package/legacy/agent/c2f30958043b/src/routes.ts +53 -0
  31. package/legacy/blank/5a91c558af22/src/routes.ts +18 -0
  32. package/legacy/blank/9c650cf52bf3/src/routes.ts +14 -0
  33. package/legacy/blank/aea589283445/src/routes.ts +16 -0
  34. package/legacy/blank/b2cbb759c889/src/routes.ts +16 -0
  35. package/legacy/blank/d6d371c9a65e/src/routes.ts +18 -0
  36. package/legacy/chat/7076cc821e2c/src/app.ts +72 -0
  37. package/legacy/chat/7076cc821e2c/src/routes.ts +13 -0
  38. package/legacy/chat/7dbe6d5170df/src/app.ts +107 -0
  39. package/legacy/chat/7dbe6d5170df/src/routes.ts +19 -0
  40. package/legacy/chat/a9a9ab44c58e/src/app.ts +107 -0
  41. package/legacy/chat/a9a9ab44c58e/src/routes.ts +15 -0
  42. package/legacy/chat/f20ad9a51b06/src/app.ts +106 -0
  43. package/legacy/chat/f20ad9a51b06/src/routes.ts +15 -0
  44. package/legacy/layouts.json +524 -0
  45. package/legacy/nodejs/1d477f37aaf4/src/app.ts +92 -0
  46. package/legacy/nodejs/1d477f37aaf4/src/routes.ts +40 -0
  47. package/legacy/nodejs/b1bd9462496c/src/app.ts +61 -0
  48. package/legacy/nodejs/b1bd9462496c/src/routes.ts +37 -0
  49. package/legacy/nodejs/d5bab75076ef/src/app.ts +95 -0
  50. package/legacy/nodejs/d5bab75076ef/src/routes.ts +40 -0
  51. package/legacy/notes/4edc36949c15/src/app.ts +107 -0
  52. package/legacy/notes/4edc36949c15/src/routes.ts +14 -0
  53. package/legacy/notes/6cab2d09747c/src/app.ts +72 -0
  54. package/legacy/notes/6cab2d09747c/src/routes.ts +12 -0
  55. package/legacy/notes/7ca2dfed623a/src/app.ts +106 -0
  56. package/legacy/notes/7ca2dfed623a/src/routes.ts +14 -0
  57. package/legacy/todo/02e613b3625c/src/app.ts +71 -0
  58. package/legacy/todo/02e613b3625c/src/todo.ts +123 -0
  59. package/legacy/todo/45694a8d9e15/src/app.ts +71 -0
  60. package/legacy/todo/45694a8d9e15/src/todo.ts +96 -0
  61. package/legacy/todo/5923d3a6099f/src/app.ts +71 -0
  62. package/legacy/todo/5923d3a6099f/src/todo.ts +125 -0
  63. package/legacy/todo/849a18d66dc1/src/app.ts +91 -0
  64. package/legacy/todo/849a18d66dc1/src/todo.ts +125 -0
  65. package/legacy/todo/8e64211312f3/src/app.ts +71 -0
  66. package/legacy/todo/8e64211312f3/src/todo.ts +125 -0
  67. package/legacy/todo/b79078a87565/src/app.ts +71 -0
  68. package/legacy/todo/b79078a87565/src/todo.ts +125 -0
  69. package/lib/{audit-BFpULBCj.d.ts → audit-Bok_UNCn.d.ts} +26 -3
  70. package/lib/{client-modules-drj4mjhj.js → client-modules-Bkdr83jO.js} +1 -1
  71. package/lib/cordis/door.d.ts +87 -9
  72. package/lib/cordis/door.js +324 -60
  73. package/lib/cordis/inspect.js +1 -1
  74. package/lib/cordis/node-server.d.ts +101 -3
  75. package/lib/cordis/node-server.js +3 -3
  76. package/lib/cordis/router-http.d.ts +7 -1
  77. package/lib/cordis/router-http.js +44 -3
  78. package/lib/cordis/router.d.ts +1 -1
  79. package/lib/cordis/router.js +1 -1
  80. package/lib/cordis/state.js +1 -1
  81. package/lib/door/host.d.ts +2 -2
  82. package/lib/door/host.js +1 -1
  83. package/lib/door/page.d.ts +10 -1
  84. package/lib/door/page.js +46 -7
  85. package/lib/door/protocol.d.ts +1 -1
  86. package/lib/drivers.d.ts +10 -1
  87. package/lib/{index-DMOtA-NJ.d.ts → index-DGq7H0lb.d.ts} +11 -1
  88. package/lib/node-server.d.ts +7 -1
  89. package/lib/node-server.js +2 -2
  90. package/lib/{protocol-a2aqrp2X.d.ts → protocol-BeIYCgTo.d.ts} +4 -0
  91. package/lib/{snapshot-text-BwM6T2nG.js → snapshot-text-CXTnjxGx.js} +87 -7
  92. package/lib/src-CtkIX_Gg.js +427 -0
  93. package/lib/{src-BTBm4OPD.js → src-iBJ2ysV9.js} +77 -14
  94. package/lib/{test-websocket-BLeEvybj.js → test-websocket-CQOSvBth.js} +17 -7
  95. package/lib/{test-websocket-DyeS50Tr.d.ts → test-websocket-GUTAkFQT.d.ts} +3 -1
  96. package/package.json +2 -1
  97. package/plugins/acryl-door/plugin.json +3 -0
  98. package/plugins/acryl-door/src/cli/acryl.ts +63 -6
  99. package/plugins/acryl-door/src/host/channel.ts +86 -9
  100. package/plugins/acryl-door/src/host/chat-tools.ts +8 -6
  101. package/plugins/acryl-door/src/host/door-file.ts +7 -2
  102. package/plugins/acryl-door/src/host/door.ts +128 -24
  103. package/plugins/acryl-door/src/host/operator-routes.ts +21 -1
  104. package/plugins/acryl-door/src/host/page-routes.ts +3 -1
  105. package/plugins/acryl-door/src/host/page-socket.ts +18 -8
  106. package/plugins/acryl-door/src/host/verbs.ts +14 -1
  107. package/plugins/acryl-door/src/index.ts +168 -30
  108. package/plugins/acryl-door/src/page/base.ts +32 -0
  109. package/plugins/acryl-door/src/page/channel.ts +3 -1
  110. package/plugins/acryl-door/src/page/driver/accessibility.ts +11 -0
  111. package/plugins/acryl-door/src/page/driver/actions.ts +4 -0
  112. package/plugins/acryl-door/src/page/driver/driver.ts +32 -4
  113. package/plugins/acryl-door/src/page/driver/snapshot.ts +5 -2
  114. package/plugins/acryl-door/src/page/index.ts +41 -2
  115. package/plugins/acryl-door/src/page/outline.ts +56 -0
  116. package/plugins/acryl-door/src/page/protocol.ts +4 -0
  117. package/plugins/acryl-door/src/page/snapshot-text.ts +2 -1
  118. package/plugins/acryl-node-server/src/adapter/listener.ts +4 -0
  119. package/plugins/acryl-node-server/src/adapter/raw.ts +18 -0
  120. package/plugins/acryl-node-server/src/adapter/websocket.ts +15 -9
  121. package/plugins/acryl-node-server/src/client-modules.ts +7 -1
  122. package/plugins/acryl-node-server/src/index.ts +304 -16
  123. package/plugins/acryl-node-server/src/upgrade-hub.ts +2 -2
  124. package/plugins/acryl-page/plugin.json +12 -0
  125. package/plugins/acryl-page/src/index.ts +163 -0
  126. package/plugins/acryl-router/src/service.ts +77 -14
  127. package/plugins/acryl-router-http/src/index.ts +15 -1
  128. package/plugins/acryl-router-http/src/serve.ts +31 -0
  129. package/plugins/acryl-web-server/plugin.json +14 -0
  130. package/plugins/acryl-web-server/src/index.ts +164 -0
  131. package/plugins/agent/agent.ts +38 -7
  132. package/plugins/agent/index.ts +1 -1
  133. package/plugins/agent/model.ts +10 -2
  134. package/plugins/agent/tools.ts +9 -3
  135. package/plugins/agent-panel/client/describe.ts +57 -0
  136. package/plugins/agent-panel/client/driver-pick.ts +22 -0
  137. package/plugins/agent-panel/client/frame.ts +51 -0
  138. package/plugins/agent-panel/client/main.ts +238 -26
  139. package/plugins/agent-panel/drivers.ts +7 -2
  140. package/plugins/agent-panel/headless.ts +5 -2
  141. package/plugins/agent-panel/index.ts +146 -57
  142. package/plugins/agent-panel/mount.ts +27 -0
  143. package/plugins/agent-panel/visitors.ts +231 -0
  144. package/plugins/builder/index.ts +13 -35
  145. package/plugins/builder/package.ts +186 -0
  146. package/plugins/builder/plugin.json +3 -1
  147. package/plugins/desktop/frame.ts +231 -0
  148. package/plugins/desktop/launch.ts +44 -9
  149. package/plugins/desktop/main.ts +100 -32
  150. package/plugins/desktop/window.ts +21 -4
  151. package/plugins/driver-pty/client/terminal.ts +4 -1
  152. package/plugins/driver-pty/index.ts +38 -11
  153. package/plugins/driver-pty/spawn.ts +38 -0
  154. package/plugins/driver-pty/terminal.ts +2 -0
  155. package/plugins/extensions/index.ts +47 -18
  156. package/plugins/extensions/plugin-store.ts +27 -0
  157. package/plugins/extensions/plugin.json +1 -1
  158. package/plugins/extensions/resolve.ts +13 -0
  159. package/plugins/extensions/stage.ts +15 -5
  160. package/plugins/feedback/client.d.ts +22 -0
  161. package/plugins/feedback/client.js +471 -0
  162. package/plugins/feedback/index.ts +143 -0
  163. package/plugins/feedback/plugin.json +11 -0
  164. package/plugins/feedback/store.ts +307 -0
  165. package/plugins/home/index.ts +122 -0
  166. package/plugins/home/plugin.json +11 -0
  167. package/plugins/instance/index.ts +6 -0
  168. package/plugins/manifest/app-plugins.ts +86 -0
  169. package/plugins/manifest/index.ts +12 -2
  170. package/plugins/manifest/plugin.json +1 -1
  171. package/plugins/manifest/snapshot.ts +275 -0
  172. package/plugins/openai/index.ts +64 -5
  173. package/plugins/openai/prompt.ts +23 -0
  174. package/plugins/plugin-share/README.md +51 -0
  175. package/plugins/plugin-share/index.ts +467 -0
  176. package/plugins/plugin-share/plugin.json +15 -0
  177. package/plugins/plugin-share/publish.ts +65 -0
  178. package/plugins/plugin-share/pull-check.ts +129 -0
  179. package/plugins/plugin-share/pulled-records.ts +70 -0
  180. package/shared/AGENTS.core.md +36 -16
  181. package/shared/skills/acryl-add-ui/SKILL.md +19 -0
  182. package/shared/skills/acryl-app/SKILL.md +13 -0
  183. package/shared/skills/acryl-build-plugin/SKILL.md +22 -0
  184. package/shared/skills/acryl-change-plugin/SKILL.md +14 -0
  185. package/shared/skills/acryl-diagnose-plugin/SKILL.md +13 -0
  186. package/shared/skills/acryl-fix-plugin/SKILL.md +13 -0
  187. package/shared/skills/acryl-remove-plugin/SKILL.md +12 -0
  188. package/shared/skills/acryl-share-plugin/SKILL.md +16 -0
  189. package/shared/skills/cordis-plugin/SKILL.md +26 -0
  190. package/templates/agent/.gitattributes +2 -0
  191. package/templates/agent/AGENTS.md +1 -1
  192. package/templates/agent/blend.yaml +6 -0
  193. package/templates/agent/gitignore +1 -0
  194. package/templates/agent/package.json +1 -0
  195. package/templates/agent/src/app.ts +154 -33
  196. package/templates/agent/src/frame.ts +17 -0
  197. package/templates/agent/src/main.ts +5 -3
  198. package/templates/agent/src/routes.ts +38 -19
  199. package/templates/agent/template.json +3 -0
  200. package/templates/agent/test/app.test.ts +45 -3
  201. package/templates/blank/.gitattributes +2 -0
  202. package/templates/blank/AGENTS.md +3 -2
  203. package/templates/blank/blend.yaml +11 -0
  204. package/templates/blank/gitignore +1 -0
  205. package/templates/blank/package.json +1 -0
  206. package/templates/blank/src/app.ts +100 -29
  207. package/templates/blank/src/door.ts +7 -0
  208. package/templates/blank/src/first-page.ts +5 -0
  209. package/templates/blank/src/frame.ts +17 -0
  210. package/templates/blank/src/routes.ts +65 -8
  211. package/templates/blank/template.json +14 -1
  212. package/templates/blank/test/app.test.ts +103 -16
  213. package/templates/chat/.gitattributes +2 -0
  214. package/templates/chat/blend.yaml +2 -1
  215. package/templates/chat/gitignore +1 -0
  216. package/templates/chat/package.json +1 -0
  217. package/templates/chat/src/app.ts +72 -22
  218. package/templates/chat/src/chat-app.ts +11 -2
  219. package/templates/chat/src/client/chat.tsx +4 -2
  220. package/templates/chat/src/frame.ts +17 -0
  221. package/templates/chat/src/page.tsx +6 -4
  222. package/templates/chat/src/routes.ts +14 -4
  223. package/templates/chat/src/runtime.ts +9 -3
  224. package/templates/chat/template.json +3 -1
  225. package/templates/chat/test/chat.test.ts +46 -1
  226. package/templates/chat/test/server.test.ts +17 -3
  227. package/templates/nodejs/.gitattributes +2 -0
  228. package/templates/nodejs/blend.yaml +2 -1
  229. package/templates/nodejs/gitignore +1 -0
  230. package/templates/nodejs/package.json +1 -0
  231. package/templates/nodejs/src/app.ts +76 -21
  232. package/templates/nodejs/src/frame.ts +17 -0
  233. package/templates/nodejs/src/page.tsx +3 -3
  234. package/templates/nodejs/src/routes.ts +32 -13
  235. package/templates/nodejs/template.json +3 -1
  236. package/templates/nodejs/test/app.test.ts +38 -3
  237. package/templates/nodejs/test/server.test.ts +17 -3
  238. package/templates/notes/.gitattributes +2 -0
  239. package/templates/notes/blend.yaml +2 -1
  240. package/templates/notes/gitignore +1 -0
  241. package/templates/notes/package.json +1 -0
  242. package/templates/notes/src/app.ts +72 -22
  243. package/templates/notes/src/frame.ts +17 -0
  244. package/templates/notes/src/notes-app.ts +24 -17
  245. package/templates/notes/src/routes.ts +11 -4
  246. package/templates/notes/src/runtime.ts +9 -3
  247. package/templates/notes/src/views.tsx +26 -17
  248. package/templates/notes/template.json +3 -1
  249. package/templates/notes/test/notes.test.ts +39 -1
  250. package/templates/notes/test/server.test.ts +17 -3
  251. package/templates/todo/.gitattributes +2 -0
  252. package/templates/todo/blend.yaml +3 -0
  253. package/templates/todo/gitignore +1 -0
  254. package/templates/todo/package.json +1 -0
  255. package/templates/todo/src/app.ts +81 -27
  256. package/templates/todo/src/door.ts +7 -0
  257. package/templates/todo/src/frame.ts +17 -0
  258. package/templates/todo/src/page.ts +13 -6
  259. package/templates/todo/src/todo.ts +18 -6
  260. package/templates/todo/template.json +11 -1
  261. package/templates/todo/test/todo.test.ts +67 -9
  262. package/lib/src-LX0E4ec4.js +0 -117
  263. package/shared/SKILL.md +0 -14
@@ -2,16 +2,31 @@ import { t as Acryl } from "../app-CQcGWjrk.js";
2
2
  import { r as streamSSE } from "../streaming-BlDaph8U.js";
3
3
  import { r as loopbackAuthority } from "../loopback-only-BbasoF7R.js";
4
4
  import "../helpers-BEjcdbgw.js";
5
- import { n as clientModules } from "../client-modules-drj4mjhj.js";
5
+ import { n as clientModules } from "../client-modules-Bkdr83jO.js";
6
6
  import { a as MAX_WAIT_MS, d as parseChannelToHost, l as UiControlError, p as parseUiRequest, r as MAX_SNAPSHOT_NODES, u as WINDOW_ID } from "../protocol-CQ_if7Dg.js";
7
- import { a as INVALID_JSON, c as operatorOnly, d as AuditLog, f as auditPath, i as DOOR_PREFIX, l as privateApi, o as error, p as UiChannel, r as pageChannelRoutes, s as jsonBody, t as renderSnapshot, u as readJson } from "../snapshot-text-BwM6T2nG.js";
8
- import "../src-BTBm4OPD.js";
7
+ import { a as INVALID_JSON, c as operatorOnly, d as AuditLog, f as auditPath, i as DOOR_PREFIX, l as privateApi, o as error, p as UiChannel, r as pageChannelRoutes, s as jsonBody, t as renderSnapshot, u as readJson } from "../snapshot-text-CXTnjxGx.js";
8
+ import "../src-iBJ2ysV9.js";
9
9
  import "../middleware-BfG_fp8P.js";
10
10
  import { isAbsolute, join, relative, resolve } from "node:path";
11
11
  import { chmodSync, existsSync, mkdirSync, readFileSync, renameSync, rmSync, writeFileSync } from "node:fs";
12
12
  import z from "@deepseek-ai/schemastery";
13
13
  import { randomBytes } from "node:crypto";
14
14
  import { spawnSync } from "node:child_process";
15
+ //#region ../../plugins/acryl-door/src/page/base.ts
16
+ /**
17
+ * `path` (an app path, optionally with a query) under `base`: '/board' under '/app' is '/app/board'; the app's root is the base itself, so '/' is
18
+ * '/app' (its mount) and not '/app/'; without a base, the path as written.
19
+ */
20
+ function underAppBase(base, path) {
21
+ if (base === "") return path;
22
+ const at = path.search(/[?#]/u);
23
+ const route = at === -1 ? path : path.slice(0, at);
24
+ const rest = at === -1 ? "" : path.slice(at);
25
+ if (route === base || route.startsWith(`${base}/`)) return path;
26
+ if (route === "/" || route === "") return `${base}${rest}`;
27
+ return `${base}${route.startsWith("/") ? "" : "/"}${route}${rest}`;
28
+ }
29
+ //#endregion
15
30
  //#region ../../plugins/acryl-door/src/shell/protocol.ts
16
31
  var ShellError = class extends Error {
17
32
  code;
@@ -283,12 +298,16 @@ function shellRoutes(prefix, secret, channel, heartbeatMs = SHELL_HEARTBEAT_MS)
283
298
  * MCP (`tools/list`) and the built-in chat (the `tools` registry). A verb added here is in all three, or the comparison test fails.
284
299
  */
285
300
  /** Every verb, in the order `brief` lists them. */
301
+ /** The verbs that only use the page: what a page-only door (a production assistant) offers. */
302
+ const PAGE_VERBS = ["look", "act"];
286
303
  const VERBS = [
287
304
  "brief",
288
305
  "look",
289
306
  "act",
290
307
  "plugin-list",
291
308
  "plugin-add",
309
+ "plugin-pull",
310
+ "plugin-publish",
292
311
  "plugin-remove",
293
312
  "plugin-reload",
294
313
  "tool-list",
@@ -444,6 +463,42 @@ const VERB_SPECS = {
444
463
  additionalProperties: false
445
464
  }
446
465
  },
466
+ "plugin-pull": {
467
+ usage: "plugin pull <source>: fetch a plugin someone released (github:<owner>/<repo>[@<version>][@sha256:<digest>], or a .tgz), check it and start it, live",
468
+ description: "Fetch a plugin someone released, without running anything; check its digest, that it works on every OS and that this app has the services it needs; then install and start it without a restart. A refusal says why and changes nothing.",
469
+ arguments: {
470
+ type: "object",
471
+ properties: { source: {
472
+ type: "string",
473
+ description: "github:<owner>/<repo>[@<version>][@sha256:<digest>], or a .tgz path or https address"
474
+ } },
475
+ required: ["source"],
476
+ additionalProperties: false
477
+ }
478
+ },
479
+ "plugin-publish": {
480
+ usage: "plugin publish <name> [--owner <account>] [--license <id>]: show what publishing this plugin would send; in a person's own terminal, ask them",
481
+ description: "Show what publishing a plugin would send: the account, repository, version, license, files and the line a friend pulls it with. Sends nothing. Only a person can say yes, in their own terminal or with Share in the frame; tell them the plan and how.",
482
+ arguments: {
483
+ type: "object",
484
+ properties: {
485
+ name: {
486
+ type: "string",
487
+ description: "a plugin of plugins/"
488
+ },
489
+ owner: {
490
+ type: "string",
491
+ description: "the GitHub account or organization; the signed-in one when absent"
492
+ },
493
+ license: {
494
+ type: "string",
495
+ description: "an SPDX license id; MIT when absent"
496
+ }
497
+ },
498
+ required: ["name"],
499
+ additionalProperties: false
500
+ }
501
+ },
447
502
  "plugin-remove": {
448
503
  usage: "plugin remove <name>: stop and remove an installed plugin",
449
504
  description: "Stop and remove an installed plugin.",
@@ -598,6 +653,14 @@ var DoorError = class extends Error {
598
653
  this.name = "DoorError";
599
654
  }
600
655
  };
656
+ /** What a plan says, for a person deciding. */
657
+ function renderPublishPlan(plan) {
658
+ return [
659
+ `Publish to ${plan.owner}/${plan.repo}${plan.createsRepository ? " (a new repository)" : ""}, ${plan.visibility}, version ${plan.version}, license ${plan.license}.`,
660
+ `Files: ${plan.files.join(", ")}.`,
661
+ `A friend gets exactly these bytes with: ${plan.pullLine}`
662
+ ].join("\n");
663
+ }
601
664
  /** A shell answer as text an agent reads: windows and menu items one to a line. */
602
665
  function renderShell(result) {
603
666
  if ("windows" in result) return result.windows.length === 0 ? "no windows are open" : result.windows.map((window) => `${String(window.id)} ${JSON.stringify(window.title)} ${window.path} ${String(window.width)}x${String(window.height)}${window.x === void 0 || window.y === void 0 ? "" : `@${String(window.x)},${String(window.y)}`}${window.focused ? " focused" : ""}`).join("\n");
@@ -625,19 +688,19 @@ var Door = class {
625
688
  return this.deps.channel.driveFor(windowId);
626
689
  }
627
690
  /** Run one verb. @throws DoorError or UiControlError. */
628
- async run(verb, args, signal) {
691
+ async run(verb, args, signal, scope = {}) {
629
692
  const input = typeof args === "object" && args !== null && !Array.isArray(args) ? args : {};
630
693
  switch (verb) {
631
694
  case "brief": return this.brief();
632
695
  case "look": {
633
- const snapshot = await this.look(input, signal);
696
+ const snapshot = await this.look(input, signal, scope);
634
697
  return {
635
698
  value: snapshot,
636
699
  text: renderSnapshot(snapshot, (cursor) => `run look --cursor ${String(cursor)}`)
637
700
  };
638
701
  }
639
702
  case "act": {
640
- const result = await this.act(input, signal);
703
+ const result = await this.act(input, signal, scope);
641
704
  return {
642
705
  value: result,
643
706
  text: `done${result.target === void 0 ? "" : `: ${result.target.role} "${result.target.name}"`}${result.detail === void 0 ? "" : ` (${result.detail})`}; look again before the next act`
@@ -645,6 +708,8 @@ var Door = class {
645
708
  }
646
709
  case "plugin-list": return this.plugin((extensions) => extensions.list(), false);
647
710
  case "plugin-add": return this.plugin((extensions) => extensions.install(this.insideApp(input.folder)));
711
+ case "plugin-pull": return this.pull(input.source);
712
+ case "plugin-publish": return this.planPublish(input);
648
713
  case "plugin-remove": {
649
714
  const name = input.name;
650
715
  if (typeof name !== "string" || name === "") throw new DoorError("invalid", "plugin remove needs a plugin name");
@@ -694,33 +759,47 @@ var Door = class {
694
759
  ].join("\n")
695
760
  };
696
761
  }
697
- look(input, signal) {
698
- const { window, rest } = this.windowOf(input);
762
+ look(input, signal, scope = {}) {
763
+ const { window, rest } = this.windowOf(input, scope);
699
764
  const request = this.parse({
700
765
  ...rest,
701
766
  op: "snapshot"
702
767
  });
703
- return this.callPage("look", request, signal, window);
768
+ return this.callPage("look", request, signal, window, scope.page);
704
769
  }
705
- async act(input, signal) {
770
+ async act(input, signal, scope = {}) {
706
771
  if (input.op === "snapshot") throw new DoorError("invalid", "use look to see the page");
707
- const { window, rest } = this.windowOf(input);
772
+ const { window, rest } = this.windowOf(input, scope);
708
773
  const request = this.parse(rest);
709
- if (request.op !== "reload" && request.op !== "navigate") return await this.callPage(`act ${String(input.op)}`, request, signal, window);
710
- const back = this.deps.channel.expectNewPage(this.deps.pageBackMs ?? PAGE_BACK_MS);
774
+ const page = scope.page;
775
+ const moves = request.op === "reload" || request.op === "navigate";
776
+ if (!moves && request.op !== "click" && request.op !== "press" && request.op !== "type") return await this.callPage(`act ${String(input.op)}`, request, signal, window, page);
777
+ const backMs = this.deps.pageBackMs ?? PAGE_BACK_MS;
778
+ const stay = new AbortController();
779
+ const back = page === void 0 ? this.deps.channel.expectNewPage(backMs, stay.signal) : this.deps.channel.expectPageBack(page, backMs, stay.signal);
711
780
  let answer;
781
+ let left = moves;
712
782
  try {
713
- answer = await this.callPage(`act ${request.op}`, request, signal, window);
783
+ answer = await this.callPage(`act ${request.op}`, request, signal, window, page);
784
+ left ||= answer.leaving === true;
714
785
  } catch (cause) {
715
- if (!(cause instanceof UiControlError && cause.code === "disconnected")) throw cause;
786
+ if (!(cause instanceof UiControlError && cause.code === "disconnected")) {
787
+ stay.abort();
788
+ throw cause;
789
+ }
716
790
  answer = {
717
791
  ok: true,
718
- detail: request.op === "reload" ? "reloading" : `going to ${request.path}`
792
+ detail: request.op === "reload" ? "reloading" : request.op === "navigate" ? `going to ${request.path}` : "the page is going to another address"
719
793
  };
794
+ left = true;
795
+ }
796
+ if (!left) {
797
+ stay.abort();
798
+ return answer;
720
799
  }
721
800
  const came = await back;
722
- if (came !== void 0) this.deps.channel.prefer(came);
723
- if (came === void 0) throw new UiControlError("timeout", request.op === "reload" ? "the page did not come back after the reload" : `the page did not come back after going to ${request.path}: is there a page at that path, and does it load the door's script?`);
801
+ if (came !== void 0 && page === void 0) this.deps.channel.prefer(came);
802
+ if (came === void 0) throw new UiControlError("timeout", request.op === "reload" ? "the page did not come back after the reload" : request.op === "navigate" ? `the page did not come back after going to ${request.path}: is there a page at that path, and does it load the door's script?` : `the ${request.op} took the page to another address, and no page came back: does the page there load the door's script?`);
724
803
  const landed = request.op === "navigate" ? await this.statusAt(request.path) : void 0;
725
804
  return {
726
805
  ...answer,
@@ -735,7 +814,8 @@ var Door = class {
735
814
  const base = this.deps.app.url;
736
815
  if (base === void 0) return void 0;
737
816
  try {
738
- const response = await fetch(new URL(path, base), {
817
+ const target = underAppBase(this.deps.appBase ?? "", path);
818
+ const response = await fetch(new URL(target, base), {
739
819
  headers: { accept: "text/html" },
740
820
  signal: AbortSignal.timeout(3e3)
741
821
  });
@@ -800,12 +880,13 @@ var Door = class {
800
880
  }
801
881
  }
802
882
  /** `window` on look and act: the desktop window (the number `window list` prints) whose page the call is for; without it, the page used last. */
803
- windowOf(input) {
883
+ windowOf(input, scope = {}) {
804
884
  const { window, ...rest } = input;
805
885
  if (window === void 0) return {
806
886
  window: void 0,
807
887
  rest
808
888
  };
889
+ if (scope.page !== void 0) throw new DoorError("invalid", "this run drives only the page it was asked from; window does not apply");
809
890
  if (typeof window !== "number" || !Number.isSafeInteger(window) || window < 1) throw new DoorError("invalid", "window must be the number `window list` prints for the window");
810
891
  return {
811
892
  window,
@@ -820,12 +901,13 @@ var Door = class {
820
901
  }
821
902
  }
822
903
  /** Run one call on the page and record it. The audit holds which control was touched and how it ended, never what was typed. */
823
- async callPage(verb, request, signal, window) {
904
+ async callPage(verb, request, signal, window, page) {
824
905
  const approval = "token";
825
906
  try {
826
907
  const value = await this.deps.channel.call(request, {
827
908
  signal,
828
909
  ...window === void 0 ? {} : { window },
910
+ ...page === void 0 ? {} : { page },
829
911
  ...request.op === "wait" ? { timeoutMs: (request.timeoutMs ?? 3e3) + 5e3 } : {}
830
912
  });
831
913
  const target = "target" in value ? value.target : void 0;
@@ -862,7 +944,7 @@ var Door = class {
862
944
  }
863
945
  async plugin(run, mutates = true) {
864
946
  const extensions = this.deps.extensions();
865
- if (extensions === void 0) throw new DoorError("unavailable", "this app has no extensions plugin, so plugins cannot be installed while it runs");
947
+ if (extensions === void 0) throw new DoorError("unavailable", "this door has no extensions plugin to drive. In ACRYL's frame none is needed: a saved change in src/ or plugins/ reloads the app by itself, so do not restart it. Elsewhere, the app has no way to install plugins while it runs");
866
948
  const value = await run(extensions);
867
949
  const unchanged = typeof value === "object" && value !== null && value.action === "unchanged";
868
950
  if (mutates && !unchanged && this.deps.reloadOnChange !== false) await this.deps.channel.reloadAll();
@@ -871,6 +953,50 @@ var Door = class {
871
953
  text: JSON.stringify(value, null, 2)
872
954
  };
873
955
  }
956
+ share() {
957
+ const share = this.deps.pluginShare?.();
958
+ if (share === void 0) throw new DoorError("unavailable", "plugin-share is not running in this app. If the app just changed it is restarting, so try again in a moment. An app made by an older version does not have it, and upgrade does not add it: run `npx acrylblends add plugin-share` and follow its README");
959
+ return share;
960
+ }
961
+ /** Only a plan: no argument of any verb can publish, so an agent never says yes for a person. */
962
+ async planPublish(input) {
963
+ const { name, owner, license } = input;
964
+ if (typeof name !== "string" || name === "") throw new DoorError("invalid", "plugin publish needs a plugin name, e.g. plugin publish pomodoro");
965
+ const value = await this.share().planPublish(name, {
966
+ ...typeof owner === "string" && owner !== "" ? { owner } : {},
967
+ ...typeof license === "string" && license !== "" ? { license } : {}
968
+ });
969
+ if ("ok" in value) throw new DoorError("refused", `${value.reason}. ${value.next}.`);
970
+ return {
971
+ value,
972
+ text: `${renderPublishPlan(value)}\nNothing was sent. Only a person can say yes: they run \`npm run acryl -- plugin publish ${name}\` in their own terminal, or press Share in the frame. \`--license <id>\` changes the license before the yes.`
973
+ };
974
+ }
975
+ /** The person's yes, from their own terminal. Not a verb: MCP and the chat do not have it. */
976
+ async publishConfirmed(name, confirmed, options) {
977
+ const value = await this.share().publish(name, confirmed, options);
978
+ this.deps.audit.record({
979
+ at: (/* @__PURE__ */ new Date()).toISOString(),
980
+ tool: "plugin-publish",
981
+ target: {
982
+ role: "plugin",
983
+ name: `${confirmed.owner}/${confirmed.repo}@${confirmed.version}`
984
+ },
985
+ outcome: value.ok ? "ok" : "refused",
986
+ approval: "token"
987
+ });
988
+ return value;
989
+ }
990
+ async pull(source) {
991
+ if (typeof source !== "string" || source === "") throw new DoorError("invalid", "plugin pull needs a source, e.g. plugin pull github:someone/pomodoro@0.1.0");
992
+ const value = await this.share().pull(source);
993
+ if (!value.ok) throw new DoorError("refused", `${value.reason ?? "the pull was refused"}. ${value.next ?? "nothing changed"}.`);
994
+ if (value.action !== "unchanged" && this.deps.reloadOnChange !== false) await this.deps.channel.reloadAll();
995
+ return {
996
+ value,
997
+ text: JSON.stringify(value, null, 2)
998
+ };
999
+ }
874
1000
  /** A folder named by an agent, resolved from the app folder; it must be inside the app. */
875
1001
  insideApp(folder) {
876
1002
  if (typeof folder !== "string" || folder === "") throw new DoorError("invalid", "plugin add needs a folder, e.g. plugins/pomodoro");
@@ -914,8 +1040,8 @@ var Door = class {
914
1040
  * `door_<verb>`. A tool answers the verb's text, what an agent reads in a terminal; a failed verb throws with its code, as the operator
915
1041
  * client prints it.
916
1042
  */
917
- /** Register every verb. @returns the function that removes them all; it also aborts the calls still running. */
918
- function registerChatTools(tools, door) {
1043
+ /** Register the verbs (default: every one). @returns the function that removes them all; it also aborts the calls still running. */
1044
+ function registerChatTools(tools, door, verbs = VERBS) {
919
1045
  const running = new AbortController();
920
1046
  const removers = [];
921
1047
  const removeAll = () => {
@@ -923,12 +1049,13 @@ function registerChatTools(tools, door) {
923
1049
  for (const remove of removers.splice(0).reverse()) remove();
924
1050
  };
925
1051
  try {
926
- for (const verb of VERBS) removers.push(tools.register(chatToolName(verb), {
1052
+ for (const verb of verbs) removers.push(tools.register(chatToolName(verb), {
927
1053
  description: VERB_SPECS[verb].description,
928
1054
  parameters: VERB_SPECS[verb].arguments,
929
- run: async (input) => {
1055
+ run: async (input, options) => {
1056
+ const signal = options?.signal === void 0 ? running.signal : AbortSignal.any([running.signal, options.signal]);
930
1057
  try {
931
- return (await door.run(verb, input ?? {}, running.signal)).text;
1058
+ return (await door.run(verb, input ?? {}, signal, options?.page === void 0 ? {} : { page: options.page })).text;
932
1059
  } catch (cause) {
933
1060
  if (cause instanceof DoorError || cause instanceof UiControlError) throw new Error(`${cause.code}: ${cause.message}`, { cause });
934
1061
  throw cause;
@@ -997,10 +1124,14 @@ function alive(pid) {
997
1124
  * Write this run's connection file, replacing a stale one.
998
1125
  * @throws when another live process already serves this folder's door: two apps from one folder would race for one file.
999
1126
  */
1000
- function writeDoorFile(appRoot, url, secret) {
1001
- const dir = ensureStateDir(appRoot);
1127
+ /** @throws when another live process already serves this folder's door: two apps from one folder would race for one file. */
1128
+ function assertDoorIsFree(appRoot) {
1002
1129
  const existing = readDoorFile(appRoot);
1003
1130
  if (existing !== void 0 && existing.pid !== process.pid && alive(existing.pid)) throw new Error(`acryl-door: another process (pid ${String(existing.pid)}) serves the door of ${appRoot}; stop it first`);
1131
+ }
1132
+ function writeDoorFile(appRoot, url, secret) {
1133
+ const dir = ensureStateDir(appRoot);
1134
+ assertDoorIsFree(appRoot);
1004
1135
  const file = doorFilePath(appRoot);
1005
1136
  const temporary = join(dir, `${DOOR_FILE}.${String(process.pid)}.tmp`);
1006
1137
  const content = {
@@ -1157,6 +1288,33 @@ function operatorRoutes(prefix, secret, door, reportError) {
1157
1288
  reportError(`run the door verb ${verb}`, cause);
1158
1289
  return c.json(error("the verb could not be run"), 500);
1159
1290
  }
1291
+ }).post(`${prefix}/publish-confirmed`, operatorOnly(secret), jsonBody, async (c) => {
1292
+ const body = await readJson(c);
1293
+ const { name, confirm, owner, license } = body === INVALID_JSON || typeof body !== "object" || body === null ? {} : body;
1294
+ if (typeof name !== "string" || typeof confirm !== "object" || confirm === null) return c.json({
1295
+ ok: false,
1296
+ code: "invalid",
1297
+ message: "the body is { name, confirm }"
1298
+ }, 400);
1299
+ try {
1300
+ const value = await door.publishConfirmed(name, confirm, {
1301
+ ...typeof owner === "string" ? { owner } : {},
1302
+ ...typeof license === "string" ? { license } : {}
1303
+ });
1304
+ return c.json({
1305
+ ok: true,
1306
+ value,
1307
+ text: JSON.stringify(value, null, 2)
1308
+ });
1309
+ } catch (cause) {
1310
+ if (cause instanceof DoorError) return c.json({
1311
+ ok: false,
1312
+ code: cause.code,
1313
+ message: cause.message
1314
+ }, 409);
1315
+ reportError("publish a plugin", cause);
1316
+ return c.json(error("the plugin could not be published"), 500);
1317
+ }
1160
1318
  });
1161
1319
  }
1162
1320
  /** The upgrade request comes from this app's own page: a loopback peer on this port, and an `Origin` that is exactly this app's origin. Nothing else is read as proof. */
@@ -1171,11 +1329,13 @@ const ownOrigin = async (c, next) => {
1171
1329
  };
1172
1330
  function pageSocketRoutes(channel, options) {
1173
1331
  const heartbeatMs = options.heartbeatMs ?? 15e3;
1174
- return new Acryl().get(`${options.prefix}/channel/ws`, ownOrigin, options.upgrade((c) => {
1332
+ const events = (c) => {
1175
1333
  const windowId = c.req.query("window");
1176
1334
  const focused = c.req.query("focused") === "1";
1177
1335
  const shellQuery = Number(c.req.query("shell"));
1178
1336
  const shellWindow = Number.isSafeInteger(shellQuery) && shellQuery > 0 ? shellQuery : void 0;
1337
+ const tabQuery = c.req.query("tab");
1338
+ const tab = tabQuery !== void 0 && WINDOW_ID.test(tabQuery) ? tabQuery : void 0;
1179
1339
  let detach;
1180
1340
  let beat;
1181
1341
  return {
@@ -1199,7 +1359,7 @@ function pageSocketRoutes(channel, options) {
1199
1359
  close: () => {
1200
1360
  ws.close(1e3, "replaced");
1201
1361
  }
1202
- }, shellWindow);
1362
+ }, shellWindow, tab);
1203
1363
  write({ e: "ready" });
1204
1364
  beat = setInterval(() => {
1205
1365
  try {
@@ -1223,7 +1383,12 @@ function pageSocketRoutes(channel, options) {
1223
1383
  detach?.();
1224
1384
  }
1225
1385
  };
1226
- }, { textOnly: true }));
1386
+ };
1387
+ return new Acryl().get(`${options.prefix}/channel/ws`, ownOrigin, async (c, next) => {
1388
+ const allow = options.upgrade();
1389
+ if (allow === void 0) return c.json(error("the page channel is not ready yet"), 503);
1390
+ return allow(events, { textOnly: true })(c, next);
1391
+ });
1227
1392
  }
1228
1393
  /** The oldest Node the client runs on (the framework's own floor). */
1229
1394
  const MIN_NODE = [22, 19];
@@ -1343,14 +1508,13 @@ function writeShims(root, target) {
1343
1508
  */
1344
1509
  /** Stable Cordis plugin name, equal to the package name and the Loader row id. */
1345
1510
  const name = "acryl-door";
1346
- const inject = [
1347
- "router",
1348
- "appInstance",
1349
- "nodeServer"
1350
- ];
1511
+ const inject = ["router", "appInstance"];
1351
1512
  const Config = z.object({
1352
1513
  allowInProduction: z.boolean().default(false).description("Run in production too (NODE_ENV=production); ACRYL_DOOR=off still turns it off."),
1514
+ pageOnly: z.boolean().default(false).description("Only the page and the look and act chat tools (a production assistant); runs in production."),
1353
1515
  reloadOnChange: z.boolean().default(true).description("Reload the open pages after a plugin was added, removed or reloaded while the app runs."),
1516
+ hostIndex: z.boolean().default(true).description("Put the page script in the host's own index page too; off when that index is a frame around the app."),
1517
+ appBase: z.string().default("").description("The prefix the app pages are served under (the frame canvas), without a trailing slash; empty at the root."),
1354
1518
  tools: z.object({ expose: z.array(z.string().pattern(/^(?!door_)[A-Za-z][A-Za-z0-9_-]{0,63}$/u)).description("App tools (from the tools registry) the door offers; none by default.") })
1355
1519
  });
1356
1520
  /** The page module, served compiled from source by the app's own TypeScript. */
@@ -1382,26 +1546,53 @@ function notFoundPage(path, scriptTag) {
1382
1546
  }
1383
1547
  function apply(ctx, rawConfig = {}) {
1384
1548
  const config = Config(rawConfig);
1385
- if (process.env.ACRYL_DOOR === "off" || process.env.NODE_ENV === "production" && config.allowInProduction !== true) return;
1549
+ const pageOnly = config.pageOnly === true;
1550
+ if (process.env.ACRYL_DOOR === "off" || process.env.NODE_ENV === "production" && config.allowInProduction !== true && !pageOnly) return;
1386
1551
  const app = ctx.get("appInstance");
1387
- const server = ctx.get("nodeServer");
1552
+ const carrier = () => {
1553
+ const web = ctx.get("acrylWeb");
1554
+ if (web !== void 0) return web;
1555
+ const node = ctx.get("nodeServer");
1556
+ if (typeof node?.url !== "string") return void 0;
1557
+ try {
1558
+ const parsed = new URL(node.url);
1559
+ return {
1560
+ host: parsed.hostname,
1561
+ port: Number(parsed.port)
1562
+ };
1563
+ } catch {
1564
+ return;
1565
+ }
1566
+ };
1388
1567
  if (typeof app?.root !== "string") throw new Error("acryl-door: the appInstance service has no app folder (root)");
1389
1568
  const reportError = (operation, cause) => {
1390
1569
  ctx.logger?.error?.(`acryl-door: failed to ${operation}: ${cause instanceof Error ? cause.message : String(cause)}`);
1391
1570
  };
1571
+ const address = { url: "" };
1572
+ if (!pageOnly) assertDoorIsFree(app.root);
1392
1573
  const secret = newSecret();
1393
- const channel = new UiChannel({ attachWaitMs: 2e3 });
1574
+ const channel = new UiChannel({
1575
+ attachWaitMs: 2e3,
1576
+ requirePage: pageOnly
1577
+ });
1394
1578
  const shell = new ShellChannel();
1579
+ const audit = new AuditLog(auditPath(stateDir(app.root)));
1580
+ const rawBase = typeof config.appBase === "string" ? config.appBase.trim() : "";
1581
+ const appBase = rawBase.startsWith("/") && rawBase !== "/" ? rawBase.replace(/\/+$/u, "") : "";
1395
1582
  const door = new Door({
1396
1583
  channel,
1397
1584
  shell,
1398
- audit: new AuditLog(auditPath(stateDir(app.root))),
1585
+ audit,
1399
1586
  app: {
1400
1587
  name: app.name,
1401
1588
  root: app.root,
1402
- url: loopbackUrl(server.url)
1589
+ get url() {
1590
+ return address.url;
1591
+ }
1403
1592
  },
1593
+ ...appBase === "" ? {} : { appBase },
1404
1594
  extensions: () => ctx.get("extensions"),
1595
+ pluginShare: () => ctx.get("pluginShare"),
1405
1596
  tools: () => ctx.get("tools"),
1406
1597
  expose: config.tools?.expose ?? [],
1407
1598
  reloadOnChange: config.reloadOnChange !== false
@@ -1412,8 +1603,12 @@ function apply(ctx, rawConfig = {}) {
1412
1603
  ctx.effect(() => () => {
1413
1604
  shell.close();
1414
1605
  }, "acryl-door: desktop shell channel");
1415
- const pageModule = new Acryl().get(`${DOOR_PREFIX}/page.js`, (c) => c.body(`import '${DOOR_PREFIX}/page/main.ts'\n`, 200, { "content-type": "text/javascript; charset=utf-8" })).route(`${DOOR_PREFIX}/page`, clientModules({ root: PAGE_DIR }));
1606
+ const pageModule = new Acryl().get(`${DOOR_PREFIX}/page.js`, (c) => c.body(`import '${DOOR_PREFIX}/page/main.ts'\n`, 200, { "content-type": "text/javascript; charset=utf-8" })).route(`${DOOR_PREFIX}/page`, clientModules({
1607
+ root: PAGE_DIR,
1608
+ mount: `${DOOR_PREFIX}/page`
1609
+ }));
1416
1610
  const tag = `<script type="module" src="${DOOR_PREFIX}/page.js"><\/script>`;
1611
+ const meta = appBase === "" ? "" : `<meta name="acryl-app-base" content="${appBase}">`;
1417
1612
  const addScript = async (c, next) => {
1418
1613
  await next();
1419
1614
  if (!(c.res.headers.get("content-type") ?? "").includes("text/html")) {
@@ -1423,11 +1618,12 @@ function apply(ctx, rawConfig = {}) {
1423
1618
  });
1424
1619
  return;
1425
1620
  }
1426
- const html = await c.res.text();
1621
+ let html = await c.res.text();
1427
1622
  const headers = new Headers(c.res.headers);
1428
1623
  headers.delete("content-length");
1429
- const carries = html.includes(`${DOOR_PREFIX}/page.js`);
1430
- c.res = new Response(carries ? html : html.includes("</body>") ? html.replace("</body>", `${tag}</body>`) : html + tag, {
1624
+ if (meta !== "" && !html.includes("name=\"acryl-app-base\"")) html = html.includes("</head>") ? html.replace("</head>", `${meta}</head>`) : meta + html;
1625
+ if (!html.includes(`/api/acryl-door/page.js`)) html = html.includes("</body>") ? html.replace("</body>", `${tag}</body>`) : html + tag;
1626
+ c.res = new Response(html, {
1431
1627
  status: c.res.status,
1432
1628
  headers
1433
1629
  });
@@ -1435,31 +1631,99 @@ function apply(ctx, rawConfig = {}) {
1435
1631
  const router = ctx.router;
1436
1632
  if (typeof router.wrap === "function") router.wrap(addScript);
1437
1633
  else ctx.router.route("/", new Acryl().use("*", addScript));
1438
- let upgrade;
1439
- try {
1440
- upgrade = ctx.nodeServer?.upgrades?.();
1441
- } catch (cause) {
1442
- reportError("open the page channel over a WebSocket (the pages use the event stream)", cause);
1443
- }
1444
- ctx.router.route("/", privateApi(DOOR_PREFIX, pageModule, pageChannelRoutes(channel, { prefix: DOOR_PREFIX }), ...upgrade === void 0 ? [] : [pageSocketRoutes(channel, {
1634
+ if (config.hostIndex !== false) ctx.inject(["webServer"], (inner) => {
1635
+ inner.effect(() => {
1636
+ const taps = inner.get("webServer");
1637
+ if (typeof taps?.tapIndex !== "function") return () => {};
1638
+ return taps.tapIndex((html) => {
1639
+ if (html.includes(`/api/acryl-door/page.js`)) return html;
1640
+ return html.includes("</body>") ? html.replace("</body>", `${tag}</body>`) : html + tag;
1641
+ });
1642
+ }, "acryl-door: the page script in the host index");
1643
+ });
1644
+ let pageUpgrade;
1645
+ let nodeUpgrade;
1646
+ const endNodeOwner = () => {
1647
+ if (pageUpgrade === nodeUpgrade) pageUpgrade = void 0;
1648
+ nodeUpgrade = void 0;
1649
+ };
1650
+ const nodeFallback = () => {
1651
+ if (ctx.get("routerHttp") !== void 0 || nodeUpgrade !== void 0) return;
1652
+ try {
1653
+ const allow = ctx.get("nodeServer")?.upgrades?.();
1654
+ if (allow !== void 0) {
1655
+ nodeUpgrade = allow;
1656
+ pageUpgrade = allow;
1657
+ }
1658
+ } catch (cause) {
1659
+ reportError("open the page channel over a WebSocket (the pages use the event stream)", cause);
1660
+ }
1661
+ };
1662
+ ctx.inject(["routerHttp"], (inner) => {
1663
+ endNodeOwner();
1664
+ const http = inner.get("routerHttp");
1665
+ try {
1666
+ const allow = http?.upgrades?.();
1667
+ if (allow === void 0 || http === void 0 || typeof http.mount !== "function") return;
1668
+ pageUpgrade = allow;
1669
+ const remove = http.mount(DOOR_PREFIX, { upgrade: [`${DOOR_PREFIX}/channel/ws`] });
1670
+ inner.effect(() => () => {
1671
+ remove();
1672
+ if (pageUpgrade === allow) pageUpgrade = void 0;
1673
+ nodeFallback();
1674
+ }, "acryl-door: the page socket via routerHttp");
1675
+ } catch (cause) {
1676
+ reportError("open the page channel over a WebSocket (the pages use the event stream)", cause);
1677
+ }
1678
+ });
1679
+ ctx.inject(["nodeServer"], (inner) => {
1680
+ inner.effect(() => {
1681
+ nodeFallback();
1682
+ const onService = inner.on;
1683
+ const off = onService("internal/service", () => {
1684
+ if (ctx.get("routerHttp") !== void 0) endNodeOwner();
1685
+ else nodeFallback();
1686
+ });
1687
+ return () => {
1688
+ off();
1689
+ endNodeOwner();
1690
+ };
1691
+ }, "acryl-door: the page socket on the node server");
1692
+ });
1693
+ ctx.router.route("/", pageSocketRoutes(channel, {
1445
1694
  prefix: DOOR_PREFIX,
1446
- upgrade
1447
- })], shellRoutes(DOOR_PREFIX, secret, shell), operatorRoutes(DOOR_PREFIX, secret, door, reportError), mcpRoutes(DOOR_PREFIX, secret, door, reportError)));
1695
+ upgrade: () => pageUpgrade
1696
+ }));
1697
+ ctx.router.route("/", privateApi(DOOR_PREFIX, pageModule, pageChannelRoutes(channel, { prefix: DOOR_PREFIX }), ...pageOnly ? [] : [
1698
+ shellRoutes(DOOR_PREFIX, secret, shell),
1699
+ operatorRoutes(DOOR_PREFIX, secret, door, reportError),
1700
+ mcpRoutes(DOOR_PREFIX, secret, door, reportError)
1701
+ ]));
1448
1702
  ctx.inject(["tools"], (inner) => {
1449
- inner.effect(() => registerChatTools(inner.get("tools"), door), "acryl-door: the verbs in the tools registry");
1703
+ inner.effect(() => registerChatTools(inner.get("tools"), door, pageOnly ? PAGE_VERBS : VERBS), "acryl-door: the verbs in the tools registry");
1450
1704
  });
1451
1705
  ctx.effect(function* () {
1452
1706
  yield ctx.reflect.provide("door", door);
1453
- writeDoorFile(app.root, loopbackUrl(server.url), secret);
1707
+ }, "acryl-door: the door service");
1708
+ if (pageOnly) return;
1709
+ ctx.effect(async () => {
1710
+ let bound = carrier();
1711
+ for (let at = 0; at < 500 && (bound === void 0 || !(bound.port > 0)); at += 1) {
1712
+ await new Promise((done) => setTimeout(done, 10));
1713
+ bound = carrier();
1714
+ }
1715
+ if (bound === void 0) throw new Error("acryl-door: no carrier bound the app (neither acrylWeb nor nodeServer answered)");
1716
+ address.url = loopbackUrl(`http://${bound.host}:${String(bound.port)}`);
1717
+ writeDoorFile(app.root, address.url, secret);
1454
1718
  try {
1455
1719
  writeShims(app.root, shimTarget());
1456
1720
  } catch (cause) {
1457
1721
  reportError("write .acryl/bin", cause);
1458
1722
  }
1459
- yield () => {
1723
+ return () => {
1460
1724
  removeDoorFile(app.root, secret);
1461
1725
  };
1462
- }, "acryl-door: the door service, .acryl/door.json and .acryl/bin/acryl");
1726
+ }, "acryl-door: .acryl/door.json and .acryl/bin/acryl");
1463
1727
  }
1464
1728
  //#endregion
1465
- export { Config, DOOR_PREFIX, Door, DoorError, STATE_DIR, VERBS, VERB_SPECS, apply, chatToolName, doorFilePath, inject, mcpTools, name, notFoundPage, readDoorFile };
1729
+ export { Config, DOOR_PREFIX, Door, DoorError, PAGE_VERBS, STATE_DIR, VERBS, VERB_SPECS, apply, assertDoorIsFree, chatToolName, doorFilePath, inject, mcpTools, name, notFoundPage, readDoorFile };
@@ -1,6 +1,6 @@
1
1
  import { t as Acryl } from "../app-CQcGWjrk.js";
2
2
  import { i as loopbackOnly } from "../loopback-only-BbasoF7R.js";
3
- import "../src-BTBm4OPD.js";
3
+ import "../src-iBJ2ysV9.js";
4
4
  import "../middleware-BfG_fp8P.js";
5
5
  import { Service } from "@deepseek-ai/cordis";
6
6
  //#region ../../plugins/acryl-inspect/src/report.ts