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
@@ -0,0 +1,70 @@
1
+ /**
2
+ * What an app remembers about the plugins it pulled: their source and digests, in the `plugin-share` row's config of `blend.yaml`. A row's config is
3
+ * resolved into `blend.lock.json` too, so the lock records them with no new format, and a copy of the app can fetch exactly the same bytes. Pure text
4
+ * to text; comments and the person's formatting stay.
5
+ */
6
+ import { isMap, isSeq, parseDocument } from 'yaml'
7
+
8
+ export const ROW = 'plugin-share'
9
+
10
+ export interface PulledRecord {
11
+ /** `github:<owner>/<repo>@<version>` or an https address: something another machine can fetch. */
12
+ readonly source: string
13
+ /** The sha512 of the release file. */
14
+ readonly integrity: string
15
+ /** The sha256 of the plugin's content. */
16
+ readonly digest: string
17
+ }
18
+
19
+ const isRecord = (value: unknown): value is PulledRecord => {
20
+ const record = value as Partial<Record<keyof PulledRecord, unknown>> | null
21
+ return (
22
+ typeof record?.source === 'string' &&
23
+ typeof record.integrity === 'string' &&
24
+ typeof record.digest === 'string'
25
+ )
26
+ }
27
+
28
+ /** The plugins this definition remembers pulling. */
29
+ export function pulledRecords(text: string): Readonly<Record<string, PulledRecord>> {
30
+ const rows = (parseDocument(text).toJS() as { spec?: { rows?: unknown } } | null)?.spec?.rows
31
+ if (!Array.isArray(rows)) return {}
32
+ const pulled = (
33
+ rows.find((row: { name?: unknown }) => row?.name === ROW) as
34
+ | { config?: { pulled?: unknown } }
35
+ | undefined
36
+ )?.config?.pulled
37
+ if (typeof pulled !== 'object' || pulled === null) return {}
38
+ return Object.fromEntries(
39
+ Object.entries(pulled).filter(([, record]) => isRecord(record)),
40
+ ) as Record<string, PulledRecord>
41
+ }
42
+
43
+ /**
44
+ * The definition with one plugin's record written, or taken out when `record` is undefined. Unchanged when it has no `plugin-share` row: an app
45
+ * composed without the row has nowhere to remember.
46
+ */
47
+ export function withPulledRecord(
48
+ text: string,
49
+ name: string,
50
+ record: PulledRecord | undefined,
51
+ ): string {
52
+ const document = parseDocument(text)
53
+ const rows = document.getIn(['spec', 'rows'])
54
+ if (!isSeq(rows)) return text
55
+ const row = rows.items.find((item) => isMap(item) && item.get('name') === ROW)
56
+ if (!isMap(row)) return text
57
+ if (record === undefined) {
58
+ if (!row.hasIn(['config', 'pulled', name])) return text
59
+ row.deleteIn(['config', 'pulled', name])
60
+ const left = row.getIn(['config', 'pulled'])
61
+ if (isMap(left) && left.items.length === 0) row.deleteIn(['config', 'pulled'])
62
+ const config = row.get('config')
63
+ if (isMap(config) && config.items.length === 0) row.delete('config')
64
+ } else {
65
+ row.setIn(['config'], row.get('config') ?? document.createNode({}))
66
+ if (!row.hasIn(['config', 'pulled'])) row.setIn(['config', 'pulled'], document.createNode({}))
67
+ row.setIn(['config', 'pulled', name], document.createNode(record))
68
+ }
69
+ return String(document)
70
+ }
@@ -19,35 +19,52 @@ The docs write `import { Acryl } from 'acrylblends'` and `from 'acrylblends/<sub
19
19
 
20
20
  **JSX: write `.tsx`.** The scripts in `package.json` run Node.js with `--import ./plugins/acryl-node-server/src/register.ts`, which compiles `.tsx` files with this app's own `typescript`; `tsconfig.json` sets `jsx: react-jsx` and `jsxImportSource: #acryl-jsx`, which `package.json`'s `imports` maps to the vendored runtime in `plugins/acryl-router`. See `src/about.tsx` (blank template) for a page and `test/app.test.ts` for how it is tested. Render a component with `.toString()` and return it with `c.html(...)`; import `jsx`, `Fragment` and the types from `../plugins/acryl-router/src/core/jsx/index.ts` when you need them. A command that starts the app without those scripts (a bare `node src/main.ts`) cannot load a `.tsx` file. Without a build step you can write the call JSX compiles to instead: `jsx('h1', { class: 'title' }, 'Hello')`, children after the props.
