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
@@ -6,22 +6,25 @@ import * as door from '../plugins/acryl-door/src/index.ts'
6
6
  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
- import { agent, memory, model, tools } from '../plugins/agent/index.ts'
9
+ import { agent, memory, model, scripted, tools } from '../plugins/agent/index.ts'
10
10
  import * as panel from '../plugins/agent-panel/index.ts'
11
11
  import { builder } from '../plugins/builder/index.ts'
12
12
  import { extensions } from '../plugins/extensions/index.ts'
13
13
  import { type AppInstance, instance } from '../plugins/instance/index.ts'
14
14
  import {
15
+ appPluginNames,
15
16
  blueprintsIn,
16
17
  compose,
18
+ loadAppPlugins,
17
19
  loadManifest,
18
20
  type ParameterValue,
19
21
  } from '../plugins/manifest/index.ts'
20
- import { type OpenAIConfig, openai } from '../plugins/openai/index.ts'
22
+ import { ASSISTANT_PROMPT, type OpenAIConfig, openai } from '../plugins/openai/index.ts'
23
+ import { pluginShare } from '../plugins/plugin-share/index.ts'
21
24
  import { sandboxPlugin } from '../plugins/sandbox/index.ts'
22
25
  import { save } from '../plugins/save/index.ts'
23
26
  import { tasks } from '../plugins/tasks/index.ts'
24
- import { routes } from './routes.ts'
27
+ import { FRAME_MOUNT_PREFIX, routes } from './routes.ts'
25
28
 
26
29
  /** The plugins this app's code provides. `blend.yaml` chooses, orders and configures them. */
