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.
- package/README.md +9 -0
- package/dist/index.d.ts +47 -2
- package/dist/index.js +2017 -32
- 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 +5 -3
- package/docs/framework/workshop/README.md +139 -153
- 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 +87 -9
- package/lib/cordis/door.js +324 -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/plugin.json +3 -0
- package/plugins/acryl-door/src/cli/acryl.ts +63 -6
- 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 +128 -24
- package/plugins/acryl-door/src/host/operator-routes.ts +21 -1
- 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 +14 -1
- package/plugins/acryl-door/src/index.ts +168 -30
- 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/builder/index.ts +13 -35
- package/plugins/builder/package.ts +186 -0
- package/plugins/builder/plugin.json +3 -1
- 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 +47 -18
- package/plugins/extensions/plugin-store.ts +27 -0
- package/plugins/extensions/plugin.json +1 -1
- package/plugins/extensions/resolve.ts +13 -0
- package/plugins/extensions/stage.ts +15 -5
- 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/plugin.json +1 -1
- package/plugins/manifest/snapshot.ts +275 -0
- package/plugins/openai/index.ts +64 -5
- package/plugins/openai/prompt.ts +23 -0
- package/plugins/plugin-share/README.md +51 -0
- package/plugins/plugin-share/index.ts +467 -0
- package/plugins/plugin-share/plugin.json +15 -0
- package/plugins/plugin-share/publish.ts +65 -0
- package/plugins/plugin-share/pull-check.ts +129 -0
- package/plugins/plugin-share/pulled-records.ts +70 -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/.gitattributes +2 -0
- package/templates/agent/AGENTS.md +1 -1
- package/templates/agent/blend.yaml +6 -0
- package/templates/agent/gitignore +1 -0
- package/templates/agent/package.json +1 -0
- package/templates/agent/src/app.ts +154 -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 +3 -0
- package/templates/agent/test/app.test.ts +45 -3
- package/templates/blank/.gitattributes +2 -0
- 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/.gitattributes +2 -0
- 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/.gitattributes +2 -0
- 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/.gitattributes +2 -0
- 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/.gitattributes +2 -0
- 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,83 @@
|
|
|
1
|
+
import type { Context } from '@deepseek-ai/cordis'
|
|
2
|
+
import { Acryl } from '../plugins/acryl-router/src/index.ts'
|
|
3
|
+
import { bodyLimit } from '../plugins/acryl-router/src/middleware/index.ts'
|
|
4
|
+
import type {} from '../plugins/agent/index.ts'
|
|
5
|
+
import type {} from '../plugins/extensions/index.ts'
|
|
6
|
+
import type {} from '../plugins/tasks/index.ts'
|
|
7
|
+
|
|
8
|
+
export const APP_NAME = '__APP_NAME__'
|
|
9
|
+
|
|
10
|
+
/** 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
|
+
const DOOR_TAG =
|
|
12
|
+
process.env.NODE_ENV !== 'production' && process.env.ACRYL_DOOR !== 'off'
|
|
13
|
+
? '<script type="module" src="/api/acryl-door/page.js"></script>'
|
|
14
|
+
: ''
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* The app's own routes on the router. Always: the page and `GET /health`. While the agent, the extensions and the task recorder run: `POST /ask` for the agent; `GET /extensions`
|
|
18
|
+
* (and `POST /extensions/approve` and `/deny`) and `GET /tasks` to observe and steer what it did (every run is a durable task owning one task per tool call); `POST /tasks/cancel`.
|
|
19
|
+
* What changes is also streamed: `GET /api/state/tasks/events` and `GET /api/state/extensions/events`. In production `src/app.ts` leaves the agent, builder and extensions rows
|
|
20
|
+
* out (unless `ACRYL_PRODUCTION_KEEP` names them), and then those routes are not mounted at all. The mounts are removed when the plugin is disposed.
|
|
21
|
+
*/
|
|
22
|
+
export const routes = {
|
|
23
|
+
name: 'routes',
|
|
24
|
+
inject: ['router'],
|
|
25
|
+
apply(ctx: Context) {
|
|
26
|
+
// A body here is a name, an id or a question; anything bigger is refused before it is read.
|
|
27
|
+
const json = async (c: {
|
|
28
|
+
req: { json(): Promise<unknown> }
|
|
29
|
+
}): Promise<Record<string, unknown>> => {
|
|
30
|
+
const body = await c.req.json().catch(() => undefined)
|
|
31
|
+
return typeof body === 'object' && body !== null ? (body as Record<string, unknown>) : {}
|
|
32
|
+
}
|
|
33
|
+
const limit = bodyLimit({ maxSize: 64 * 1024 })
|
|
34
|
+
const base = new Acryl()
|
|
35
|
+
.use(limit)
|
|
36
|
+
// 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.
|
|
37
|
+
.get('/', (c) =>
|
|
38
|
+
(c.req.header('accept') ?? '').includes('text/html')
|
|
39
|
+
? c.html(
|
|
40
|
+
`<!doctype html><html lang="en"><head><meta charset="utf-8"><title>${APP_NAME}</title></head><body><h1>${APP_NAME}</h1><p>This app runs an agent unless it is switched off (production switches it off, see ACRYL_PRODUCTION_KEEP in AGENTS.md). Ask it for a feature (<code>POST /ask</code>), or add the chat panel with <code>npx acrylblends add agent-panel</code>.</p>${DOOR_TAG}</body></html>`,
|
|
41
|
+
)
|
|
42
|
+
: c.json({ app: APP_NAME, ok: true }),
|
|
43
|
+
)
|
|
44
|
+
.get('/health', (c) => c.json({ ok: true }))
|
|
45
|
+
ctx.router.route('/', base)
|
|
46
|
+
// The routes that give a visitor the agent: mounted only while the agent, the extensions and the tasks run (see the header).
|
|
47
|
+
ctx.inject(['agent', 'extensions', 'tasks'], (inner) => {
|
|
48
|
+
const app = new Acryl().use(limit).get('/extensions', (c) =>
|
|
49
|
+
c.json({
|
|
50
|
+
installed: inner.extensions.list(),
|
|
51
|
+
notInstalled: inner.extensions.pending(),
|
|
52
|
+
waitingForApproval: inner.extensions.approvals(),
|
|
53
|
+
}),
|
|
54
|
+
)
|
|
55
|
+
// The owner's decision on an install that is waiting (the `approval` parameter in blend.yaml).
|
|
56
|
+
for (const [path, decide] of [
|
|
57
|
+
['/extensions/approve', (name: string) => inner.extensions.approve(name)],
|
|
58
|
+
['/extensions/deny', (name: string) => inner.extensions.deny(name)],
|
|
59
|
+
] as const) {
|
|
60
|
+
app.post(path, async (c) => {
|
|
61
|
+
const { name } = await json(c)
|
|
62
|
+
if (typeof name !== 'string') return c.json({ error: 'name must be a string' }, 400)
|
|
63
|
+
return c.json(await decide(name))
|
|
64
|
+
})
|
|
65
|
+
}
|
|
66
|
+
app
|
|
67
|
+
.get('/tasks', (c) => c.json(inner.tasks.list().slice(0, 100)))
|
|
68
|
+
.post('/tasks/cancel', async (c) => {
|
|
69
|
+
const { id } = await json(c)
|
|
70
|
+
if (typeof id !== 'string' || !inner.tasks.get(id))
|
|
71
|
+
return c.json({ error: 'no such task' }, 404)
|
|
72
|
+
return c.json({ cancelled: inner.tasks.cancel(id) })
|
|
73
|
+
})
|
|
74
|
+
.post('/ask', async (c) => {
|
|
75
|
+
const { question } = await json(c)
|
|
76
|
+
if (typeof question !== 'string')
|
|
77
|
+
return c.json({ error: 'question must be a string' }, 400)
|
|
78
|
+
return c.json(await inner.agent.run(question))
|
|
79
|
+
})
|
|
80
|
+
inner.router.route('/', app)
|
|
81
|
+
})
|
|
82
|
+
},
|
|
83
|
+
}
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import { existsSync } from 'node:fs'
|
|
2
|
+
import { join } from 'node:path'
|
|
3
|
+
import { fileURLToPath } from 'node:url'
|
|
4
|
+
import { Context, type Fiber } from '@deepseek-ai/cordis'
|
|
5
|
+
import * as nodeServer from '../plugins/acryl-node-server/src/index.ts'
|
|
6
|
+
import * as router from '../plugins/acryl-router/src/index.ts'
|
|
7
|
+
import * as state from '../plugins/acryl-state/src/index.ts'
|
|
8
|
+
import { agent, memory, model, tools } from '../plugins/agent/index.ts'
|
|
9
|
+
import { builder } from '../plugins/builder/index.ts'
|
|
10
|
+
import { extensions } from '../plugins/extensions/index.ts'
|
|
11
|
+
import { type AppInstance, instance } from '../plugins/instance/index.ts'
|
|
12
|
+
import {
|
|
13
|
+
blueprintsIn,
|
|
14
|
+
compose,
|
|
15
|
+
loadManifest,
|
|
16
|
+
type ParameterValue,
|
|
17
|
+
} from '../plugins/manifest/index.ts'
|
|
18
|
+
import { type OpenAIConfig, openai } from '../plugins/openai/index.ts'
|
|
19
|
+
import { sandboxPlugin } from '../plugins/sandbox/index.ts'
|
|
20
|
+
import { save } from '../plugins/save/index.ts'
|
|
21
|
+
import { tasks } from '../plugins/tasks/index.ts'
|
|
22
|
+
import { routes } from './routes.ts'
|
|
23
|
+
|
|
24
|
+
/** The plugins this app's code provides. `blend.yaml` chooses, orders and configures them. */
|
|
25
|
+
export const registry = {
|
|
26
|
+
tasks,
|
|
27
|
+
model,
|
|
28
|
+
tools,
|
|
29
|
+
memory,
|
|
30
|
+
agent,
|
|
31
|
+
'acryl-router': router,
|
|
32
|
+
'acryl-state': state,
|
|
33
|
+
extensions,
|
|
34
|
+
builder,
|
|
35
|
+
save,
|
|
36
|
+
sandbox: sandboxPlugin,
|
|
37
|
+
routes,
|
|
38
|
+
'acryl-node-server': nodeServer,
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** The manifest next to this app's source. */
|
|
42
|
+
export const MANIFEST = join(fileURLToPath(new URL('..', import.meta.url)), 'blend.yaml')
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* The app's manifest, resolved. A Blend finds its Blueprint in `blueprints/<id>.yaml`; an app that is itself a Blueprint never looks.
|
|
46
|
+
* `createApp` and the tests read it the same way.
|
|
47
|
+
*/
|
|
48
|
+
export function readManifest(values?: Readonly<Record<string, ParameterValue>>) {
|
|
49
|
+
return loadManifest(MANIFEST, {
|
|
50
|
+
known: Object.keys(registry),
|
|
51
|
+
getBlueprint: blueprintsIn(join(fileURLToPath(new URL('..', import.meta.url)), 'blueprints')),
|
|
52
|
+
...(values ? { values } : {}),
|
|
53
|
+
})
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export interface App {
|
|
57
|
+
readonly root: Context
|
|
58
|
+
readonly url: string
|
|
59
|
+
stop(): Promise<void>
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* The app: `instance` first (where it lives, chosen at run time), then the manifest's rows in order. The app's `plugins/`
|
|
64
|
+
* folder is trusted to install at start, and is where the builder writes.
|
|
65
|
+
*/
|
|
66
|
+
export async function createApp(
|
|
67
|
+
chosen: AppInstance,
|
|
68
|
+
options: {
|
|
69
|
+
values?: Readonly<Record<string, ParameterValue>>
|
|
70
|
+
/** A real model (any OpenAI-compatible API) in place of the offline scripted one. */
|
|
71
|
+
model?: OpenAIConfig
|
|
72
|
+
} = {},
|
|
73
|
+
): Promise<App> {
|
|
74
|
+
const manifest = readManifest(options.values)
|
|
75
|
+
const extensionsDir = join(chosen.root, 'plugins')
|
|
76
|
+
// One release of grace: an app made before 0.7.8 kept what the agent wrote in `extensions/`. It is still installed, with a warning.
|
|
77
|
+
const legacyDir = join(chosen.root, 'extensions')
|
|
78
|
+
const legacy = existsSync(legacyDir)
|
|
79
|
+
if (legacy)
|
|
80
|
+
console.warn(
|
|
81
|
+
'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.',
|
|
82
|
+
)
|
|
83
|
+
const root = new Context()
|
|
84
|
+
const first = root.plugin(instance, chosen)
|
|
85
|
+
await first.await()
|
|
86
|
+
const fibers: Fiber[] = [
|
|
87
|
+
first,
|
|
88
|
+
// The `model` row mounts the scripted model, or the real one when a model is configured.
|
|
89
|
+
...(await compose(root, manifest, options.model ? { ...registry, model: openai } : registry, {
|
|
90
|
+
...(options.model ? { model: { ...options.model } } : {}),
|
|
91
|
+
// Runtime values the manifest cannot hold: the manifest's own digest (every task records it), and who may install.
|
|
92
|
+
tasks: { manifest: `sha256:${manifest.digest}` },
|
|
93
|
+
// The `approval` parameter in blend.yaml (the extensions row) turns this source into one that asks.
|
|
94
|
+
extensions: {
|
|
95
|
+
sources: [
|
|
96
|
+
{ dir: extensionsDir, trust: 'install' },
|
|
97
|
+
...(legacy ? [{ dir: legacyDir, trust: 'install' as const }] : []),
|
|
98
|
+
],
|
|
99
|
+
},
|
|
100
|
+
builder: { dir: extensionsDir },
|
|
101
|
+
server: { port: chosen.port.start, scan: chosen.port.scan ? 20 : 0, hostname: chosen.host },
|
|
102
|
+
})),
|
|
103
|
+
]
|
|
104
|
+
return {
|
|
105
|
+
root,
|
|
106
|
+
url: root.nodeServer.url,
|
|
107
|
+
async stop() {
|
|
108
|
+
for (const fiber of [...fibers].reverse()) await fiber.dispose()
|
|
109
|
+
},
|
|
110
|
+
}
|
|
111
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import type { Context } from '@deepseek-ai/cordis'
|
|
2
|
+
import { Acryl } from '../plugins/acryl-router/src/index.ts'
|
|
3
|
+
import { bodyLimit } from '../plugins/acryl-router/src/middleware/index.ts'
|
|
4
|
+
import type {} from '../plugins/agent/index.ts'
|
|
5
|
+
import type {} from '../plugins/extensions/index.ts'
|
|
6
|
+
import type {} from '../plugins/tasks/index.ts'
|
|
7
|
+
|
|
8
|
+
export const APP_NAME = '__APP_NAME__'
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* The app's own routes on the router: `POST /ask` for the agent; `GET /extensions` and `GET /tasks` to observe what it did (every run is a
|
|
12
|
+
* durable task owning one task per tool call); `POST /tasks/cancel`. What changes is also streamed: `GET /api/state/tasks/events` and
|
|
13
|
+
* `GET /api/state/extensions/events`. The mount is removed when the plugin is disposed.
|
|
14
|
+
*/
|
|
15
|
+
export const routes = {
|
|
16
|
+
name: 'routes',
|
|
17
|
+
inject: ['router', 'agent', 'extensions', 'tasks'],
|
|
18
|
+
apply(ctx: Context) {
|
|
19
|
+
// A body here is a name, an id or a question; anything bigger is refused before it is read.
|
|
20
|
+
const json = async (c: {
|
|
21
|
+
req: { json(): Promise<unknown> }
|
|
22
|
+
}): Promise<Record<string, unknown>> => {
|
|
23
|
+
const body = await c.req.json().catch(() => undefined)
|
|
24
|
+
return typeof body === 'object' && body !== null ? (body as Record<string, unknown>) : {}
|
|
25
|
+
}
|
|
26
|
+
const app = new Acryl()
|
|
27
|
+
.use(bodyLimit({ maxSize: 64 * 1024 }))
|
|
28
|
+
.get('/', (c) => c.json({ app: APP_NAME, ok: true }))
|
|
29
|
+
.get('/health', (c) => c.json({ ok: true }))
|
|
30
|
+
.get('/extensions', (c) =>
|
|
31
|
+
c.json({
|
|
32
|
+
installed: ctx.extensions.list(),
|
|
33
|
+
notInstalled: ctx.extensions.pending(),
|
|
34
|
+
waitingForApproval: ctx.extensions.approvals(),
|
|
35
|
+
}),
|
|
36
|
+
)
|
|
37
|
+
// The owner's decision on an install that is waiting (the `approval` parameter in blend.yaml).
|
|
38
|
+
for (const [path, decide] of [
|
|
39
|
+
['/extensions/approve', (name: string) => ctx.extensions.approve(name)],
|
|
40
|
+
['/extensions/deny', (name: string) => ctx.extensions.deny(name)],
|
|
41
|
+
] as const) {
|
|
42
|
+
app.post(path, async (c) => {
|
|
43
|
+
const { name } = await json(c)
|
|
44
|
+
if (typeof name !== 'string') return c.json({ error: 'name must be a string' }, 400)
|
|
45
|
+
return c.json(await decide(name))
|
|
46
|
+
})
|
|
47
|
+
}
|
|
48
|
+
app
|
|
49
|
+
.get('/tasks', (c) => c.json(ctx.tasks.list().slice(0, 100)))
|
|
50
|
+
.post('/tasks/cancel', async (c) => {
|
|
51
|
+
const { id } = await json(c)
|
|
52
|
+
if (typeof id !== 'string' || !ctx.tasks.get(id))
|
|
53
|
+
return c.json({ error: 'no such task' }, 404)
|
|
54
|
+
return c.json({ cancelled: ctx.tasks.cancel(id) })
|
|
55
|
+
})
|
|
56
|
+
.post('/ask', async (c) => {
|
|
57
|
+
const { question } = await json(c)
|
|
58
|
+
if (typeof question !== 'string') return c.json({ error: 'question must be a string' }, 400)
|
|
59
|
+
return c.json(await ctx.agent.run(question))
|
|
60
|
+
})
|
|
61
|
+
ctx.router.route('/', app)
|
|
62
|
+
},
|
|
63
|
+
}
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
import { existsSync } from 'node:fs'
|
|
2
|
+
import { join } from 'node:path'
|
|
3
|
+
import { fileURLToPath } from 'node:url'
|
|
4
|
+
import { Context, type Fiber } from '@deepseek-ai/cordis'
|
|
5
|
+
import * as door from '../plugins/acryl-door/src/index.ts'
|
|
6
|
+
import * as nodeServer from '../plugins/acryl-node-server/src/index.ts'
|
|
7
|
+
import * as router from '../plugins/acryl-router/src/index.ts'
|
|
8
|
+
import * as state from '../plugins/acryl-state/src/index.ts'
|
|
9
|
+
import { agent, memory, model, tools } from '../plugins/agent/index.ts'
|
|
10
|
+
import * as panel from '../plugins/agent-panel/index.ts'
|
|
11
|
+
import { builder } from '../plugins/builder/index.ts'
|
|
12
|
+
import { extensions } from '../plugins/extensions/index.ts'
|
|
13
|
+
import { type AppInstance, instance } from '../plugins/instance/index.ts'
|
|
14
|
+
import {
|
|
15
|
+
blueprintsIn,
|
|
16
|
+
compose,
|
|
17
|
+
loadManifest,
|
|
18
|
+
type ParameterValue,
|
|
19
|
+
} from '../plugins/manifest/index.ts'
|
|
20
|
+
import { type OpenAIConfig, openai } from '../plugins/openai/index.ts'
|
|
21
|
+
import { sandboxPlugin } from '../plugins/sandbox/index.ts'
|
|
22
|
+
import { save } from '../plugins/save/index.ts'
|
|
23
|
+
import { tasks } from '../plugins/tasks/index.ts'
|
|
24
|
+
import { routes } from './routes.ts'
|
|
25
|
+
|
|
26
|
+
/** The plugins this app's code provides. `blend.yaml` chooses, orders and configures them. */
|
|
27
|
+
export const registry = {
|
|
28
|
+
tasks,
|
|
29
|
+
model,
|
|
30
|
+
tools,
|
|
31
|
+
memory,
|
|
32
|
+
agent,
|
|
33
|
+
'acryl-router': router,
|
|
34
|
+
'acryl-state': state,
|
|
35
|
+
extensions,
|
|
36
|
+
builder,
|
|
37
|
+
save,
|
|
38
|
+
sandbox: sandboxPlugin,
|
|
39
|
+
routes,
|
|
40
|
+
'acryl-node-server': nodeServer,
|
|
41
|
+
'agent-panel': panel,
|
|
42
|
+
'acryl-door': door,
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** The manifest next to this app's source. */
|
|
46
|
+
export const MANIFEST = join(fileURLToPath(new URL('..', import.meta.url)), 'blend.yaml')
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* The app's manifest, resolved. A Blend finds its Blueprint in `blueprints/<id>.yaml`; an app that is itself a Blueprint never looks.
|
|
50
|
+
* `createApp` and the tests read it the same way.
|
|
51
|
+
*/
|
|
52
|
+
export function readManifest(values?: Readonly<Record<string, ParameterValue>>) {
|
|
53
|
+
return loadManifest(MANIFEST, {
|
|
54
|
+
known: Object.keys(registry),
|
|
55
|
+
getBlueprint: blueprintsIn(join(fileURLToPath(new URL('..', import.meta.url)), 'blueprints')),
|
|
56
|
+
...(values ? { values } : {}),
|
|
57
|
+
})
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export interface App {
|
|
61
|
+
readonly root: Context
|
|
62
|
+
/** The address the server listens on; empty when the app was made with `listen: false`. */
|
|
63
|
+
readonly url: string
|
|
64
|
+
stop(): Promise<void>
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* The development rows, and what each one gives a visitor. Production leaves the agent and the builder out. The extensions row is not left out: it starts again, read-only,
|
|
69
|
+
* the extensions in the app's `plugins/` folder (what the agent built and installed in development, committed with the app), so a deployed app has its own features; nothing
|
|
70
|
+
* can be installed, updated or approved while it runs, and `/ask`, `/extensions*` and `/tasks*` stay unmounted (they need the agent). `ACRYL_PRODUCTION_KEEP=extensions` keeps the
|
|
71
|
+
* live install itself.
|
|
72
|
+
*/
|
|
73
|
+
const DEVELOPMENT_ROWS: Record<string, string> = {
|
|
74
|
+
agent: 'POST /ask runs the agent, which spends the model key and calls its tools',
|
|
75
|
+
builder: 'the agent can write plugin source into plugins/',
|
|
76
|
+
extensions:
|
|
77
|
+
'a plugin installed live runs in this process, and POST /extensions/approve decides installs',
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
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.
|
|
83
|
+
*/
|
|
84
|
+
export async function createApp(
|
|
85
|
+
chosen: AppInstance,
|
|
86
|
+
options: {
|
|
87
|
+
values?: Readonly<Record<string, ParameterValue>>
|
|
88
|
+
/** A real model (any OpenAI-compatible API) in place of the offline scripted one. */
|
|
89
|
+
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
|
+
listen?: boolean
|
|
92
|
+
} = {},
|
|
93
|
+
): Promise<App> {
|
|
94
|
+
const read = readManifest(options.values)
|
|
95
|
+
// 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
|
+
// serves a folder's door, so the tests that start several apps set it).
|
|
97
|
+
const listen = options.listen !== false
|
|
98
|
+
const leaveOut = new Set<string>()
|
|
99
|
+
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')
|
|
102
|
+
leaveOut.add('acryl-door')
|
|
103
|
+
// 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
|
+
// 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`
|
|
105
|
+
// names the ones to keep (`agent,builder,extensions`); each one kept is announced at start, because none of them is behind a login.
|
|
106
|
+
if (process.env.NODE_ENV === 'production') {
|
|
107
|
+
const keep = new Set(
|
|
108
|
+
(process.env.ACRYL_PRODUCTION_KEEP ?? '')
|
|
109
|
+
.split(',')
|
|
110
|
+
.map((name) => name.trim())
|
|
111
|
+
.filter((name) => name !== ''),
|
|
112
|
+
)
|
|
113
|
+
for (const name of keep)
|
|
114
|
+
if (!(name in DEVELOPMENT_ROWS))
|
|
115
|
+
console.warn(
|
|
116
|
+
`WARNING: ACRYL_PRODUCTION_KEEP names "${name}", which is not one of ${Object.keys(DEVELOPMENT_ROWS).join(', ')}`,
|
|
117
|
+
)
|
|
118
|
+
for (const [row, danger] of Object.entries(DEVELOPMENT_ROWS)) {
|
|
119
|
+
if (keep.has(row))
|
|
120
|
+
console.warn(
|
|
121
|
+
`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
|
+
)
|
|
123
|
+
else if (row === 'extensions') extensionsReadOnly = true
|
|
124
|
+
else leaveOut.add(row)
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
const manifest = {
|
|
128
|
+
...read,
|
|
129
|
+
plugins: read.plugins.map((row) => (leaveOut.has(row.use) ? { ...row, disabled: true } : row)),
|
|
130
|
+
}
|
|
131
|
+
const extensionsDir = join(chosen.root, 'plugins')
|
|
132
|
+
// One release of grace: an app made before 0.7.8 kept what the agent wrote in `extensions/`. It is still installed, with a warning.
|
|
133
|
+
const legacyDir = join(chosen.root, 'extensions')
|
|
134
|
+
const legacy = existsSync(legacyDir)
|
|
135
|
+
if (legacy)
|
|
136
|
+
console.warn(
|
|
137
|
+
'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
|
+
)
|
|
139
|
+
const root = new Context()
|
|
140
|
+
const first = root.plugin(instance, chosen)
|
|
141
|
+
await first.await()
|
|
142
|
+
const fibers: Fiber[] = [
|
|
143
|
+
first,
|
|
144
|
+
// 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
|
+
],
|
|
156
|
+
},
|
|
157
|
+
builder: { dir: extensionsDir },
|
|
158
|
+
server: { port: chosen.port.start, scan: chosen.port.scan ? 20 : 0, hostname: chosen.host },
|
|
159
|
+
})),
|
|
160
|
+
]
|
|
161
|
+
return {
|
|
162
|
+
root,
|
|
163
|
+
url: listen ? root.nodeServer.url : '',
|
|
164
|
+
async stop() {
|
|
165
|
+
for (const fiber of [...fibers].reverse()) await fiber.dispose()
|
|
166
|
+
},
|
|
167
|
+
}
|
|
168
|
+
}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import type { Context } from '@deepseek-ai/cordis'
|
|
2
|
+
import { Acryl } from '../plugins/acryl-router/src/index.ts'
|
|
3
|
+
import { bodyLimit } from '../plugins/acryl-router/src/middleware/index.ts'
|
|
4
|
+
import type {} from '../plugins/agent/index.ts'
|
|
5
|
+
import type {} from '../plugins/extensions/index.ts'
|
|
6
|
+
import type {} from '../plugins/tasks/index.ts'
|
|
7
|
+
|
|
8
|
+
export const APP_NAME = '__APP_NAME__'
|
|
9
|
+
|
|
10
|
+
/** 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
|
+
const DOOR_TAG =
|
|
12
|
+
process.env.NODE_ENV !== 'production' && process.env.ACRYL_DOOR !== 'off'
|
|
13
|
+
? '<script type="module" src="/api/acryl-door/page.js"></script>'
|
|
14
|
+
: ''
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* The app's own routes on the router. Always: the page and `GET /health`. While the agent, the extensions and the task recorder run: `POST /ask` for the agent; `GET /extensions`
|
|
18
|
+
* (and `POST /extensions/approve` and `/deny`) and `GET /tasks` to observe and steer what it did (every run is a durable task owning one task per tool call); `POST /tasks/cancel`.
|
|
19
|
+
* What changes is also streamed: `GET /api/state/tasks/events` and `GET /api/state/extensions/events`. In production `src/app.ts` leaves the agent and builder rows out
|
|
20
|
+
* and starts the extensions read-only (unless `ACRYL_PRODUCTION_KEEP` names them), and then those routes are not mounted at all. The mounts are removed when the plugin is disposed.
|
|
21
|
+
*/
|
|
22
|
+
/** The page's look: the person's choice from /settings (kept in this browser), applied before the page paints. */
|
|
23
|
+
const APPEARANCE_STYLE =
|
|
24
|
+
'<style>:root{color-scheme:light dark}html[data-theme=dark]{color-scheme:dark;background:#111827;color:#f9fafb}html[data-theme=light]{color-scheme:light;background:#fff;color:#111827}body{font:16px system-ui,sans-serif;margin:2rem auto;max-width:42rem;padding:0 1rem}a{color:inherit}</style>'
|
|
25
|
+
const APPEARANCE_SCRIPT =
|
|
26
|
+
"<script>(function(){try{var t=localStorage.getItem('theme');if(t==='dark'||t==='light')document.documentElement.dataset.theme=t}catch(e){}})()</script>"
|
|
27
|
+
|
|
28
|
+
/** `/settings`: the one setting the template has, the appearance. The desktop app's Settings menu item opens this page. */
|
|
29
|
+
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>`
|
|
30
|
+
|
|
31
|
+
export const routes = {
|
|
32
|
+
name: 'routes',
|
|
33
|
+
inject: ['router'],
|
|
34
|
+
apply(ctx: Context) {
|
|
35
|
+
// A body here is a name, an id or a question; anything bigger is refused before it is read.
|
|
36
|
+
const json = async (c: {
|
|
37
|
+
req: { json(): Promise<unknown> }
|
|
38
|
+
}): Promise<Record<string, unknown>> => {
|
|
39
|
+
const body = await c.req.json().catch(() => undefined)
|
|
40
|
+
return typeof body === 'object' && body !== null ? (body as Record<string, unknown>) : {}
|
|
41
|
+
}
|
|
42
|
+
const limit = bodyLimit({ maxSize: 64 * 1024 })
|
|
43
|
+
const base = new Acryl()
|
|
44
|
+
.use(limit)
|
|
45
|
+
// 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.
|
|
46
|
+
.get('/', (c) =>
|
|
47
|
+
(c.req.header('accept') ?? '').includes('text/html')
|
|
48
|
+
? c.html(
|
|
49
|
+
`<!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>`,
|
|
50
|
+
)
|
|
51
|
+
: c.json({ app: APP_NAME, ok: true }),
|
|
52
|
+
)
|
|
53
|
+
.get('/health', (c) => c.json({ ok: true }))
|
|
54
|
+
.get('/settings', (c) => c.html(settingsPage))
|
|
55
|
+
ctx.router.route('/', base)
|
|
56
|
+
// The routes that give a visitor the agent: mounted only while the agent, the extensions and the tasks run (see the header).
|
|
57
|
+
ctx.inject(['agent', 'extensions', 'tasks'], (inner) => {
|
|
58
|
+
const app = new Acryl().use(limit).get('/extensions', (c) =>
|
|
59
|
+
c.json({
|
|
60
|
+
installed: inner.extensions.list(),
|
|
61
|
+
notInstalled: inner.extensions.pending(),
|
|
62
|
+
waitingForApproval: inner.extensions.approvals(),
|
|
63
|
+
}),
|
|
64
|
+
)
|
|
65
|
+
// The owner's decision on an install that is waiting (the `approval` parameter in blend.yaml).
|
|
66
|
+
for (const [path, decide] of [
|
|
67
|
+
['/extensions/approve', (name: string) => inner.extensions.approve(name)],
|
|
68
|
+
['/extensions/deny', (name: string) => inner.extensions.deny(name)],
|
|
69
|
+
] as const) {
|
|
70
|
+
app.post(path, async (c) => {
|
|
71
|
+
const { name } = await json(c)
|
|
72
|
+
if (typeof name !== 'string') return c.json({ error: 'name must be a string' }, 400)
|
|
73
|
+
return c.json(await decide(name))
|
|
74
|
+
})
|
|
75
|
+
}
|
|
76
|
+
app
|
|
77
|
+
.get('/tasks', (c) => c.json(inner.tasks.list().slice(0, 100)))
|
|
78
|
+
.post('/tasks/cancel', async (c) => {
|
|
79
|
+
const { id } = await json(c)
|
|
80
|
+
if (typeof id !== 'string' || !inner.tasks.get(id))
|
|
81
|
+
return c.json({ error: 'no such task' }, 404)
|
|
82
|
+
return c.json({ cancelled: inner.tasks.cancel(id) })
|
|
83
|
+
})
|
|
84
|
+
.post('/ask', async (c) => {
|
|
85
|
+
const { question } = await json(c)
|
|
86
|
+
if (typeof question !== 'string')
|
|
87
|
+
return c.json({ error: 'question must be a string' }, 400)
|
|
88
|
+
return c.json(await inner.agent.run(question))
|
|
89
|
+
})
|
|
90
|
+
inner.router.route('/', app)
|
|
91
|
+
})
|
|
92
|
+
},
|
|
93
|
+
}
|