21
21
 
22
- **A plugin you install into the running app** (`.acryl/bin/acryl plugin add plugins/<name>`) **imports the framework by its package name**: `import { Acryl } from 'acrylblends'` (and `acrylblends/<subpath>`, for example `acrylblends/helpers`). `acrylblends` and `acrylblends/<subpath>` resolve to the same files as the relative paths above, and the plugin is checked in a staging copy where a relative path like `../plugins/acryl-router/src/index.ts` may not resolve (a plugin that wrote it failed its first install). Plugins that `src/app.ts` imports at start-up keep using the relative paths. `App` is the
22
+ **A plugin imports the framework by its package name**: `import { Acryl } from 'acrylblends'` (and `acrylblends/<subpath>`, for example
23
+ `acrylblends/helpers`), which resolves to the same files as the relative paths above. Only files in `src/` use the relative paths. `App` is the
23
24
  deprecated name of `Acryl`.
24
25
 
25
- ## How a feature is built
26
+ ## How a feature is built: a plugin of its own
26
27
 
27
- Routes are an ordinary `Acryl` app that a plugin mounts once with `ctx.router.route(prefix, app)`. The mount belongs to the plugin and goes away when it unloads.
28
+ **Every feature is a Cordis plugin: a folder `plugins/<name>/` with an `index.ts`, plus its row in `blend.yaml`.** Never wire a feature into
29
+ `src/app.ts`, `src/routes.ts` or `src/frame.ts`, and never import it there: the row is what starts it. Settings > Plugins lists it, switches it off
30
+ and on (`disabled: true` on its row) and shares it, and every version of the app carries it.
31
+
32
+ When your prompt names this app's builder docs (`acryl_plugin_docs`), read the doc for your task and its verified example before you write the
33
+ plugin; they are this section in full.
28
34
 
29
35
  ```ts
36
+ // plugins/notes/index.ts
30
37
  import type { Context } from '@deepseek-ai/cordis'
31
- import { Acryl, HTTPException } from '../plugins/acryl-router/src/index.ts'
32
- import { schemaValidator, type StandardSchema } from '../plugins/acryl-router/src/helpers/index.ts'
38
+ import { Acryl } from 'acrylblends'
33
39
 
34
- export const name = 'notes' // a plugin's name; blend.yaml refers to it
35
- export const inject = ['router'] // services it needs
40
+ export const name = 'notes' // the folder's name; blend.yaml's row names it
41
+ export const inject = ['router'] // services it needs (a name nobody provides leaves it waiting, silently)
36
42
  export function apply(ctx: Context) {
37
43
  const app = new Acryl()
38
44
  .get('/', (c) => c.json({ ok: true }))
39
- .get('/:id', (c) => c.json({ id: c.req.param('id') }))
40
- .post('/', schemaValidator('json', NewNote), (c) => c.json(c.req.valid('json'), 201)) // NewNote is a Standard Schema
45
+ .post('/', async (c) => c.json(await c.req.json(), 201))
41
46
  ctx.router.route('/notes', app) // build the whole app first: routes added after route() are not served
42
47
  }
43
48
  ```
44
49
 