27
30
  export const registry = {
@@ -34,6 +37,7 @@ export const registry = {
34
37
  'acryl-state': state,
35
38
  extensions,
36
39
  builder,
40
+ 'plugin-share': pluginShare,
37
41
  save,
38
42
  sandbox: sandboxPlugin,
39
43
  routes,
@@ -44,6 +48,8 @@ export const registry = {
44
48
 
45
49
  /** The manifest next to this app's source. */
46
50
  export const MANIFEST = join(fileURLToPath(new URL('..', import.meta.url)), 'blend.yaml')
51
+ /** The app's own plugins (a feature each), which `blend.yaml` mounts by their folder name next to the library copies `registry` imports. */
52
+ const PLUGINS_DIR = join(fileURLToPath(new URL('..', import.meta.url)), 'plugins')
47
53
 
48
54
  /**
49
55
  * The app's manifest, resolved. A Blend finds its Blueprint in `blueprints/<id>.yaml`; an app that is itself a Blueprint never looks.
@@ -51,7 +57,7 @@ export const MANIFEST = join(fileURLToPath(new URL('..', import.meta.url)), 'ble
51
57
  */
52
58
  export function readManifest(values?: Readonly<Record<string, ParameterValue>>) {
53
59
  return loadManifest(MANIFEST, {
54
- known: Object.keys(registry),
60
+ known: [...Object.keys(registry), ...appPluginNames(PLUGINS_DIR)],
55
61
  getBlueprint: blueprintsIn(join(fileURLToPath(new URL('..', import.meta.url)), 'blueprints')),
56
62
  ...(values ? { values } : {}),
57
63
  })
@@ -73,32 +79,66 @@ export interface App {
73
79
  const DEVELOPMENT_ROWS: Record<string, string> = {
74
80
  agent: 'POST /ask runs the agent, which spends the model key and calls its tools',
75
81
  builder: 'the agent can write plugin source into plugins/',
82
+ 'plugin-share': 'a plugin pulled from a release runs in this process',
76
83
  extensions:
77
84
  'a plugin installed live runs in this process, and POST /extensions/approve decides installs',
78
85
  }
79
86
 
87
+ /** The only tools the production Assistant's agent may call: look at the page and act on it. */
88
+ const ASSISTANT_TOOLS = ['door_look', 'door_act']
89
+
90
+ /** The most tokens one answer of the Assistant's model may use: with the step budget, what one visitor's question can cost at most. */
91
+ const ASSISTANT_MAX_TOKENS = 1024
92
+
93
+ /** Model calls per visitor question: look, open a page, look, filter, look and answer is six, with room for one wrong turn. */
94
+ const ASSISTANT_STEPS = 10
95
+
96
+ /** What the Assistant says while the app has no model: the scripted one cannot use the page. */
97
+ export const ASSISTANT_NO_MODEL =
98
+ "This assistant is not set up yet: the app's owner has not chosen a model for it."
99
+
80
100
  /**
81
- * The app: `instance` first (where it lives, chosen at run time), then the manifest's rows in order. The app's `plugins/`
82
- * folder is trusted to install at start, and is where the builder writes.
101
+ * The app's rows in the manifest's order, composed into `ctx`: the app's own server calls this from `createApp`, and the generated frame layer mounts it
102
+ * as one row, so the frame runs the app exactly as the app's own server does (same rows, same order, same config). `host: true` leaves the server and the
103
+ * door to the frame host, which mounts its own.
83
104
  */
84
- export async function createApp(
105
+ export async function composeInto(
106
+ ctx: Context,
85
107
  chosen: AppInstance,
86
108
  options: {
87
109
  values?: Readonly<Record<string, ParameterValue>>
88
- /** A real model (any OpenAI-compatible API) in place of the offline scripted one. */
89
110
  model?: OpenAIConfig
90
- /** Start the HTTP server (default). Tests pass `false` and call `app.root.router.request(path)`: it needs no port, so it also runs where a sandbox forbids listening. */
91
111
  listen?: boolean
112
+ host?: boolean
92
113
  } = {},
93
- ): Promise<App> {
114
+ ): Promise<Fiber[]> {
94
115
  const read = readManifest(options.values)
95
116
  // Rows left out: the server (and so the door, which needs it) when the app is reached in process; the door in production and with ACRYL_DOOR=off (one process at a time
96
117
  // serves a folder's door, so the tests that start several apps set it).
97
- const listen = options.listen !== false
118
+ const listen = options.listen !== false && options.host !== true
98
119
  const leaveOut = new Set<string>()
99
120
  let extensionsReadOnly = false
100
- if (!listen) leaveOut.add('acryl-node-server').add('acryl-door')
101
- if (process.env.NODE_ENV === 'production' || process.env.ACRYL_DOOR === 'off')
121
+ // In the frame host the infrastructure rows are the host's own (its server, its web-server port, its router, the app instance, the door): the app
122
+ // contributes only the plugins that are its own.
123
+ if (!listen || options.host === true) leaveOut.add('acryl-node-server').add('acryl-door')
124
+ if (options.host === true)
125
+ for (const row of [
126
+ 'instance',
127
+ 'acryl-router',
128
+ 'acryl-web-server',
129
+ 'acryl-page',
130
+ 'manifest',
131
+ 'save',
132
+ ])
133
+ leaveOut.add(row)
134
+ // The Assistant (production, ACRYL_ASSISTANT=on): the agent panel docked for the app's visitors. Its agent keeps only the page tools and the door only the page,
135
+ // so it uses the app for a visitor (opens pages, filters, fills in forms) and cannot change the app, its code or its secrets. Not in the frame host.
136
+ const assistant =
137
+ process.env.NODE_ENV === 'production' &&
138
+ process.env.ACRYL_ASSISTANT === 'on' &&
139
+ process.env.ACRYL_DOOR !== 'off' &&
140
+ listen
141
+ if (!assistant && (process.env.NODE_ENV === 'production' || process.env.ACRYL_DOOR === 'off'))
102
142
  leaveOut.add('acryl-door')
103
143
  // Production: the rows that give a visitor power over the running app or the owner's model key are left out too. The agent (`POST /ask` spends the model key and runs tools),
104
144
  // the builder and the extensions (live plugin install runs code in this process, and `POST /extensions/approve` decides it) are development tools. `ACRYL_PRODUCTION_KEEP`
@@ -121,7 +161,7 @@ export async function createApp(
121
161
  `WARNING: the "${row}" row runs in production because ACRYL_PRODUCTION_KEEP names it: ${danger}. Nothing here is behind a login; put the app behind your own.`,
122
162
  )
123
163
  else if (row === 'extensions') extensionsReadOnly = true
124
- else leaveOut.add(row)
164
+ else if (!(assistant && row === 'agent')) leaveOut.add(row)
125
165
  }
126
166
  }
127
167
  const manifest = {
@@ -136,31 +176,112 @@ export async function createApp(
136
176
  console.warn(
137
177
  'warning: extensions/ is the old folder for plugins the agent writes; move them to plugins/ ("git mv extensions/<name> plugins/<name>"). extensions/ is read for this release only.',
138
178
  )
139
- const root = new Context()
140
- const first = root.plugin(instance, chosen)
141
- await first.await()
179
+ // The Assistant's model is told it helps a visitor through the page alone, and each answer is capped, unless the app's model configuration says otherwise.
180
+ const modelConfig =
181
+ options.model && assistant
182
+ ? {
183
+ ...options.model,
184
+ system: options.model.system ?? ASSISTANT_PROMPT,
185
+ maxTokens: options.model.maxTokens ?? ASSISTANT_MAX_TOKENS,
186
+ }
187
+ : options.model
188
+ if (assistant && !options.model)
189
+ console.warn(
190
+ `WARNING: the Assistant has no model, so it answers every visitor "${ASSISTANT_NO_MODEL}" Set the app's model (ACRYLBLENDS_MODEL_URL and ACRYLBLENDS_MODEL, or model.json in the app's home).`,
191
+ )
192
+ // The rows that name one of the app's own plugin folders (a feature: plugins/<name>/ and its row) rather than a registry import. Each one that runs
193
+ // is imported here; all of them, off or on, belong to their row, so the extensions row never starts them a second time.
194
+ const ownRows = [
195
+ ...new Set(read.plugins.map((row) => row.use).filter((use) => !Object.hasOwn(registry, use))),
196
+ ]
197
+ const ownPlugins = await loadAppPlugins(
198
+ PLUGINS_DIR,
199
+ ownRows.filter((use) => manifest.plugins.some((row) => row.use === use && !row.disabled)),
200
+ )
142
201
  const fibers: Fiber[] = [
143
- first,
144
202
  // The `model` row mounts the scripted model, or the real one when a model is configured.
145
- ...(await compose(root, manifest, options.model ? { ...registry, model: openai } : registry, {
146
- ...(options.model ? { model: { ...options.model } } : {}),
147
- // Runtime values the manifest cannot hold: the manifest's own digest (every task records it), and who may install.
148
- tasks: { manifest: `sha256:${manifest.digest}` },
149
- // The `approval` parameter in blend.yaml (the extensions row) turns this source into one that asks.
150
- extensions: {
151
- ...(extensionsReadOnly ? { readOnly: true } : {}),
152
- sources: [
153
- { dir: extensionsDir, trust: 'install' },
154
- ...(legacy ? [{ dir: legacyDir, trust: 'install' as const }] : []),
155
- ],
203
+ ...(await compose(
204
+ ctx,
205
+ manifest,
206
+ { ...registry, ...ownPlugins, ...(modelConfig ? { model: openai } : {}) },
207
+ {
208
+ ...(modelConfig ? { model: { ...modelConfig } } : {}),
209
+ // Runtime values the manifest cannot hold: the manifest's own digest (every task records it), and who may install.
210
+ tasks: { manifest: `sha256:${manifest.digest}` },
211
+ // The `approval` parameter in blend.yaml (the extensions row) turns this source into one that asks.
212
+ extensions: {
213
+ ...(extensionsReadOnly ? { readOnly: true } : {}),
214
+ sources: [
215
+ { dir: extensionsDir, trust: 'install' },
216
+ ...(legacy ? [{ dir: legacyDir, trust: 'install' as const }] : []),
217
+ ],
218
+ rows: ownRows,
219
+ },
220
+ builder: { dir: extensionsDir },
221
+ 'plugin-share': { dir: extensionsDir },
222
+ ...(assistant
223
+ ? {
224
+ // Every answer comes right after a look, so it says what the visitor's page shows now.
225
+ agent: { tools: ASSISTANT_TOOLS, steps: ASSISTANT_STEPS, answerAfter: 'door_look' },
226
+ 'acryl-door': { pageOnly: true },
227
+ 'agent-panel': { assistant: true },
228
+ // The visitor reaches the agent through the panel only: no /ask, /tasks or /extensions routes.
229
+ routes: { agentRoutes: false },
230
+ }
231
+ : {}),
232
+ // The rows that serve routes of their own mount them under the app's prefix, as makeApp does. On the app's own server that prefix is the root (the
233
+ // row's default); the frame host serves the app at FRAME_MOUNT_PREFIX, and its own `/api` routes would answer for a row left at the root. The
234
+ // app's own routes row takes the same prefix for its mount and for the absolute URLs its pages write, so the page and its assets answer at `/app`.
235
+ ...(options.host === true
236
+ ? {
237
+ 'agent-panel': { mount: FRAME_MOUNT_PREFIX },
238
+ 'driver-pty': { mount: FRAME_MOUNT_PREFIX },
239
+ feedback: { mount: FRAME_MOUNT_PREFIX },
240
+ routes: { prefix: FRAME_MOUNT_PREFIX, ...(assistant ? { agentRoutes: false } : {}) },
241
+ }
242
+ : {}),
243
+ // The frame host runs the server itself and its app instance carries no port; only the app's own server configures one.
244
+ ...(options.host === true
245
+ ? {}
246
+ : {
247
+ server: {
248
+ port: chosen.port.start,
249
+ scan: chosen.port.scan ? 20 : 0,
250
+ hostname: chosen.host,
251
+ },
252
+ }),
156
253
  },
157
- builder: { dir: extensionsDir },
158
- server: { port: chosen.port.start, scan: chosen.port.scan ? 20 : 0, hostname: chosen.host },
159
- })),
254
+ )),
160
255
  ]
256
+ // Without a model the scripted one would echo the visitor's words back: it says in one sentence that the Assistant is not set up.
257
+ if (assistant && !options.model)
258
+ scripted(ctx.get('model') as Parameters<typeof scripted>[0])?.teach(() => ({
259
+ text: ASSISTANT_NO_MODEL,
260
+ }))
261
+ return fibers
262
+ }
263
+
264
+ /**
265
+ * The app: `instance` first (where it lives, chosen at run time), then the manifest's rows in order. The app's `plugins/`
266
+ * folder is trusted to install at start, and is where the builder writes.
267
+ */
268
+ export async function createApp(
269
+ chosen: AppInstance,
270
+ options: {
271
+ values?: Readonly<Record<string, ParameterValue>>
272
+ /** A real model (any OpenAI-compatible API) in place of the offline scripted one. */
273
+ model?: OpenAIConfig
274
+ /** Start the HTTP server (default). Tests pass `false` and call `app.root.router.request(path)`: it needs no port, so it also runs where a sandbox forbids listening. */
275
+ listen?: boolean
276
+ } = {},
277
+ ): Promise<App> {
278
+ const root = new Context()
279
+ const first = root.plugin(instance, chosen)
280
+ await first.await()
281
+ const fibers: Fiber[] = [first, ...(await composeInto(root, chosen, options))]
161
282
  return {
162
283
  root,
163
- url: listen ? root.nodeServer.url : '',
284
+ url: options.listen === false ? '' : root.nodeServer.url,
164
285
  async stop() {
165
286
  for (const fiber of [...fibers].reverse()) await fiber.dispose()
166
287
  },
@@ -0,0 +1,17 @@
1
+ /**
2
+ * The app as the ACRYL frame hosts it (`npm run dev`): the generated layer mounts this row, which claims {@link FRAME_MOUNT_PREFIX} on the frame's web
3
+ * server. The app's own rows (the generated `app-plugins` row, which calls `composeInto`) mount the app at that prefix, so its pages and its routes
4
+ * answer at `/app`. Nothing here runs in production: `npm start` composes the app on its own server (no `routerHttp` service, so this row is not
5
+ * mounted there), where the routes are at the root as they always were.
6
+ */
7
+ import type { Context } from '@deepseek-ai/cordis'
8
+ import { FRAME_MOUNT_PREFIX } from './routes.ts'
9
+
10
+ export const name = 'app-routes'
11
+
12
+ /** The frame always has it: its layer carries the web adapter. */
13
+ export const inject = ['routerHttp']
14
+
15
+ export function apply(ctx: Context): void {
16
+ ctx.routerHttp.mount(FRAME_MOUNT_PREFIX)
17
+ }
@@ -1,16 +1,18 @@
1
+ import { join } from 'node:path'
1
2
  import { fileURLToPath } from 'node:url'
2
3
  import { selectInstance } from '../plugins/instance/index.ts'
3
- import { modelConfigFrom } from '../plugins/openai/index.ts'
4
+ import { MODEL_FILE, modelConfigFrom, modelConfigFromFile } from '../plugins/openai/index.ts'
4
5
  import { createApp } from './app.ts'
5
6
  import { APP_NAME } from './routes.ts'
6
7
 
7
8
  // The composition root: the one place that reads the environment. selectInstance chooses where the app lives;
8
- // modelConfigFrom picks a real model when ACRYLBLENDS_MODEL_URL and ACRYLBLENDS_MODEL are set (ACRYLBLENDS_MODEL_KEY for hosted APIs).
9
+ // modelConfigFrom picks a real model when ACRYLBLENDS_MODEL_URL and ACRYLBLENDS_MODEL are set (ACRYLBLENDS_MODEL_KEY for hosted APIs); without them, the model kept in the app's
10
+ // home (model.json, readable by its owner only) is used, so a started app needs no flag.
9
11
  const chosen = selectInstance({
10
12
  projectRoot: fileURLToPath(new URL('..', import.meta.url)),
11
13
  env: process.env,
12
14
  })
13
- const model = modelConfigFrom(process.env)
15
+ const model = modelConfigFrom(process.env) ?? modelConfigFromFile(join(chosen.home, MODEL_FILE))
14
16
  const app = await createApp(chosen, model ? { model } : {})
15
17
  console.log(
16
18
  `${APP_NAME} listening on ${app.url} (data in ${chosen.home}; model: ${model ? `${model.model} at ${model.baseUrl}` : 'offline scripted'})`,
@@ -7,6 +7,9 @@ import type {} from '../plugins/tasks/index.ts'
7
7
 
8
8
  export const APP_NAME = '__APP_NAME__'
9
9
 
10
+ /** The only place the app's mount prefix is named: the root on its own server (`npm start`), `/app` when the ACRYL frame hosts it (`npm run dev`). */
11
+ export const FRAME_MOUNT_PREFIX = '/app'
12
+
10
13
  /** The door's page script (`.acryl/bin/acryl look` and `act`), in development only: `src/app.ts` leaves the door out in production and with ACRYL_DOOR=off. */
11
14
  const DOOR_TAG =
12
15
  process.env.NODE_ENV !== 'production' && process.env.ACRYL_DOOR !== 'off'
@@ -50,43 +53,59 @@ const APPEARANCE_JS = `(function () {
50
53
  /** The home and settings pages' own layout; their colours come from /appearance.js. */
51
54
  const APPEARANCE_STYLE =
52
55
  '<style>body{font:16px system-ui,sans-serif;margin:2rem auto;max-width:42rem;padding:0 1rem}a{color:inherit}</style>'
53
- const APPEARANCE_SCRIPT = '<script src="/appearance.js"></script>'
56
+ const appearanceScript = (prefix: string) => `<script src="${prefix}/appearance.js"></script>`
54
57
 
55
58
  /** `/settings`: the one setting the template has, the appearance. The desktop app's Settings menu item opens this page. */
56
- const settingsPage = `<!doctype html><html lang="en"><head><meta charset="utf-8"><title>Settings - ${APP_NAME}</title>${APPEARANCE_STYLE}${APPEARANCE_SCRIPT}</head><body><h1>Settings</h1><p><label for="theme">Appearance</label> <select id="theme" name="theme"><option value="system">Match the system</option><option value="light">Light</option><option value="dark">Dark</option></select></p><p id="saved" role="status"></p><p><a href="/">Back to ${APP_NAME}</a></p><script>(function(){var select=document.getElementById('theme'),saved=document.getElementById('saved');try{select.value=localStorage.getItem('theme')||'system'}catch(e){}select.addEventListener('change',function(){var value=select.value;try{if(value==='system'){localStorage.removeItem('theme');delete document.documentElement.dataset.theme}else{localStorage.setItem('theme',value);document.documentElement.dataset.theme=value}saved.textContent='Saved: '+select.options[select.selectedIndex].text}catch(e){saved.textContent='This browser would not keep the setting.'}})})()</script></body></html>`
59
+ const settingsPage = (prefix: string) =>
60
+ `<!doctype html><html lang="en"><head><meta charset="utf-8"><title>Settings - ${APP_NAME}</title>${APPEARANCE_STYLE}${appearanceScript(prefix)}</head><body><h1>Settings</h1><p><label for="theme">Appearance</label> <select id="theme" name="theme"><option value="system">Match the system</option><option value="light">Light</option><option value="dark">Dark</option></select></p><p id="saved" role="status"></p><p><a href="${prefix}/">Back to ${APP_NAME}</a></p><script>(function(){var select=document.getElementById('theme'),saved=document.getElementById('saved');try{select.value=localStorage.getItem('theme')||'system'}catch(e){}select.addEventListener('change',function(){var value=select.value;try{if(value==='system'){localStorage.removeItem('theme');delete document.documentElement.dataset.theme}else{localStorage.setItem('theme',value);document.documentElement.dataset.theme=value}saved.textContent='Saved: '+select.options[select.selectedIndex].text}catch(e){saved.textContent='This browser would not keep the setting.'}})})()</script></body></html>`
57
61
 
58
- export const routes = {
59
- name: 'routes',
60
- inject: ['router'],
61
- apply(ctx: Context) {
62
- // A body here is a name, an id or a question; anything bigger is refused before it is read.
63
- const json = async (c: {
64
- req: { json(): Promise<unknown> }
65
- }): Promise<Record<string, unknown>> => {
66
- const body = await c.req.json().catch(() => undefined)
67
- return typeof body === 'object' && body !== null ? (body as Record<string, unknown>) : {}
68
- }
69
- const limit = bodyLimit({ maxSize: 64 * 1024 })
70
- const base = new Acryl()
62
+ /**
63
+ * The app's own pages and routes, mountable at any prefix: the standalone server mounts it at the root, the frame row at {@link FRAME_MOUNT_PREFIX}.
64
+ * Every absolute URL a page writes is derived from the prefix it is mounted at.
65
+ */
66
+ export function makeApp(prefix = ''): Acryl {
67
+ const limit = bodyLimit({ maxSize: 64 * 1024 })
68
+ return (
69
+ new Acryl()
71
70
  .use(limit)
72
71
  // A browser gets a page (the door's and the agent panel's script tags are added where they are mounted); everything else gets the JSON.
73
72
  .get('/', (c) =>
74
73
  (c.req.header('accept') ?? '').includes('text/html')
75
74
  ? c.html(
76
- `<!doctype html><html lang="en"><head><meta charset="utf-8"><title>${APP_NAME}</title>${APPEARANCE_STYLE}${APPEARANCE_SCRIPT}</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>`,
75
+ `<!doctype html><html lang="en"><head><meta charset="utf-8"><title>${APP_NAME}</title>${APPEARANCE_STYLE}${appearanceScript(prefix)}</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). In ACRYL, ask it for a change in the chat beside the app (or with <code>POST /ask</code>); published with the assistant on, it helps visitors use the app without changing it.</p>${DOOR_TAG}</body></html>`,
77
76
  )
78
77
  : c.json({ app: APP_NAME, ok: true }),
79
78
  )
80
79
  .get('/health', (c) => c.json({ ok: true }))
81
- .get('/settings', (c) => c.html(settingsPage))
80
+ .get('/settings', (c) => c.html(settingsPage(prefix)))
82
81
  .get('/appearance.js', (c) =>
83
82
  c.body(APPEARANCE_JS, 200, {
84
83
  'content-type': 'text/javascript; charset=utf-8',
85
84
  'cache-control': 'no-cache',
86
85
  }),
87
86
  )
88
- ctx.router.route('/', base)
87
+ )
88
+ }
89
+
90
+ /** The app's own routes: an `App` mounted on the router at `options.prefix` (empty or absent: the root). */
91
+ export const routes = {
92
+ name: 'routes',
93
+ inject: ['router'],
94
+ apply(ctx: Context, options: { readonly prefix?: string; readonly agentRoutes?: boolean } = {}) {
95
+ const prefix = options.prefix ?? ''
96
+ // A body here is a name, an id or a question; anything bigger is refused before it is read.
97
+ const json = async (c: {
98
+ req: { json(): Promise<unknown> }
99
+ }): Promise<Record<string, unknown>> => {
100
+ const body = await c.req.json().catch(() => undefined)
101
+ return typeof body === 'object' && body !== null ? (body as Record<string, unknown>) : {}
102
+ }
103
+ const limit = bodyLimit({ maxSize: 64 * 1024 })
104
+ // The app's own page and routes at the one prefix; the agent routes below mount at the same prefix.
105
+ ctx.router.route(prefix === '' ? '/' : prefix, makeApp(prefix))
89
106
  // The routes that give a visitor the agent: mounted only while the agent, the extensions and the tasks run (see the header).
107
+ // `agentRoutes: false` (the production Assistant) keeps them unmounted although the agent runs.
108
+ if (options.agentRoutes === false) return
90
109
  ctx.inject(['agent', 'extensions', 'tasks'], (inner) => {
91
110
  const app = new Acryl().use(limit).get('/extensions', (c) =>
92
111
  c.json({
@@ -120,7 +139,7 @@ export const routes = {
120
139
  return c.json({ error: 'question must be a string' }, 400)
121
140
  return c.json(await inner.agent.run(question))
122
141
  })
123
- inner.router.route('/', app)
142
+ inner.router.route(prefix === '' ? '/' : prefix, app)
124
143
  })
125
144
  },
126
145
  }
@@ -4,12 +4,15 @@
4
4
  "acryl-router",
5
5
  "acryl-state",
6
6
  "acryl-node-server",
7
+ "acryl-page",
8
+ "acryl-web-server",
7
9
  "tasks",
8
10
  "agent",
9
11
  "openai",
10
12
  "sandbox",
11
13
  "extensions",
12
14
  "builder",
15
+ "plugin-share",
13
16
  "agent-panel",
14
17
  "manifest",
15
18
  "save",
@@ -45,10 +45,24 @@ test('answers health checks and 404 for unknown paths', () =>
45
45
  }))
46
46
 
47
47
  /** A sandbox may forbid listening on a port (Codex's workspace-write does); the one test that starts the real server is skipped there, and says so. */
48
+ /**
49
+ * Whether this environment forbids listening at all: only a sandbox's refusal (EPERM/EACCES) means the real-server test must skip. A loaded
50
+ * machine can fail a bind transiently, so that is retried briefly; any other outcome lets the real test run, where a host that truly cannot
51
+ * listen fails loudly instead of hiding behind a skip.
52
+ */
48
53
  const canListen = await new Promise<boolean>((done) => {
49
- const probe = createServer()
50
- probe.once('error', () => done(false))
51
- probe.listen(0, '127.0.0.1', () => probe.close(() => done(true)))
54
+ let attempts = 0
55
+ const probeOnce = (): void => {
56
+ attempts += 1
57
+ const probe = createServer()
58
+ probe.once('error', (error: NodeJS.ErrnoException) => {
59
+ if (error.code === 'EPERM' || error.code === 'EACCES') done(false)
60
+ else if (attempts < 3) setTimeout(probeOnce, 100)
61
+ else done(true)
62
+ })
63
+ probe.listen(0, '127.0.0.1', () => probe.close(() => done(true)))
64
+ }
65
+ probeOnce()
52
66
  })
53
67
  const skip = canListen ? false : 'this environment does not allow listening on a port'
54
68
  if (!canListen)
@@ -200,3 +214,31 @@ test('the app has a /settings page with the appearance setting, and the home pag
200
214
  for (const listener of listeners) listener({ key: null })
201
215
  assert.equal(dataset.theme, undefined)
202
216
  }))
217
+
218
+ test('the frame row mounts the same routes under /app; the app on its own server keeps its old URLs', () =>
219
+ inEnvironment({ NODE_ENV: 'test', ACRYL_PRODUCTION_KEEP: undefined }, async (app) => {
220
+ // The frame row claims that prefix on the frame's web adapter only; the app's own rows mount the routes there (the generated app-plugins row),
221
+ // so the frame row does not ask for the host router (127 app URLs).
222
+ const frame = await import('../src/frame.ts')
223
+ assert.deepEqual([...frame.inject].sort(), ['routerHttp'])
224
+ const { FRAME_MOUNT_PREFIX, routes } = await import('../src/routes.ts')
225
+ routes.apply(app.root, { prefix: FRAME_MOUNT_PREFIX })
226
+ for (const path of ['', '/health', '/settings', '/appearance.js']) {
227
+ const response = await app.root.router.request(`${FRAME_MOUNT_PREFIX}${path}`, {
228
+ headers: { accept: 'text/html' },
229
+ })
230
+ assert.equal(response.status, 200, path)
231
+ await response.body?.cancel()
232
+ }
233
+ // The page under the prefix writes its own URLs from it.
234
+ const page = await (
235
+ await app.root.router.request(`${FRAME_MOUNT_PREFIX}/settings`, {
236
+ headers: { accept: 'text/html' },
237
+ })
238
+ ).text()
239
+ assert.match(page, new RegExp(`src="${FRAME_MOUNT_PREFIX}/appearance.js"`, 'u'))
240
+ assert.match(page, new RegExp(`href="${FRAME_MOUNT_PREFIX}/"`, 'u'))
241
+ const missing = await app.root.router.request(`${FRAME_MOUNT_PREFIX}/nope`)
242
+ assert.equal(missing.status, 404)
243
+ await missing.body?.cancel()
244
+ }))
@@ -0,0 +1,2 @@
1
+ # Every checkout has the same line endings, on every OS. A plugin's digest counts every byte, so a CRLF checkout would make it a different plugin.
2
+ * text=auto eol=lf
@@ -1,5 +1,6 @@
1
1
 
2
2
  ## This app (blank)
3
3
 
4
- `src/routes.ts` is the app's own plugin: add routes to its `App`. `blend.yaml` lists the plugins that are mounted. `acrylblends add <plugin>` copies
5
- another library plugin (for example `acryl-state`) into `plugins/`.
4
+ The first screen (`src/routes.ts`) shows the app's name and, below it, the widget of every plugin that registers one with `ctx.home.widget`
5
+ (the `home` row). A new feature is a plugin of its own with its row in `blend.yaml`, never a change to `src/routes.ts`. `acrylblends add <plugin>`
6
+ copies another library plugin (for example `acryl-state`) into `plugins/`.
@@ -15,9 +15,20 @@ spec:
15
15
  rows:
16
16
  - id: router
17
17
  name: acryl-router
18
+ # The home page's widgets: a feature plugin puts its UI on the home page with ctx.home.widget, and it leaves with the plugin.
19
+ - id: home
20
+ name: home
18
21
  - id: routes
19
22
  name: routes
23
+ # A feature is a plugin: its folder plugins/<name>/ plus a row of its own above this one (`- id: notes` / `name: notes`), which Settings > Plugins
24
+ # switches with `disabled: true`. This row starts the folders that have no row yet (a live install with `.acryl/bin/acryl plugin add`); a folder with
25
+ # a row is never started twice. Read-only when NODE_ENV=production.
26
+ - id: extensions
27
+ name: extensions
20
28
  - id: server
21
29
  name: acryl-node-server
22
30
  config:
23
31
  hostname: "{{parameters.hostname}}"
32
+ # The door: an agent working in this folder looks at the page and acts on it through .acryl/bin/acryl. Development only: src/app.ts leaves it out when NODE_ENV=production.
33
+ - id: acryl-door
34
+ name: acryl-door
@@ -1,3 +1,4 @@
1
1
  node_modules/
2
2
  .acrylblends/
3
3
  .acryl/
4
+ package-lock.json
@@ -16,6 +16,7 @@
16
16
  "test": "node --import ./plugins/acryl-node-server/src/register.ts --test \"test/**/*.test.{ts,tsx}\"",
17
17
  "check": "tsc --noEmit",
18
18
  "lock": "node plugins/manifest/lock.ts",
19
+ "snapshot": "node plugins/manifest/snapshot.ts",
19
20
  "save": "node plugins/save/cli.ts",
20
21
  "upgrade": "node plugins/manifest/upgrade-cli.ts"
21
22
  },