acrylblends 1.1.0 → 1.2.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.
- package/README.md +9 -0
- package/dist/index.d.ts +45 -1
- package/dist/index.js +1980 -27
- package/dist/index.js.map +1 -1
- package/docs/framework/agents/index.md +1 -1
- package/docs/framework/getting-started/desktop.md +23 -4
- package/docs/framework/guides/create-acrylblends.md +1 -1
- package/docs/framework/workshop/README.md +138 -152
- package/docs/framework/workshop/facilitator.md +69 -101
- package/legacy/README.md +9 -0
- package/legacy/agent/157ce3db1112/src/app.ts +98 -0
- package/legacy/agent/157ce3db1112/src/routes.ts +63 -0
- package/legacy/agent/24c681a00fca/src/app.ts +160 -0
- package/legacy/agent/24c681a00fca/src/routes.ts +83 -0
- package/legacy/agent/2e790da977b2/src/app.ts +98 -0
- package/legacy/agent/2e790da977b2/src/routes.ts +63 -0
- package/legacy/agent/6307045545dc/src/app.ts +168 -0
- package/legacy/agent/6307045545dc/src/routes.ts +83 -0
- package/legacy/agent/6a5fb9c15c53/src/app.ts +158 -0
- package/legacy/agent/6a5fb9c15c53/src/routes.ts +83 -0
- package/legacy/agent/71fb54a5a942/src/app.ts +111 -0
- package/legacy/agent/71fb54a5a942/src/routes.ts +63 -0
- package/legacy/agent/74ece148269c/src/app.ts +168 -0
- package/legacy/agent/74ece148269c/src/routes.ts +93 -0
- package/legacy/agent/855359304707/src/app.ts +168 -0
- package/legacy/agent/855359304707/src/routes.ts +126 -0
- package/legacy/agent/b5ef3c786ae4/src/app.ts +127 -0
- package/legacy/agent/b5ef3c786ae4/src/routes.ts +76 -0
- package/legacy/agent/c2f30958043b/src/app.ts +98 -0
- package/legacy/agent/c2f30958043b/src/routes.ts +53 -0
- package/legacy/blank/5a91c558af22/src/routes.ts +18 -0
- package/legacy/blank/9c650cf52bf3/src/routes.ts +14 -0
- package/legacy/blank/aea589283445/src/routes.ts +16 -0
- package/legacy/blank/b2cbb759c889/src/routes.ts +16 -0
- package/legacy/blank/d6d371c9a65e/src/routes.ts +18 -0
- package/legacy/chat/7076cc821e2c/src/app.ts +72 -0
- package/legacy/chat/7076cc821e2c/src/routes.ts +13 -0
- package/legacy/chat/7dbe6d5170df/src/app.ts +107 -0
- package/legacy/chat/7dbe6d5170df/src/routes.ts +19 -0
- package/legacy/chat/a9a9ab44c58e/src/app.ts +107 -0
- package/legacy/chat/a9a9ab44c58e/src/routes.ts +15 -0
- package/legacy/chat/f20ad9a51b06/src/app.ts +106 -0
- package/legacy/chat/f20ad9a51b06/src/routes.ts +15 -0
- package/legacy/layouts.json +524 -0
- package/legacy/nodejs/1d477f37aaf4/src/app.ts +92 -0
- package/legacy/nodejs/1d477f37aaf4/src/routes.ts +40 -0
- package/legacy/nodejs/b1bd9462496c/src/app.ts +61 -0
- package/legacy/nodejs/b1bd9462496c/src/routes.ts +37 -0
- package/legacy/nodejs/d5bab75076ef/src/app.ts +95 -0
- package/legacy/nodejs/d5bab75076ef/src/routes.ts +40 -0
- package/legacy/notes/4edc36949c15/src/app.ts +107 -0
- package/legacy/notes/4edc36949c15/src/routes.ts +14 -0
- package/legacy/notes/6cab2d09747c/src/app.ts +72 -0
- package/legacy/notes/6cab2d09747c/src/routes.ts +12 -0
- package/legacy/notes/7ca2dfed623a/src/app.ts +106 -0
- package/legacy/notes/7ca2dfed623a/src/routes.ts +14 -0
- package/legacy/todo/02e613b3625c/src/app.ts +71 -0
- package/legacy/todo/02e613b3625c/src/todo.ts +123 -0
- package/legacy/todo/45694a8d9e15/src/app.ts +71 -0
- package/legacy/todo/45694a8d9e15/src/todo.ts +96 -0
- package/legacy/todo/5923d3a6099f/src/app.ts +71 -0
- package/legacy/todo/5923d3a6099f/src/todo.ts +125 -0
- package/legacy/todo/849a18d66dc1/src/app.ts +91 -0
- package/legacy/todo/849a18d66dc1/src/todo.ts +125 -0
- package/legacy/todo/8e64211312f3/src/app.ts +71 -0
- package/legacy/todo/8e64211312f3/src/todo.ts +125 -0
- package/legacy/todo/b79078a87565/src/app.ts +71 -0
- package/legacy/todo/b79078a87565/src/todo.ts +125 -0
- package/lib/{audit-BFpULBCj.d.ts → audit-Bok_UNCn.d.ts} +26 -3
- package/lib/{client-modules-drj4mjhj.js → client-modules-Bkdr83jO.js} +1 -1
- package/lib/cordis/door.d.ts +42 -7
- package/lib/cordis/door.js +204 -60
- package/lib/cordis/inspect.js +1 -1
- package/lib/cordis/node-server.d.ts +101 -3
- package/lib/cordis/node-server.js +3 -3
- package/lib/cordis/router-http.d.ts +7 -1
- package/lib/cordis/router-http.js +44 -3
- package/lib/cordis/router.d.ts +1 -1
- package/lib/cordis/router.js +1 -1
- package/lib/cordis/state.js +1 -1
- package/lib/door/host.d.ts +2 -2
- package/lib/door/host.js +1 -1
- package/lib/door/page.d.ts +10 -1
- package/lib/door/page.js +46 -7
- package/lib/door/protocol.d.ts +1 -1
- package/lib/drivers.d.ts +10 -1
- package/lib/{index-DMOtA-NJ.d.ts → index-DGq7H0lb.d.ts} +11 -1
- package/lib/node-server.d.ts +7 -1
- package/lib/node-server.js +2 -2
- package/lib/{protocol-a2aqrp2X.d.ts → protocol-BeIYCgTo.d.ts} +4 -0
- package/lib/{snapshot-text-BwM6T2nG.js → snapshot-text-CXTnjxGx.js} +87 -7
- package/lib/src-CtkIX_Gg.js +427 -0
- package/lib/{src-BTBm4OPD.js → src-iBJ2ysV9.js} +77 -14
- package/lib/{test-websocket-BLeEvybj.js → test-websocket-CQOSvBth.js} +17 -7
- package/lib/{test-websocket-DyeS50Tr.d.ts → test-websocket-GUTAkFQT.d.ts} +3 -1
- package/package.json +2 -1
- package/plugins/acryl-door/src/host/channel.ts +86 -9
- package/plugins/acryl-door/src/host/chat-tools.ts +8 -6
- package/plugins/acryl-door/src/host/door-file.ts +7 -2
- package/plugins/acryl-door/src/host/door.ts +53 -23
- package/plugins/acryl-door/src/host/page-routes.ts +3 -1
- package/plugins/acryl-door/src/host/page-socket.ts +18 -8
- package/plugins/acryl-door/src/host/verbs.ts +3 -0
- package/plugins/acryl-door/src/index.ts +166 -29
- package/plugins/acryl-door/src/page/base.ts +32 -0
- package/plugins/acryl-door/src/page/channel.ts +3 -1
- package/plugins/acryl-door/src/page/driver/accessibility.ts +11 -0
- package/plugins/acryl-door/src/page/driver/actions.ts +4 -0
- package/plugins/acryl-door/src/page/driver/driver.ts +32 -4
- package/plugins/acryl-door/src/page/driver/snapshot.ts +5 -2
- package/plugins/acryl-door/src/page/index.ts +41 -2
- package/plugins/acryl-door/src/page/outline.ts +56 -0
- package/plugins/acryl-door/src/page/protocol.ts +4 -0
- package/plugins/acryl-door/src/page/snapshot-text.ts +2 -1
- package/plugins/acryl-node-server/src/adapter/listener.ts +4 -0
- package/plugins/acryl-node-server/src/adapter/raw.ts +18 -0
- package/plugins/acryl-node-server/src/adapter/websocket.ts +15 -9
- package/plugins/acryl-node-server/src/client-modules.ts +7 -1
- package/plugins/acryl-node-server/src/index.ts +304 -16
- package/plugins/acryl-node-server/src/upgrade-hub.ts +2 -2
- package/plugins/acryl-page/plugin.json +12 -0
- package/plugins/acryl-page/src/index.ts +163 -0
- package/plugins/acryl-router/src/service.ts +77 -14
- package/plugins/acryl-router-http/src/index.ts +15 -1
- package/plugins/acryl-router-http/src/serve.ts +31 -0
- package/plugins/acryl-web-server/plugin.json +14 -0
- package/plugins/acryl-web-server/src/index.ts +164 -0
- package/plugins/agent/agent.ts +38 -7
- package/plugins/agent/index.ts +1 -1
- package/plugins/agent/model.ts +10 -2
- package/plugins/agent/tools.ts +9 -3
- package/plugins/agent-panel/client/describe.ts +57 -0
- package/plugins/agent-panel/client/driver-pick.ts +22 -0
- package/plugins/agent-panel/client/frame.ts +51 -0
- package/plugins/agent-panel/client/main.ts +238 -26
- package/plugins/agent-panel/drivers.ts +7 -2
- package/plugins/agent-panel/headless.ts +5 -2
- package/plugins/agent-panel/index.ts +146 -57
- package/plugins/agent-panel/mount.ts +27 -0
- package/plugins/agent-panel/visitors.ts +231 -0
- package/plugins/desktop/frame.ts +231 -0
- package/plugins/desktop/launch.ts +44 -9
- package/plugins/desktop/main.ts +100 -32
- package/plugins/desktop/window.ts +21 -4
- package/plugins/driver-pty/client/terminal.ts +4 -1
- package/plugins/driver-pty/index.ts +38 -11
- package/plugins/driver-pty/spawn.ts +38 -0
- package/plugins/driver-pty/terminal.ts +2 -0
- package/plugins/extensions/index.ts +26 -1
- package/plugins/extensions/resolve.ts +13 -0
- package/plugins/feedback/client.d.ts +22 -0
- package/plugins/feedback/client.js +471 -0
- package/plugins/feedback/index.ts +143 -0
- package/plugins/feedback/plugin.json +11 -0
- package/plugins/feedback/store.ts +307 -0
- package/plugins/home/index.ts +122 -0
- package/plugins/home/plugin.json +11 -0
- package/plugins/instance/index.ts +6 -0
- package/plugins/manifest/app-plugins.ts +86 -0
- package/plugins/manifest/index.ts +12 -2
- package/plugins/manifest/snapshot.ts +275 -0
- package/plugins/openai/index.ts +64 -5
- package/plugins/openai/prompt.ts +23 -0
- package/shared/AGENTS.core.md +36 -16
- package/shared/skills/acryl-add-ui/SKILL.md +19 -0
- package/shared/skills/acryl-app/SKILL.md +13 -0
- package/shared/skills/acryl-build-plugin/SKILL.md +22 -0
- package/shared/skills/acryl-change-plugin/SKILL.md +14 -0
- package/shared/skills/acryl-diagnose-plugin/SKILL.md +13 -0
- package/shared/skills/acryl-fix-plugin/SKILL.md +13 -0
- package/shared/skills/acryl-remove-plugin/SKILL.md +12 -0
- package/shared/skills/acryl-share-plugin/SKILL.md +16 -0
- package/shared/skills/cordis-plugin/SKILL.md +26 -0
- package/templates/agent/AGENTS.md +1 -1
- package/templates/agent/blend.yaml +4 -0
- package/templates/agent/gitignore +1 -0
- package/templates/agent/package.json +1 -0
- package/templates/agent/src/app.ts +150 -33
- package/templates/agent/src/frame.ts +17 -0
- package/templates/agent/src/main.ts +5 -3
- package/templates/agent/src/routes.ts +38 -19
- package/templates/agent/template.json +2 -0
- package/templates/agent/test/app.test.ts +45 -3
- package/templates/blank/AGENTS.md +3 -2
- package/templates/blank/blend.yaml +11 -0
- package/templates/blank/gitignore +1 -0
- package/templates/blank/package.json +1 -0
- package/templates/blank/src/app.ts +100 -29
- package/templates/blank/src/door.ts +7 -0
- package/templates/blank/src/first-page.ts +5 -0
- package/templates/blank/src/frame.ts +17 -0
- package/templates/blank/src/routes.ts +65 -8
- package/templates/blank/template.json +14 -1
- package/templates/blank/test/app.test.ts +103 -16
- package/templates/chat/blend.yaml +2 -1
- package/templates/chat/gitignore +1 -0
- package/templates/chat/package.json +1 -0
- package/templates/chat/src/app.ts +72 -22
- package/templates/chat/src/chat-app.ts +11 -2
- package/templates/chat/src/client/chat.tsx +4 -2
- package/templates/chat/src/frame.ts +17 -0
- package/templates/chat/src/page.tsx +6 -4
- package/templates/chat/src/routes.ts +14 -4
- package/templates/chat/src/runtime.ts +9 -3
- package/templates/chat/template.json +3 -1
- package/templates/chat/test/chat.test.ts +46 -1
- package/templates/chat/test/server.test.ts +17 -3
- package/templates/nodejs/blend.yaml +2 -1
- package/templates/nodejs/gitignore +1 -0
- package/templates/nodejs/package.json +1 -0
- package/templates/nodejs/src/app.ts +76 -21
- package/templates/nodejs/src/frame.ts +17 -0
- package/templates/nodejs/src/page.tsx +3 -3
- package/templates/nodejs/src/routes.ts +32 -13
- package/templates/nodejs/template.json +3 -1
- package/templates/nodejs/test/app.test.ts +38 -3
- package/templates/nodejs/test/server.test.ts +17 -3
- package/templates/notes/blend.yaml +2 -1
- package/templates/notes/gitignore +1 -0
- package/templates/notes/package.json +1 -0
- package/templates/notes/src/app.ts +72 -22
- package/templates/notes/src/frame.ts +17 -0
- package/templates/notes/src/notes-app.ts +24 -17
- package/templates/notes/src/routes.ts +11 -4
- package/templates/notes/src/runtime.ts +9 -3
- package/templates/notes/src/views.tsx +26 -17
- package/templates/notes/template.json +3 -1
- package/templates/notes/test/notes.test.ts +39 -1
- package/templates/notes/test/server.test.ts +17 -3
- package/templates/todo/blend.yaml +3 -0
- package/templates/todo/gitignore +1 -0
- package/templates/todo/package.json +1 -0
- package/templates/todo/src/app.ts +81 -27
- package/templates/todo/src/door.ts +7 -0
- package/templates/todo/src/frame.ts +17 -0
- package/templates/todo/src/page.ts +13 -6
- package/templates/todo/src/todo.ts +18 -6
- package/templates/todo/template.json +11 -1
- package/templates/todo/test/todo.test.ts +67 -9
- package/lib/src-LX0E4ec4.js +0 -117
- package/shared/SKILL.md +0 -14
|
@@ -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.
|
|
@@ -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`
|
|
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
|
|
@@ -51,6 +54,7 @@ spec:
|
|
|
51
54
|
# 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
55
|
- id: agent-panel
|
|
53
56
|
name: agent-panel
|
|
57
|
+
# The app's own pages and API.
|
|
54
58
|
- id: routes
|
|
55
59
|
name: routes
|
|
56
60
|
- id: server
|
|
@@ -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
|
},
|
|
@@ -6,22 +6,24 @@ 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'
|
|
21
23
|
import { sandboxPlugin } from '../plugins/sandbox/index.ts'
|
|
22
24
|
import { save } from '../plugins/save/index.ts'
|
|
23
25
|
import { tasks } from '../plugins/tasks/index.ts'
|
|
24
|
-
import { routes } from './routes.ts'
|
|
26
|
+
import { FRAME_MOUNT_PREFIX, routes } from './routes.ts'
|
|
25
27
|
|
|
26
28
|
/** The plugins this app's code provides. `blend.yaml` chooses, orders and configures them. */
|
|
27
29
|
export const registry = {
|
|
@@ -44,6 +46,8 @@ export const registry = {
|
|
|
44
46
|
|
|
45
47
|
/** The manifest next to this app's source. */
|
|
46
48
|
export const MANIFEST = join(fileURLToPath(new URL('..', import.meta.url)), 'blend.yaml')
|
|
49
|
+
/** The app's own plugins (a feature each), which `blend.yaml` mounts by their folder name next to the library copies `registry` imports. */
|
|
50
|
+
const PLUGINS_DIR = join(fileURLToPath(new URL('..', import.meta.url)), 'plugins')
|
|
47
51
|
|
|
48
52
|
/**
|
|
49
53
|
* 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 +55,7 @@ export const MANIFEST = join(fileURLToPath(new URL('..', import.meta.url)), 'ble
|
|
|
51
55
|
*/
|
|
52
56
|
export function readManifest(values?: Readonly<Record<string, ParameterValue>>) {
|
|
53
57
|
return loadManifest(MANIFEST, {
|
|
54
|
-
known: Object.keys(registry),
|
|
58
|
+
known: [...Object.keys(registry), ...appPluginNames(PLUGINS_DIR)],
|
|
55
59
|
getBlueprint: blueprintsIn(join(fileURLToPath(new URL('..', import.meta.url)), 'blueprints')),
|
|
56
60
|
...(values ? { values } : {}),
|
|
57
61
|
})
|
|
@@ -77,28 +81,61 @@ const DEVELOPMENT_ROWS: Record<string, string> = {
|
|
|
77
81
|
'a plugin installed live runs in this process, and POST /extensions/approve decides installs',
|
|
78
82
|
}
|
|
79
83
|
|
|
84
|
+
/** The only tools the production Assistant's agent may call: look at the page and act on it. */
|
|
85
|
+
const ASSISTANT_TOOLS = ['door_look', 'door_act']
|
|
86
|
+
|
|
87
|
+
/** 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. */
|
|
88
|
+
const ASSISTANT_MAX_TOKENS = 1024
|
|
89
|
+
|
|
90
|
+
/** Model calls per visitor question: look, open a page, look, filter, look and answer is six, with room for one wrong turn. */
|
|
91
|
+
const ASSISTANT_STEPS = 10
|
|
92
|
+
|
|
93
|
+
/** What the Assistant says while the app has no model: the scripted one cannot use the page. */
|
|
94
|
+
export const ASSISTANT_NO_MODEL =
|
|
95
|
+
"This assistant is not set up yet: the app's owner has not chosen a model for it."
|
|
96
|
+
|
|
80
97
|
/**
|
|
81
|
-
* The app
|
|
82
|
-
*
|
|
98
|
+
* 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
|
|
99
|
+
* 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
|
|
100
|
+
* door to the frame host, which mounts its own.
|
|
83
101
|
*/
|
|
84
|
-
export async function
|
|
102
|
+
export async function composeInto(
|
|
103
|
+
ctx: Context,
|
|
85
104
|
chosen: AppInstance,
|
|
86
105
|
options: {
|
|
87
106
|
values?: Readonly<Record<string, ParameterValue>>
|
|
88
|
-
/** A real model (any OpenAI-compatible API) in place of the offline scripted one. */
|
|
89
107
|
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
108
|
listen?: boolean
|
|
109
|
+
host?: boolean
|
|
92
110
|
} = {},
|
|
93
|
-
): Promise<
|
|
111
|
+
): Promise<Fiber[]> {
|
|
94
112
|
const read = readManifest(options.values)
|
|
95
113
|
// 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
114
|
// serves a folder's door, so the tests that start several apps set it).
|
|
97
|
-
const listen = options.listen !== false
|
|
115
|
+
const listen = options.listen !== false && options.host !== true
|
|
98
116
|
const leaveOut = new Set<string>()
|
|
99
117
|
let extensionsReadOnly = false
|
|
100
|
-
|
|
101
|
-
|
|
118
|
+
// 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
|
|
119
|
+
// contributes only the plugins that are its own.
|
|
120
|
+
if (!listen || options.host === true) leaveOut.add('acryl-node-server').add('acryl-door')
|
|
121
|
+
if (options.host === true)
|
|
122
|
+
for (const row of [
|
|
123
|
+
'instance',
|
|
124
|
+
'acryl-router',
|
|
125
|
+
'acryl-web-server',
|
|
126
|
+
'acryl-page',
|
|
127
|
+
'manifest',
|
|
128
|
+
'save',
|
|
129
|
+
])
|
|
130
|
+
leaveOut.add(row)
|
|
131
|
+
// 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,
|
|
132
|
+
// 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.
|
|
133
|
+
const assistant =
|
|
134
|
+
process.env.NODE_ENV === 'production' &&
|
|
135
|
+
process.env.ACRYL_ASSISTANT === 'on' &&
|
|
136
|
+
process.env.ACRYL_DOOR !== 'off' &&
|
|
137
|
+
listen
|
|
138
|
+
if (!assistant && (process.env.NODE_ENV === 'production' || process.env.ACRYL_DOOR === 'off'))
|
|
102
139
|
leaveOut.add('acryl-door')
|
|
103
140
|
// 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
141
|
// 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 +158,7 @@ export async function createApp(
|
|
|
121
158
|
`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
159
|
)
|
|
123
160
|
else if (row === 'extensions') extensionsReadOnly = true
|
|
124
|
-
else leaveOut.add(row)
|
|
161
|
+
else if (!(assistant && row === 'agent')) leaveOut.add(row)
|
|
125
162
|
}
|
|
126
163
|
}
|
|
127
164
|
const manifest = {
|
|
@@ -136,31 +173,111 @@ export async function createApp(
|
|
|
136
173
|
console.warn(
|
|
137
174
|
'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
175
|
)
|
|
139
|
-
|
|
140
|
-
const
|
|
141
|
-
|
|
176
|
+
// 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.
|
|
177
|
+
const modelConfig =
|
|
178
|
+
options.model && assistant
|
|
179
|
+
? {
|
|
180
|
+
...options.model,
|
|
181
|
+
system: options.model.system ?? ASSISTANT_PROMPT,
|
|
182
|
+
maxTokens: options.model.maxTokens ?? ASSISTANT_MAX_TOKENS,
|
|
183
|
+
}
|
|
184
|
+
: options.model
|
|
185
|
+
if (assistant && !options.model)
|
|
186
|
+
console.warn(
|
|
187
|
+
`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).`,
|
|
188
|
+
)
|
|
189
|
+
// 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
|
|
190
|
+
// is imported here; all of them, off or on, belong to their row, so the extensions row never starts them a second time.
|
|
191
|
+
const ownRows = [
|
|
192
|
+
...new Set(read.plugins.map((row) => row.use).filter((use) => !Object.hasOwn(registry, use))),
|
|
193
|
+
]
|
|
194
|
+
const ownPlugins = await loadAppPlugins(
|
|
195
|
+
PLUGINS_DIR,
|
|
196
|
+
ownRows.filter((use) => manifest.plugins.some((row) => row.use === use && !row.disabled)),
|
|
197
|
+
)
|
|
142
198
|
const fibers: Fiber[] = [
|
|
143
|
-
first,
|
|
144
199
|
// The `model` row mounts the scripted model, or the real one when a model is configured.
|
|
145
|
-
...(await compose(
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
200
|
+
...(await compose(
|
|
201
|
+
ctx,
|
|
202
|
+
manifest,
|
|
203
|
+
{ ...registry, ...ownPlugins, ...(modelConfig ? { model: openai } : {}) },
|
|
204
|
+
{
|
|
205
|
+
...(modelConfig ? { model: { ...modelConfig } } : {}),
|
|
206
|
+
// Runtime values the manifest cannot hold: the manifest's own digest (every task records it), and who may install.
|
|
207
|
+
tasks: { manifest: `sha256:${manifest.digest}` },
|
|
208
|
+
// The `approval` parameter in blend.yaml (the extensions row) turns this source into one that asks.
|
|
209
|
+
extensions: {
|
|
210
|
+
...(extensionsReadOnly ? { readOnly: true } : {}),
|
|
211
|
+
sources: [
|
|
212
|
+
{ dir: extensionsDir, trust: 'install' },
|
|
213
|
+
...(legacy ? [{ dir: legacyDir, trust: 'install' as const }] : []),
|
|
214
|
+
],
|
|
215
|
+
rows: ownRows,
|
|
216
|
+
},
|
|
217
|
+
builder: { dir: extensionsDir },
|
|
218
|
+
...(assistant
|
|
219
|
+
? {
|
|
220
|
+
// Every answer comes right after a look, so it says what the visitor's page shows now.
|
|
221
|
+
agent: { tools: ASSISTANT_TOOLS, steps: ASSISTANT_STEPS, answerAfter: 'door_look' },
|
|
222
|
+
'acryl-door': { pageOnly: true },
|
|
223
|
+
'agent-panel': { assistant: true },
|
|
224
|
+
// The visitor reaches the agent through the panel only: no /ask, /tasks or /extensions routes.
|
|
225
|
+
routes: { agentRoutes: false },
|
|
226
|
+
}
|
|
227
|
+
: {}),
|
|
228
|
+
// 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
|
|
229
|
+
// 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
|
|
230
|
+
// 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`.
|
|
231
|
+
...(options.host === true
|
|
232
|
+
? {
|
|
233
|
+
'agent-panel': { mount: FRAME_MOUNT_PREFIX },
|
|
234
|
+
'driver-pty': { mount: FRAME_MOUNT_PREFIX },
|
|
235
|
+
feedback: { mount: FRAME_MOUNT_PREFIX },
|
|
236
|
+
routes: { prefix: FRAME_MOUNT_PREFIX, ...(assistant ? { agentRoutes: false } : {}) },
|
|
237
|
+
}
|
|
238
|
+
: {}),
|
|
239
|
+
// The frame host runs the server itself and its app instance carries no port; only the app's own server configures one.
|
|
240
|
+
...(options.host === true
|
|
241
|
+
? {}
|
|
242
|
+
: {
|
|
243
|
+
server: {
|
|
244
|
+
port: chosen.port.start,
|
|
245
|
+
scan: chosen.port.scan ? 20 : 0,
|
|
246
|
+
hostname: chosen.host,
|
|
247
|
+
},
|
|
248
|
+
}),
|
|
156
249
|
},
|
|
157
|
-
|
|
158
|
-
server: { port: chosen.port.start, scan: chosen.port.scan ? 20 : 0, hostname: chosen.host },
|
|
159
|
-
})),
|
|
250
|
+
)),
|
|
160
251
|
]
|
|
252
|
+
// 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.
|
|
253
|
+
if (assistant && !options.model)
|
|
254
|
+
scripted(ctx.get('model') as Parameters<typeof scripted>[0])?.teach(() => ({
|
|
255
|
+
text: ASSISTANT_NO_MODEL,
|
|
256
|
+
}))
|
|
257
|
+
return fibers
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
/**
|
|
261
|
+
* The app: `instance` first (where it lives, chosen at run time), then the manifest's rows in order. The app's `plugins/`
|
|
262
|
+
* folder is trusted to install at start, and is where the builder writes.
|
|
263
|
+
*/
|
|
264
|
+
export async function createApp(
|
|
265
|
+
chosen: AppInstance,
|
|
266
|
+
options: {
|
|
267
|
+
values?: Readonly<Record<string, ParameterValue>>
|
|
268
|
+
/** A real model (any OpenAI-compatible API) in place of the offline scripted one. */
|
|
269
|
+
model?: OpenAIConfig
|
|
270
|
+
/** 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. */
|
|
271
|
+
listen?: boolean
|
|
272
|
+
} = {},
|
|
273
|
+
): Promise<App> {
|
|
274
|
+
const root = new Context()
|
|
275
|
+
const first = root.plugin(instance, chosen)
|
|
276
|
+
await first.await()
|
|
277
|
+
const fibers: Fiber[] = [first, ...(await composeInto(root, chosen, options))]
|
|
161
278
|
return {
|
|
162
279
|
root,
|
|
163
|
-
url: listen ? root.nodeServer.url
|
|
280
|
+
url: options.listen === false ? '' : root.nodeServer.url,
|
|
164
281
|
async stop() {
|
|
165
282
|
for (const fiber of [...fibers].reverse()) await fiber.dispose()
|
|
166
283
|
},
|
|
@@ -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
|
|
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 =
|
|
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
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
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}${
|
|
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
|
-
|
|
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
|
}
|
|
@@ -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
|
-
|
|
50
|
-
|
|
51
|
-
|
|
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
|
+
}))
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
|
|
2
2
|
## This app (blank)
|
|
3
3
|
|
|
4
|
-
`src/routes.ts`
|
|
5
|
-
|
|
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
|
|
@@ -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
|
},
|