45
- - A small route goes on the `App` in `src/routes.ts`. A new feature is a new plugin file: add it to `registry` in `src/app.ts`, add a row
46
- (`- id: notes` / `name: notes`) to `blend.yaml` before the `acryl-node-server` row, and pass runtime values (a data file) as its config in `createApp`.
47
- - State, when `plugins/acryl-state` exists: `const cell = ctx.state.cell<T>('notes.list', initial)` (add `'state'` to `inject`), then
48
- `cell.get()`, `cell.set(value)`, `cell.update(fn)` and `cell.subscribe(fn)` (returns the unsubscribe). Every cell is also served as JSON at `/api/state/<key>`
49
- and streamed at `/api/state/<key>/events`.
50
- - Test a route without a socket: `await ctx.router.request('/notes')`, or boot the app as `test/app.test.ts` does and use `fetch`.
50
+ ```yaml
51
+ # blend.yaml, before the `acryl-node-server` row
52
+ - id: notes
53
+ name: notes
54
+ ```
55
+
56
+ - Routes are an ordinary `Acryl` app that the plugin mounts once with `ctx.router.route('/notes', app)`; the mount belongs to the plugin and goes
57
+ away when it unloads. The frame that hosts the app in development adds its canvas prefix to every route a plugin mounts: the same plugin
58
+ answers `/notes` under `npm start` and `/app/notes` in the canvas. Write the app's own path, and fetch it from a page by that path
59
+ (`fetch('/notes')`); never put the frame's prefix in a route or a URL.
60
+ - A plugin's name is its folder's name. It imports only `@deepseek-ai/cordis` (types), `acrylblends`, `node:*`, its own files, and type-only
61
+ `import type {} from '../home/index.ts'` / `'../instance/index.ts'` for the services those give `ctx`. Never guess another path.
62
+ - Something on the first screen (blank template): add `'home'` to `inject` and register a widget in an effect,
63
+ `ctx.effect(() => ctx.home.widget({ id: 'notes', title: 'Notes', render: () => '<ul>...</ul>' }))`; it leaves the page with the plugin.
64
+ - Data that must survive a restart goes in a file in `ctx.appInstance.home` (add `'appInstance'` to `inject`), written whole through a temporary
65
+ file and a rename; a variable starts again at every reload. With `plugins/acryl-state`: `ctx.state.cell<T>('notes.list', initial)` (add
66
+ `'state'` to `inject`), served as JSON at `/api/state/<key>` and streamed at `/api/state/<key>/events`.
67
+ - Test it in `test/<name>.test.ts` without a socket: boot the app as `test/app.test.ts` does and use `app.root.router.request('/notes')`.
51
68
 
52
69
  ## Things that surprise people
53
70
 
@@ -58,7 +75,10 @@ export function apply(ctx: Context) {
58
75
  - Handlers get the request as `c.req`: `c.req.param('id')`, `c.req.query('q')`, `await c.req.json()`, `c.req.raw` for the Web `Request`.
59
76
  - A server-sent stream is `streamSSE(c, async (stream) => { await stream.writeSSE({ data: JSON.stringify(x) }) ... })`; call `stream.onAbort(cleanup)`.
60
77
  - `npm run dev` restarts the app whenever a file changes.
78
+ - **In ACRYL's chat, the running app is the process that runs you: never stop, kill or restart it.** A saved change in `blend.yaml`, `src/` or `plugins/` reloads the app by itself and the page beside you shows it; a new plugin starts with that reload once its folder and its row are there. If a change does not show, the reload failed: read the error line in the frame's bar or `look` again, don't restart. `.acryl/bin/acryl plugin ...` is not available there, and is not needed.
61
79
 
62
80
  ## Before you say you are done
63
81
 
64
- `npm test` and `npm run check` (type-check) both pass. Put tests in `test/`, written with `node:test` like `test/app.test.ts`.
82
+ `npm test` and `npm run check` (type-check) both pass, and `.acryl/bin/acryl look` shows the feature on the page. Put tests in `test/`, written
83
+ with `node:test` like `test/app.test.ts`. A failure names the file and the line: fix the cause in the plugin, never the test, and never move the
84
+ feature into `src/` to get around it.
@@ -0,0 +1,19 @@
1
+ ---
2
+ name: acryl-add-ui
3
+ description: Use when a feature of __APP_NAME__ needs something the person sees - a widget on the first screen, a page of its own, a button or a form.
4
+ ---
5
+ # Add UI to the app
6
+
7
+ `<docs>` is the builder docs folder that the `acryl_plugin_docs` section of your prompt names (paths below are under it).
8
+
9
+ The app's UI is HTML its plugins serve. Two places:
10
+
11
+ - **On the first screen (the canvas)**: a widget. Read `<docs>/docs/extending/home-widget.md` completely and the example
12
+ `<docs>/example-plugins/packages/home-widget-basic/`. Add `'home'` to `inject`, register the widget inside `ctx.effect`, escape every value
13
+ you put into its HTML, prefix its element ids with the plugin's name, and let its `<script type="module">` fetch the plugin's own routes by the
14
+ app's path (`fetch('/<name>/items')`, never `/app/...` or `./...`).
15
+ - **A page of its own**: read `<docs>/docs/extending/routes-and-pages.md` and the example `route-page-basic`; link to it from the widget.
16
+
17
+ Decide where the data lives first (`<docs>/docs/extending/state.md`): what the person types must survive a reload, so it goes in a file in
18
+ `ctx.appInstance.home`, never only in a variable or in `localStorage`. Then build it with the acryl-build-plugin workflow and check the page with
19
+ `.acryl/bin/acryl look` and `act`.
@@ -0,0 +1,13 @@
1
+ ---
2
+ name: acryl-app
3
+ description: How the __APP_NAME__ app is put together - its blend.yaml rows, its plugins/ folder and src/ - and how to change it, run it and check it through the door. Use before adding a feature, a route, a plugin or a dependency to this app, and before saying the work is done.
4
+ ---
5
+
6
+ # Working in __APP_NAME__
7
+
8
+ 1. **Read first.** `AGENTS.md` (the same text as `CLAUDE.md`) is the app's guide. The framework's API is in `docs/framework/`: start at `docs/framework/README.md`, and `docs/framework/api/summary.md` is the one-page summary. Parts the docs mark "not yet available" are not in this app.
9
+ 2. **The app is data plus code.** `blend.yaml` is the blueprint: which plugins (rows) the app mounts, in what order, with what config. `src/app.ts` is the runtime: a row's `name` is a library plugin its `registry` imports or a folder `plugins/<name>/` of the app's own, and `compose` starts them in row order. `src/routes.ts` is the app's own `Acryl` app (its page and API). A plugin the app owns is a folder `plugins/<name>/`.
10
+ 3. **Make the change small.** A route is a handler on the app's `Acryl` app mounted with `ctx.router.route('/', app)`; test it with `await app.request('/path')`. Write the app's own paths: the frame that hosts the app adds its canvas prefix to every plugin route (`/board` answers `/board` under `npm start` and `/app/board` in the canvas), so never put the prefix in a route. Every feature is a Cordis plugin: a `plugins/<name>/` folder plus its row in `blend.yaml` (see the acryl-build-plugin skill); `src/app.ts` and `src/routes.ts` are never touched for a feature. `npx acrylblends add <name>` copies a library plugin there.
11
+ 4. **Defaults are swappable.** The database, the key-value store, the server and the window are plugins behind ports; the table "Defaults you can swap" in `AGENTS.md` names each default and where it is swapped. Never reach around a port to a driver: `npx acrylblends check-deps` fails the check.
12
+ 5. **Look at the running app.** In development the app writes `.acryl/bin/acryl`: `acryl brief` says what this folder runs, `acryl look` shows the open page as text with refs, and `acryl act click|type|select|press|scroll|wait|navigate|reload` acts on it. Look again before each act, because the refs change. This is the app's control surface: use it instead of guessing what the page shows.
13
+ 6. **Done means checked.** `npm test` passes, and the change shows in the running app. No secret goes in a file or in `git status`: keys come from environment variables.
@@ -0,0 +1,22 @@
1
+ ---
2
+ name: acryl-build-plugin
3
+ description: Use when the person asks for a new feature, widget, page, tool, list or integration in __APP_NAME__ (build a plugin). The workflow from reading the docs to a feature that is on the page and checked.
4
+ ---
5
+ # Build a feature as a plugin
6
+
7
+ `<docs>` is the builder docs folder that the `acryl_plugin_docs` section of your prompt names (paths below are under it).
8
+
9
+ 1. Read `<docs>/docs/start-here/this-app.md` completely: what the app is made of, the rule (a feature is `plugins/<name>/` plus its row in
10
+ `blend.yaml`, never code in `src/`) and the imports a plugin may use.
11
+ 2. Pick the docs for the feature from `<docs>/docs/README.md` (read-it-when column): routes and pages, a home widget, state. Read each one
12
+ completely and follow its links.
13
+ 3. Open the closest verified example in `<docs>/example-plugins/README.md` and read every file of it. Copy it to `plugins/<name>/` and change
14
+ it: `name` equals the folder's name, `inject` names only services that exist (`router`, `home`, `appInstance`, or a row's service).
15
+ Never guess a plugin's shape or an import path from memory.
16
+ 4. Add its row to `blend.yaml` before the `acryl-node-server` row (`- id: <name>` / `name: <name>`). Do not touch `src/app.ts`,
17
+ `src/routes.ts` or `src/frame.ts`.
18
+ 5. Write `test/<name>.test.ts` like `test/app.test.ts`: the widget is on the first screen, the routes answer, the data survives a remount.
19
+ 6. Verify (`<docs>/docs/start-here/verify-before-done.md`): `npm test`, `npm run check`, then `.acryl/bin/acryl look` shows it. Fix the
20
+ cause of the first failure and run again.
21
+ 7. Tell the person what you built, where it is on the page, and that it is the plugin `<name>` in Settings > Plugins. Never claim it works
22
+ before the three checks agree.
@@ -0,0 +1,14 @@
1
+ ---
2
+ name: acryl-change-plugin
3
+ description: Use when the person asks you to change, improve, extend or restyle a feature or plugin that already exists in __APP_NAME__.
4
+ ---
5
+ # Change a plugin
6
+
7
+ `<docs>` is the builder docs folder that the `acryl_plugin_docs` section of your prompt names (paths below are under it).
8
+
9
+ 1. Find it: the feature's folder in `plugins/` and its row in `blend.yaml` (Settings > Plugins lists them too). If no plugin has it, it may be
10
+ part of the app's own page in `src/`; say so instead of guessing.
11
+ 2. Read every file of the plugin before editing, and the doc for what you change (`<docs>/docs/README.md`).
12
+ 3. Edit its folder. The frame reloads the app 300 ms after the change; a change to `inject` or the row's config reloads it the same way.
13
+ Keep its name: a new name is a new plugin with a new row.
14
+ 4. Update its test, then verify (`<docs>/docs/start-here/verify-before-done.md`) and say what changed and that it is on the page.
@@ -0,0 +1,13 @@
1
+ ---
2
+ name: acryl-diagnose-plugin
3
+ description: Use when a feature of __APP_NAME__ misbehaves and the cause is unclear - nothing shows, buttons do nothing, data is lost, the top bar says Not reloaded, or a check fails in a way you do not understand.
4
+ ---
5
+ # Diagnose a plugin methodically
6
+
7
+ `<docs>` is the builder docs folder that the `acryl_plugin_docs` section of your prompt names (paths below are under it).
8
+
9
+ Read `<docs>/docs/start-here/troubleshooting.md`. Then, in order: (1) `npm test` and `npm run check`, and read the first error completely
10
+ (it names the file and the line); (2) `.acryl/bin/acryl look` to see what the page shows now; (3) check the plugin's row in `blend.yaml`
11
+ (present, not `disabled: true`, the folder's name) and that every name in `inject` is provided (a missing service leaves the plugin waiting
12
+ without an error); (4) change ONE thing, run the checks again, look again. Never guess a state you have not read. If the same cause survives two
13
+ fixes, stop and tell the person what you observed.
@@ -0,0 +1,13 @@
1
+ ---
2
+ name: acryl-fix-plugin
3
+ description: Use when a plugin you wrote in __APP_NAME__ fails - npm test or npm run check fails, the top bar says "Not reloaded", or the feature is not on the page.
4
+ ---
5
+ # Fix a plugin that does not work
6
+
7
+ `<docs>` is the builder docs folder that the `acryl_plugin_docs` section of your prompt names (paths below are under it).
8
+
9
+ Read `<docs>/docs/start-here/troubleshooting.md` and `<docs>/docs/start-here/verify-before-done.md`. The error is the real cause: read it
10
+ first. `Cannot find module` is a guessed import (a plugin imports only `@deepseek-ai/cordis`, `acrylblends`, `node:*` and its own files);
11
+ `exports the plugin name` means `name` differs from the folder; `no plugin '<name>' in the app's registry` means the row names a folder that is
12
+ not there; a plugin with no error and nothing on the page waits for a service its `inject` names. Fix the cause in the plugin (never the test,
13
+ never by moving the feature into `src/`), run the checks again, and only then tell the person it works.
@@ -0,0 +1,12 @@
1
+ ---
2
+ name: acryl-remove-plugin
3
+ description: Use when the person asks to remove, delete, switch off, hide or undo a feature or plugin of __APP_NAME__.
4
+ ---
5
+ # Switch off or remove a plugin
6
+
7
+ `<docs>` is the builder docs folder that the `acryl_plugin_docs` section of your prompt names (paths below are under it). Read `<docs>/docs/delivery/switch-and-remove.md`.
8
+
9
+ Find the exact plugin in `plugins/` and its row in `blend.yaml` (never guess the name). To hide it for now, switch it off: `disabled: true` on
10
+ its row (the switch in Settings > Plugins does the same); everything stays and it comes back when switched on. To remove it, and only when the
11
+ person clearly asked: delete its row, its folder and its test; its data in `.acrylblends/` stays unless they ask. Check with `npm test` and
12
+ `.acryl/bin/acryl look` that it is gone before you say so. Only touch the plugin the person named.
@@ -0,0 +1,16 @@
1
+ ---
2
+ name: acryl-share-plugin
3
+ description: Use when the person wants to share, publish or version a plugin of __APP_NAME__, save or restore a version of the app, or make a blueprint of it.
4
+ ---
5
+ # Share a plugin, versions and blueprints
6
+
7
+ `<docs>` is the builder docs folder that the `acryl_plugin_docs` section of your prompt names (paths below are under it). Read `<docs>/docs/delivery/share-and-snapshot.md`.
8
+
9
+ The app is `blend.yaml` + `plugins/` + `src/`, and every way it is kept or passed on reads exactly that. A version is a commit the frame
10
+ makes after a turn that changed files; the left panel lists and restores them. A plugin is shared from Settings > Plugins > Share on its row: it is
11
+ packaged for the CordisPlugins registry and published to a GitHub repository of the person's own account, with their own `gh` login.
12
+
13
+ 1. Make sure it works first (`<docs>/docs/start-here/verify-before-done.md`) and give it a doc comment whose first sentence says what it does.
14
+ 2. Sharing sends the person's work out under their name: never share, push or publish yourself. Tell them to press **Share** on the plugin's row.
15
+ Never ask for or handle a token.
16
+ 3. A fix to a plugin that was already shared needs a new version: Share again offers the next patch version and asks first.
@@ -0,0 +1,26 @@
1
+ ---
2
+ name: cordis-plugin
3
+ description: Add a Cordis plugin to __APP_NAME__ - a plugins/<name>/ folder with name, inject, apply and an optional check, its routes as an Acryl app, how its blend.yaml row mounts it, and how to verify it. Use when a feature should be its own plugin, or when asked to extend the app.
4
+ ---
5
+
6
+ # Adding a Cordis plugin
7
+
8
+ A plugin is a folder `plugins/<name>/` with an `index.ts` exporting `name` (its id), `inject` (the services it needs) and `apply(ctx)`. Everything it acquires belongs to one `ctx.effect(...)`, so unloading the plugin or stopping the app releases it.
9
+
10
+ ```ts
11
+ import type { Context } from '@deepseek-ai/cordis'
12
+ import { Acryl } from 'acrylblends'
13
+
14
+ export const name = 'due-dates'
15
+ export const inject = ['router']
16
+
17
+ export function apply(ctx: Context): void {
18
+ const app = new Acryl().get('/due-dates', (c) => c.json({ ok: true }))
19
+ ctx.router.route('/due-dates', app)
20
+ }
21
+ ```
22
+
23
+ - **Routes** are an `Acryl` app mounted with `ctx.router.route('/due-dates', app)`; removing the plugin removes them. Write the app's own path: the frame that hosts the app in development adds its canvas prefix, so the same route answers `/due-dates` under `npm start` and `/app/due-dates` in the canvas. Never put the frame's prefix in a plugin's route. Import the framework by name (`acrylblends`, `acrylblends/body-limit`); never guess a relative path into another plugin.
24
+ - **State** lives in cells: `ctx.state.cell('due-dates.tasks', [])`. A page follows one with `watch` from `/api/state/client.js`, never with an `EventSource`, and never by polling what `watch` already pushes.
25
+ - **Its row starts it.** Add a row to `blend.yaml` (`- id: <name>` and `name: <name>`, before the `acryl-node-server` row); `src/app.ts` finds the folder by that name, so it is never imported there. `disabled: true` on the row switches it off (Settings > Plugins does the same). `.acryl/bin/acryl plugin add plugins/<name>` installs a folder without a row live, for trying it; a folder with a row is the app's.
26
+ - **Check it.** `npm test` and `npm run check` must pass. With the app running, use the door: `.acryl/bin/acryl look` reads the page as text with refs and `acryl act ...` acts on it; an install or reload shows the new feature in the page.
@@ -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
@@ -5,7 +5,7 @@ Besides `src/routes.ts`, the app runs an agent (`POST /ask`) that can write plug
5
5
  rollback; `GET /extensions` and `GET /tasks` show what it did. A plugin there is a folder with an `index.ts` exporting `name`, `inject`, `apply(ctx)`
6
6
  and an optional `check(ctx)` that receives the plugin's own context (so `ctx.router` reads as in `apply`). It may import `acryl-router`.
7
7
 
8
- **Write an extension, not a row.** A feature you build while the app runs is a folder `plugins/<name>/` installed with `.acryl/bin/acryl plugin add plugins/<name>` (the `extensions` plugin loads it again at every start). Add a row to `blend.yaml` and an entry to the `registry` of `src/app.ts` only when the person asks for a plugin that is part of the app itself. Both kinds may import the framework by name (`acrylblends`, `acrylblends/body-limit`, `acryl-router`); a row's relative imports of its sibling plugins also work.
8
+ **Write an extension, not a row.** A feature you build while the app runs is a folder `plugins/<name>/` installed with `.acryl/bin/acryl plugin add plugins/<name>` (the `extensions` plugin loads it again at every start). Add a row to `blend.yaml` (`- id: <name>` / `name: <name>`; `src/app.ts` finds the folder by that name) only when the person asks for a plugin that is part of the app itself. Both kinds may import the framework by name (`acrylblends`, `acrylblends/body-limit`, `acryl-router`); a row's relative imports of its sibling plugins also work.
9
9
 
10
10
  **Drive the app you build.** `.acryl/bin/acryl look` reads the open page as text with refs, and `acryl act click|type|press|scroll|wait --ref <ref>` presses its buttons, opens its sidebars and fills its forms (look again before each act: the refs change). `act navigate --path /settings` goes to another page of the app; a path the app does not serve answers `404 at /nope` and the window shows a 404 page, so navigate on from there. In the desktop app (`npm run desktop`) the app's windows and menu are verbs too: `acryl window list`, `acryl window open --path /settings`, `acryl look --window <id>` (and `act ... --window <id>`) to drive that window's page, `acryl window resize|focus|close --id <id>`, `acryl menu list` and `acryl menu click --path "File>New Note"`. Closing the last window and Quit end the desktop app, so they need `--force`: do not use it unless the person asks. The app has a `/settings` page already (the appearance: system, light or dark, kept in the browser) and the desktop app's menu has a Settings item that opens it; add a person's other settings to that page, not to a new one, so `acryl menu click --path "File>Settings..."` and `acryl window open --path /settings` keep opening them (on macOS the item is in the application menu). Without a desktop app these answer "no desktop window is attached; run npm run desktop".
11
11
 
@@ -24,10 +24,13 @@ spec:
24
24
  name: tasks
25
25
  config:
26
26
  store: "{{parameters.taskStore}}"
27
+ # The model the agent asks at each step: a scripted offline one until a provider key is set.
27
28
  - id: model
28
29
  name: model
30
+ # The tools the agent can call, each with what it does and the input it takes.
29
31
  - id: tools
30
32
  name: tools
33
+ # Long-term memory for the agent.
31
34
  - id: memory
32
35
  name: memory
33
36
  - id: agent
@@ -46,11 +49,14 @@ spec:
46
49
  approval: "{{parameters.approval}}"
47
50
  - id: builder
48
51
  name: builder
52
+ - id: plugin-share
53
+ name: plugin-share
49
54
  - id: save
50
55
  name: save
51
56
  # The panel: a chat button on every page in development (the app's own agent, or Codex, Claude or pi started in this folder). It starts nothing in production.
52
57
  - id: agent-panel
53
58
  name: agent-panel
59
+ # The app's own pages and API.
54
60
  - id: routes
55
61
  name: routes
56
62
  - id: server
@@ -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
  },