acrylblends 0.7.6 → 0.7.8
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/dist/index.d.ts +1 -1
- package/dist/index.js +148 -24
- package/dist/index.js.map +1 -1
- package/docs/framework/README.md +1 -1
- package/docs/framework/UPSTREAM.md +1 -1
- package/docs/framework/agents/index.md +18 -3
- package/docs/framework/getting-started/desktop.md +2 -2
- package/docs/framework/guides/client-modules.md +4 -3
- package/docs/framework/guides/data.md +72 -8
- package/docs/framework/helpers/streaming.md +12 -27
- package/docs/framework/upstream.json +2 -2
- package/lib/browser/read-sse.js +84 -0
- package/lib/index-BLTYsyc1.d.ts +93 -0
- package/lib/kv/db.d.ts +12 -0
- package/lib/kv/db.js +57 -0
- package/lib/kv/json.d.ts +11 -0
- package/lib/kv/json.js +224 -0
- package/lib/kv.d.ts +2 -0
- package/lib/kv.js +2 -0
- package/lib/node-server.d.ts +3 -3
- package/lib/node-server.js +13 -8
- package/lib/read-sse.d.ts +33 -0
- package/lib/read-sse.js +84 -0
- package/lib/src-DRFVEQQb.js +90 -0
- package/package.json +17 -1
- package/plugins/acryl-db/plugin.json +1 -1
- package/plugins/acryl-db/upstream.json +2 -2
- package/plugins/acryl-db-sqlite/upstream.json +1 -1
- package/plugins/acryl-inspect/upstream.json +1 -1
- package/plugins/acryl-kv/LICENSE +47 -0
- package/plugins/acryl-kv/plugin.json +12 -0
- package/plugins/acryl-kv/src/check.ts +58 -0
- package/plugins/acryl-kv/src/copy.ts +39 -0
- package/plugins/acryl-kv/src/cordis.ts +13 -0
- package/plugins/acryl-kv/src/index.ts +14 -0
- package/plugins/acryl-kv/src/kv.ts +37 -0
- package/plugins/acryl-kv/upstream.json +18 -0
- package/plugins/acryl-kv-db/LICENSE +47 -0
- package/plugins/acryl-kv-db/plugin.json +13 -0
- package/plugins/acryl-kv-db/src/index.ts +37 -0
- package/plugins/acryl-kv-db/src/open.ts +80 -0
- package/plugins/acryl-kv-db/upstream.json +21 -0
- package/plugins/acryl-kv-json/LICENSE +47 -0
- package/plugins/acryl-kv-json/plugin.json +13 -0
- package/plugins/acryl-kv-json/src/index.ts +71 -0
- package/plugins/acryl-kv-json/src/open.ts +249 -0
- package/plugins/acryl-kv-json/upstream.json +22 -0
- package/plugins/acryl-node-server/src/client-modules.ts +10 -9
- package/plugins/acryl-node-server/src/index.ts +12 -1
- package/plugins/acryl-node-server/upstream.json +2 -2
- package/plugins/acryl-router/src/browser/read-sse.ts +3 -0
- package/plugins/acryl-router/src/helpers/read-sse.ts +111 -0
- package/plugins/acryl-router/upstream.json +2 -2
- package/plugins/acryl-router-http/upstream.json +1 -1
- package/plugins/acryl-state/upstream.json +1 -1
- package/plugins/builder/index.ts +2 -2
- package/plugins/builder/plugin.json +1 -1
- package/plugins/desktop/main.ts +3 -3
- package/plugins/desktop/plugin.json +2 -2
- package/plugins/desktop/upstream.json +1 -1
- package/plugins/desktop-web/main.ts +2 -2
- package/plugins/desktop-web/plugin.json +2 -2
- package/plugins/desktop-web/upstream.json +1 -1
- package/plugins/extensions/index.ts +19 -1
- package/plugins/extensions/plugin.json +1 -1
- package/plugins/extensions/resolve.ts +1 -1
- package/plugins/manifest/lock.ts +7 -3
- package/plugins/manifest/upgrade-cli.ts +1 -1
- package/plugins/save/cli.ts +1 -1
- package/plugins/save/plugin.json +1 -1
- package/plugins/tsconfig.json +2 -0
- package/shared/AGENTS.core.md +10 -10
- package/shared/DEFAULTS.md +40 -0
- package/shared/SKILL.md +14 -0
- package/templates/agent/AGENTS.md +1 -1
- package/templates/agent/README.md +6 -6
- package/templates/agent/gitignore +1 -0
- package/templates/agent/package.json +8 -8
- package/templates/agent/src/app.ts +28 -15
- package/templates/agent/src/main.ts +2 -2
- package/templates/agent/src/routes.ts +5 -5
- package/templates/agent/test/agent.test.ts +1 -1
- package/templates/agent/test/app.test.ts +1 -1
- package/templates/agent/test/builder.test.ts +1 -1
- package/templates/agent/test/model.test.ts +1 -1
- package/templates/agent/tsconfig.json +48 -50
- package/templates/blank/AGENTS.md +1 -1
- package/templates/blank/README.md +2 -2
- package/templates/blank/gitignore +1 -0
- package/templates/blank/package.json +8 -8
- package/templates/blank/src/about.tsx +2 -2
- package/templates/blank/src/app.ts +4 -4
- package/templates/blank/src/main.ts +1 -1
- package/templates/blank/src/routes.ts +1 -1
- package/templates/blank/test/app.test.ts +1 -1
- package/templates/blank/tsconfig.json +1 -1
- package/templates/todo/README.md +1 -1
- package/templates/todo/gitignore +1 -0
- package/templates/todo/package.json +8 -8
- package/templates/todo/src/app.ts +5 -5
- package/templates/todo/src/main.ts +1 -1
- package/templates/todo/src/todo.ts +4 -4
- package/templates/todo/test/todo.test.ts +1 -1
- package/templates/todo/tsconfig.json +1 -1
- package/templates/todo/upstream.json +2 -2
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `readSSE(response)`: the events of a `text/event-stream` answer from `fetch`, for the browser. `EventSource` reads GET streams only; a chat
|
|
3
|
+
* or any stream that needs a POST body, a header or an `AbortSignal` is read with `fetch`, and this is the loop every such page otherwise writes by
|
|
4
|
+
* hand. It follows the parsing rules of the HTML standard (lines end in LF, CRLF or CR; the `data` lines of one event join with LF; a line that
|
|
5
|
+
* starts with a colon is a comment; an event ends at a blank line; an event with no `data` line is not dispatched; the last event is dispatched
|
|
6
|
+
* only if its blank line arrived), and it is one module with no imports, so it also runs in a service worker and on a fetch runtime.
|
|
7
|
+
*
|
|
8
|
+
* ACRYL's own addition, not part of the upstream port.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
/** One event of the stream. */
|
|
12
|
+
export interface SSEEvent {
|
|
13
|
+
/** The `event:` name; `message` when the event has none. */
|
|
14
|
+
readonly event: string
|
|
15
|
+
/** The `data:` lines of the event, joined with a line feed. */
|
|
16
|
+
readonly data: string
|
|
17
|
+
/** The `id:` of the event, when it had one. */
|
|
18
|
+
readonly id?: string
|
|
19
|
+
/** The `retry:` of the event in milliseconds, when it had one. */
|
|
20
|
+
readonly retry?: number
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export interface ReadSSEOptions {
|
|
24
|
+
/** Stops the loop and cancels the response body when it aborts. (Abort the `fetch` itself with the signal you pass to it.) */
|
|
25
|
+
readonly signal?: AbortSignal
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Read the events of a `text/event-stream` response as they arrive. Leaving the loop early (`break`, a thrown error) cancels the body, so the
|
|
30
|
+
* request to the server ends.
|
|
31
|
+
* @throws when the response is not `ok` (the status is in the message; the body is cancelled, not parsed), has no body, or fails while it is read.
|
|
32
|
+
*/
|
|
33
|
+
export async function* readSSE(response: Response, options: ReadSSEOptions = {}): AsyncGenerator<SSEEvent, void, undefined> {
|
|
34
|
+
if (!response.ok) {
|
|
35
|
+
await response.body?.cancel().catch(() => undefined)
|
|
36
|
+
throw new Error(`readSSE: the request failed with ${String(response.status)}${response.statusText === '' ? '' : ` ${response.statusText}`}`)
|
|
37
|
+
}
|
|
38
|
+
if (response.body === null) throw new Error('readSSE: the response has no body to read')
|
|
39
|
+
const { signal } = options
|
|
40
|
+
if (signal?.aborted === true) {
|
|
41
|
+
await response.body.cancel().catch(() => undefined)
|
|
42
|
+
return
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const reader = response.body.getReader()
|
|
46
|
+
const onAbort = (): void => {
|
|
47
|
+
void reader.cancel().catch(() => undefined)
|
|
48
|
+
}
|
|
49
|
+
signal?.addEventListener('abort', onAbort, { once: true })
|
|
50
|
+
const decoder = new TextDecoder('utf-8')
|
|
51
|
+
|
|
52
|
+
let buffer = ''
|
|
53
|
+
let skipLineFeed = false
|
|
54
|
+
let name = ''
|
|
55
|
+
let data: string[] = []
|
|
56
|
+
let id: string | undefined
|
|
57
|
+
let retry: number | undefined
|
|
58
|
+
|
|
59
|
+
/** Apply one line; returns the event when this line completes one. */
|
|
60
|
+
const apply = (line: string): SSEEvent | undefined => {
|
|
61
|
+
if (line === '') {
|
|
62
|
+
const event = data.length === 0 ? undefined : { event: name === '' ? 'message' : name, data: data.join('\n'), ...(id === undefined ? {} : { id }), ...(retry === undefined ? {} : { retry }) }
|
|
63
|
+
name = ''
|
|
64
|
+
data = []
|
|
65
|
+
id = undefined
|
|
66
|
+
retry = undefined
|
|
67
|
+
return event
|
|
68
|
+
}
|
|
69
|
+
if (line.startsWith(':')) return undefined
|
|
70
|
+
const colon = line.indexOf(':')
|
|
71
|
+
const field = colon === -1 ? line : line.slice(0, colon)
|
|
72
|
+
let value = colon === -1 ? '' : line.slice(colon + 1)
|
|
73
|
+
if (value.startsWith(' ')) value = value.slice(1)
|
|
74
|
+
if (field === 'event') name = value
|
|
75
|
+
else if (field === 'data') data.push(value)
|
|
76
|
+
else if (field === 'id') {
|
|
77
|
+
if (!value.includes('\u0000')) id = value
|
|
78
|
+
} else if (field === 'retry' && /^\d+$/u.test(value)) retry = Number(value)
|
|
79
|
+
return undefined
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
try {
|
|
83
|
+
for (;;) {
|
|
84
|
+
const { done, value } = await reader.read()
|
|
85
|
+
if (done) break
|
|
86
|
+
let text = decoder.decode(value, { stream: true })
|
|
87
|
+
// A CR at the end of the last chunk already ended its line; the LF that completes the CRLF may open this one.
|
|
88
|
+
if (skipLineFeed && text.startsWith('\n')) text = text.slice(1)
|
|
89
|
+
if (text !== '') skipLineFeed = false
|
|
90
|
+
buffer += text
|
|
91
|
+
let start = 0
|
|
92
|
+
for (let index = 0; index < buffer.length; index += 1) {
|
|
93
|
+
const code = buffer.charCodeAt(index)
|
|
94
|
+
if (code !== 10 && code !== 13) continue
|
|
95
|
+
const line = buffer.slice(start, index)
|
|
96
|
+
if (code === 13) {
|
|
97
|
+
if (index + 1 < buffer.length) {
|
|
98
|
+
if (buffer.charCodeAt(index + 1) === 10) index += 1
|
|
99
|
+
} else skipLineFeed = true
|
|
100
|
+
}
|
|
101
|
+
start = index + 1
|
|
102
|
+
const event = apply(line)
|
|
103
|
+
if (event !== undefined) yield event
|
|
104
|
+
}
|
|
105
|
+
buffer = buffer.slice(start)
|
|
106
|
+
}
|
|
107
|
+
} finally {
|
|
108
|
+
signal?.removeEventListener('abort', onAbort)
|
|
109
|
+
await reader.cancel().catch(() => undefined)
|
|
110
|
+
}
|
|
111
|
+
}
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"source": {
|
|
3
3
|
"repo": "acryldev/acryl",
|
|
4
4
|
"path": "plugins/acryl-router",
|
|
5
|
-
"commit": "
|
|
5
|
+
"commit": "f132283c43337f5e1d58dfbbbc15786efb5e756c"
|
|
6
6
|
},
|
|
7
7
|
"vendored": [
|
|
8
8
|
"src",
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
"LICENSE"
|
|
11
11
|
],
|
|
12
12
|
"rewrittenSpecifiers": 6,
|
|
13
|
-
"digest": "sha256:
|
|
13
|
+
"digest": "sha256:5f07e873992fa2f0a178e029a1fe3d8c99cb4ce3ae8c07445d75431f95125417",
|
|
14
14
|
"testsNotRun": {
|
|
15
15
|
"tests/loader.spec.ts": "loads the built lib/ entry through the real Loader; the library ships source and has no build"
|
|
16
16
|
},
|
package/plugins/builder/index.ts
CHANGED
|
@@ -14,7 +14,7 @@ import type {} from '../instance/index.ts'
|
|
|
14
14
|
import { builderRules } from './script.ts'
|
|
15
15
|
|
|
16
16
|
export interface BuilderConfig {
|
|
17
|
-
/** Where the agent writes extensions. Defaults to `<project>/
|
|
17
|
+
/** Where the agent writes extensions. Defaults to `<project>/plugins`. Must be one of the extensions sources. */
|
|
18
18
|
dir?: string
|
|
19
19
|
/** Where `package_plugin` writes standalone packages. Defaults to `<project>/plugin-packages`. */
|
|
20
20
|
packagesDir?: string
|
|
@@ -64,7 +64,7 @@ export const builder = {
|
|
|
64
64
|
name: 'builder',
|
|
65
65
|
inject: ['extensions', 'tools', 'appInstance'],
|
|
66
66
|
apply(ctx: Context, config: BuilderConfig) {
|
|
67
|
-
const dir = resolve(config?.dir ?? join(ctx.appInstance.root, '
|
|
67
|
+
const dir = resolve(config?.dir ?? join(ctx.appInstance.root, 'plugins'))
|
|
68
68
|
if (!ctx.extensions.sources.some((source) => resolve(source.dir) === dir)) {
|
|
69
69
|
throw new BuilderError(
|
|
70
70
|
`the builder writes into ${dir}, which is not an extensions source; add it to the extensions plugin's sources`,
|
|
@@ -7,5 +7,5 @@
|
|
|
7
7
|
"dependencies": {
|
|
8
8
|
"@deepseek-ai/cordis": "4.0.4"
|
|
9
9
|
},
|
|
10
|
-
"mount": "mount it after agent and extensions: root.plugin(builder, { dir: join(root, '
|
|
10
|
+
"mount": "mount it after agent and extensions: root.plugin(builder, { dir: join(root, 'plugins') }); dir must be an extensions source"
|
|
11
11
|
}
|
package/plugins/desktop/main.ts
CHANGED
|
@@ -10,14 +10,14 @@ import { mkdirSync, readFileSync } from 'node:fs'
|
|
|
10
10
|
import { join } from 'node:path'
|
|
11
11
|
import { fileURLToPath } from 'node:url'
|
|
12
12
|
import { app, BrowserWindow, shell } from 'electron'
|
|
13
|
-
import type { App } from '../../app.ts'
|
|
13
|
+
import type { App } from '../../src/app.ts'
|
|
14
14
|
import '../acryl-node-server/src/register.ts'
|
|
15
15
|
import { selectInstance } from '../instance/index.ts'
|
|
16
16
|
import { createDesktopShutdown, installShutdownRequests } from './shutdown.ts'
|
|
17
17
|
import { resolveDesktopUserDataOverride } from './user-data.ts'
|
|
18
18
|
import { windowOptions } from './window.ts'
|
|
19
19
|
|
|
20
|
-
const projectRoot = fileURLToPath(new URL('
|
|
20
|
+
const projectRoot = fileURLToPath(new URL('../..', import.meta.url))
|
|
21
21
|
const chosen = selectInstance({ projectRoot, env: process.env })
|
|
22
22
|
|
|
23
23
|
/** Where the window opens: `acrylblends.desktopPath` in package.json (the todo template sets `/todo`), else the root. */
|
|
@@ -65,7 +65,7 @@ if (!app.requestSingleInstanceLock()) {
|
|
|
65
65
|
|
|
66
66
|
void app.whenReady().then(async () => {
|
|
67
67
|
// The hook above is registered before the app's modules are loaded, so the app may have .tsx pages: they are loaded here, not as a static import.
|
|
68
|
-
const { createApp } = await import('../../app.ts')
|
|
68
|
+
const { createApp } = await import('../../src/app.ts')
|
|
69
69
|
running = await createApp(chosen)
|
|
70
70
|
const origin = new URL(running.url).origin
|
|
71
71
|
window = new BrowserWindow(windowOptions({ title: chosen.name }))
|
|
@@ -9,8 +9,8 @@
|
|
|
9
9
|
"electron": "44.9.3"
|
|
10
10
|
},
|
|
11
11
|
"scripts": {
|
|
12
|
-
"desktop": "electron
|
|
12
|
+
"desktop": "electron plugins/desktop/main.ts"
|
|
13
13
|
},
|
|
14
14
|
"omit": ["**/*.test.*"],
|
|
15
|
-
"mount": "nothing to mount: run `npm install`, then `npm run desktop` (the window opens on the app's own server; every electron call is in
|
|
15
|
+
"mount": "nothing to mount: run `npm install`, then `npm run desktop` (the window opens on the app's own server; every electron call is in plugins/desktop)"
|
|
16
16
|
}
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
"apps/acryl-desktop/src/shell/desktop-user-data.ts": "user-data.ts",
|
|
6
6
|
"apps/acryl-desktop/src/shell/shutdown.ts": "shutdown.ts"
|
|
7
7
|
},
|
|
8
|
-
"commit": "
|
|
8
|
+
"commit": "f132283c43337f5e1d58dfbbbc15786efb5e756c"
|
|
9
9
|
},
|
|
10
10
|
"rewrittenSpecifiers": 0,
|
|
11
11
|
"digest": "sha256:7dfe960a8bbb476c35540d2202a7aaa62b33492c2ac3e34314e2ecfca45f32f2"
|
|
@@ -17,7 +17,7 @@ import { createDesktopShutdown, installShutdownRequests } from './shutdown.ts'
|
|
|
17
17
|
import { resolveDesktopUserDataOverride } from './user-data.ts'
|
|
18
18
|
import { windowOptions } from './window.ts'
|
|
19
19
|
|
|
20
|
-
const projectRoot = fileURLToPath(new URL('
|
|
20
|
+
const projectRoot = fileURLToPath(new URL('../..', import.meta.url))
|
|
21
21
|
const manifest = JSON.parse(readFileSync(join(projectRoot, 'package.json'), 'utf8')) as {
|
|
22
22
|
name?: string
|
|
23
23
|
acrylblends?: { desktopPath?: string }
|
|
@@ -73,7 +73,7 @@ if (!app.requestSingleInstanceLock()) {
|
|
|
73
73
|
|
|
74
74
|
void app.whenReady().then(async () => {
|
|
75
75
|
// The hook above is registered before the app's modules are loaded, so the app may have .tsx pages: they are loaded here, not as a static import.
|
|
76
|
-
const { default: acrylApp, close } = (await import('../../app.ts')) as {
|
|
76
|
+
const { default: acrylApp, close } = (await import('../../src/app.ts')) as {
|
|
77
77
|
default: { fetch: Parameters<typeof serve>[0]['fetch'] }
|
|
78
78
|
close?: () => Promise<void>
|
|
79
79
|
}
|
|
@@ -9,9 +9,9 @@
|
|
|
9
9
|
"electron": "44.9.3"
|
|
10
10
|
},
|
|
11
11
|
"scripts": {
|
|
12
|
-
"desktop": "electron
|
|
12
|
+
"desktop": "electron plugins/desktop-web/main.ts"
|
|
13
13
|
},
|
|
14
14
|
"gitignore": [".acrylblends/"],
|
|
15
15
|
"omit": ["**/*.test.*"],
|
|
16
|
-
"mount": "nothing to mount: run `npm install`, then `npm run desktop` (the window opens on the app's own server; every electron call is in
|
|
16
|
+
"mount": "nothing to mount: run `npm install`, then `npm run desktop` (the window opens on the app's own server; every electron call is in plugins/desktop-web)"
|
|
17
17
|
}
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
"apps/acryl-desktop/src/shell/desktop-user-data.ts": "user-data.ts",
|
|
6
6
|
"apps/acryl-desktop/src/shell/shutdown.ts": "shutdown.ts"
|
|
7
7
|
},
|
|
8
|
-
"commit": "
|
|
8
|
+
"commit": "f132283c43337f5e1d58dfbbbc15786efb5e756c"
|
|
9
9
|
},
|
|
10
10
|
"rewrittenSpecifiers": 0,
|
|
11
11
|
"digest": "sha256:7dfe960a8bbb476c35540d2202a7aaa62b33492c2ac3e34314e2ecfca45f32f2"
|
|
@@ -55,8 +55,24 @@ export interface ExtensionSource {
|
|
|
55
55
|
readonly trust: Trust
|
|
56
56
|
}
|
|
57
57
|
|
|
58
|
+
/** The library copies a plugins folder holds (`library.json`, written by `acrylblends add`): the app mounts them itself, so they are not extensions. */
|
|
59
|
+
function libraryCopies(dir: string): ReadonlySet<string> {
|
|
60
|
+
try {
|
|
61
|
+
const listed = (
|
|
62
|
+
JSON.parse(readFileSync(join(dir, 'library.json'), 'utf8')) as { plugins?: unknown }
|
|
63
|
+
).plugins
|
|
64
|
+
return new Set(
|
|
65
|
+
Array.isArray(listed)
|
|
66
|
+
? listed.filter((name): name is string => typeof name === 'string')
|
|
67
|
+
: [],
|
|
68
|
+
)
|
|
69
|
+
} catch {
|
|
70
|
+
return new Set()
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
58
74
|
export interface ExtensionsConfig {
|
|
59
|
-
/** Where extension folders may come from. Defaults to `<app home>/extensions`, trusted to install. */
|
|
75
|
+
/** Where extension folders may come from. An app passes its `plugins/` folder (library copies listed in its `library.json` are skipped). Defaults to `<app home>/extensions`, trusted to install, for a service used without an app folder. */
|
|
60
76
|
sources?: readonly ExtensionSource[]
|
|
61
77
|
/** When true, every source that would install by itself asks for approval instead (`ask`). */
|
|
62
78
|
approval?: boolean
|
|
@@ -373,11 +389,13 @@ export class Extensions extends Service {
|
|
|
373
389
|
const found: Array<{ name: string; dir: string; trust: Trust }> = []
|
|
374
390
|
for (const source of this.sources) {
|
|
375
391
|
if (!existsSync(source.dir)) continue
|
|
392
|
+
const library = libraryCopies(source.dir)
|
|
376
393
|
for (const entry of readdirSync(source.dir, { withFileTypes: true })) {
|
|
377
394
|
const dir = join(source.dir, entry.name)
|
|
378
395
|
if (
|
|
379
396
|
entry.isDirectory() &&
|
|
380
397
|
NAME.test(entry.name) &&
|
|
398
|
+
!library.has(entry.name) &&
|
|
381
399
|
ENTRIES.some((file) => existsSync(join(dir, file)))
|
|
382
400
|
) {
|
|
383
401
|
if (!found.some((other) => other.name === entry.name))
|
|
@@ -8,5 +8,5 @@
|
|
|
8
8
|
"@deepseek-ai/cordis": "4.0.4",
|
|
9
9
|
"@webboxes/blends-core": "0.4.0"
|
|
10
10
|
},
|
|
11
|
-
"mount": "mount it after instance: root.plugin(extensions, { sources: [{ dir: join(root, '
|
|
11
|
+
"mount": "mount it after instance: root.plugin(extensions, { sources: [{ dir: join(root, 'plugins'), trust: 'install' }] })"
|
|
12
12
|
}
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* module inside the staging folder, resolves the same name from the app's project folder. Uses Node's public `module.registerHooks`
|
|
5
5
|
* (ACRYL needed the same fallback on Deno); the returned function removes the hook.
|
|
6
6
|
*
|
|
7
|
-
* The framework plugins are not packages in an app (they are source under `
|
|
7
|
+
* The framework plugins are not packages in an app (they are source under `plugins/`), so `aliases` names the files a framework specifier such as
|
|
8
8
|
* `acryl-router` stands for. A staged plugin then imports the same module the app itself runs, so `App` is the very class the router mounts.
|
|
9
9
|
*/
|
|
10
10
|
import { existsSync, mkdirSync, realpathSync } from 'node:fs'
|
package/plugins/manifest/lock.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* `npm run lock`: write `blend.lock.json` (formatVersion 3, the lock ACRYL writes too) next to the manifest, recording what the app is
|
|
3
3
|
* built from: the manifest's identity and digest, the Blueprint it was resolved over, its resolved parameters and rows, and a content
|
|
4
|
-
* digest of every plugin folder in `
|
|
4
|
+
* digest of every plugin folder in `plugins/`. Commit it; a diff of the lock is a review of
|
|
5
5
|
* what changed.
|
|
6
6
|
*/
|
|
7
7
|
import { existsSync, readdirSync, readFileSync, writeFileSync } from 'node:fs'
|
|
@@ -69,7 +69,11 @@ export function writeLock(appRoot: string): LockV3 {
|
|
|
69
69
|
version: document.metadata.version,
|
|
70
70
|
digest: manifestDigest(text),
|
|
71
71
|
},
|
|
72
|
-
modules: [
|
|
72
|
+
modules: [
|
|
73
|
+
...localModules(appRoot, 'plugins'),
|
|
74
|
+
...localModules(appRoot, 'src/plugins'),
|
|
75
|
+
...localModules(appRoot, 'extensions'),
|
|
76
|
+
],
|
|
73
77
|
...(lineage
|
|
74
78
|
? {
|
|
75
79
|
lineage: {
|
|
@@ -86,7 +90,7 @@ export function writeLock(appRoot: string): LockV3 {
|
|
|
86
90
|
}
|
|
87
91
|
|
|
88
92
|
if (process.argv[1] && resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
|
|
89
|
-
const appRoot = resolve(fileURLToPath(new URL('
|
|
93
|
+
const appRoot = resolve(fileURLToPath(new URL('../..', import.meta.url)))
|
|
90
94
|
const locked = writeLock(appRoot)
|
|
91
95
|
console.log(
|
|
92
96
|
`wrote ${LOCK_FILE}: ${locked.rows.length} rows, ${locked.modules.length} local plugins`,
|
|
@@ -65,6 +65,6 @@ const parse = (argv: readonly string[]) =>
|
|
|
65
65
|
if (process.argv[1] && resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
|
|
66
66
|
process.exitCode = runUpgrade(
|
|
67
67
|
process.argv.slice(2),
|
|
68
|
-
resolve(fileURLToPath(new URL('
|
|
68
|
+
resolve(fileURLToPath(new URL('../..', import.meta.url))),
|
|
69
69
|
)
|
|
70
70
|
}
|
package/plugins/save/cli.ts
CHANGED
|
@@ -4,7 +4,7 @@ import { fileURLToPath } from 'node:url'
|
|
|
4
4
|
import { saveFolder } from './index.ts'
|
|
5
5
|
|
|
6
6
|
if (process.argv[1] && resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
|
|
7
|
-
const appRoot = fileURLToPath(new URL('
|
|
7
|
+
const appRoot = fileURLToPath(new URL('../..', import.meta.url))
|
|
8
8
|
const result = saveFolder(appRoot, process.argv.slice(2).join(' ').trim() || 'save the app')
|
|
9
9
|
if (result.status === 'refused') {
|
|
10
10
|
console.error(`not saved: ${result.reason}`)
|
package/plugins/save/plugin.json
CHANGED
|
@@ -8,5 +8,5 @@
|
|
|
8
8
|
"@deepseek-ai/cordis": "4.0.4",
|
|
9
9
|
"@webboxes/app-persistence": "0.1.1"
|
|
10
10
|
},
|
|
11
|
-
"mount": "add a script \"save\": \"node
|
|
11
|
+
"mount": "add a script \"save\": \"node plugins/save/cli.ts\" to package.json; mount the plugin to give the agent a save_app tool"
|
|
12
12
|
}
|
package/plugins/tsconfig.json
CHANGED
|
@@ -24,6 +24,8 @@
|
|
|
24
24
|
"acryl-node-server/tests/adapter",
|
|
25
25
|
"acryl-router/tests/loader.spec.ts",
|
|
26
26
|
"acryl-db-sqlite/tests/loader.spec.ts",
|
|
27
|
+
"acryl-kv-json/tests/loader.spec.ts",
|
|
28
|
+
"acryl-kv-db/tests/loader.spec.ts",
|
|
27
29
|
"acryl-state/src/react.ts",
|
|
28
30
|
"acryl-state/tests/react.spec.ts",
|
|
29
31
|
"acryl-router-http/tests/adapter.spec.ts",
|
package/shared/AGENTS.core.md
CHANGED
|
@@ -9,17 +9,17 @@ Start at `docs/framework/README.md`; `api/summary.md` is the one-page summary. R
|
|
|
9
9
|
`api/context.md` (`c`), `cordis/validation.md` and `guides/validation.md`, `guides/helpers.md` (`streamSSE`), `concepts/middleware.md`, `cordis/plugin-lifecycle.md` (what a plugin
|
|
10
10
|
owns and when it is unloaded), `cordis/mounting.md` and `cordis/node-server.md`. Pages marked "not yet available" describe parts that are not in this app.
|
|
11
11
|
|
|
12
|
-
The docs write `import { Acryl } from 'acrylblends'` and `from 'acrylblends/<subpath>'`. In this app the framework is the vendored plugin source in `
|
|
12
|
+
The docs write `import { Acryl } from 'acrylblends'` and `from 'acrylblends/<subpath>'`. In this app the framework is the vendored plugin source in `plugins/acryl-router/src`
|
|
13
13
|
(read it, do not edit it: a change belongs in ACRYL, upstream), so from a file in `src/` import:
|
|
14
14
|
|
|
15
|
-
-
|
|
16
|
-
-
|
|
17
|
-
-
|
|
18
|
-
-
|
|
15
|
+
- `../plugins/acryl-router/src/index.ts` for `Acryl`, `Context`, `HTTPException`, `connectionOf`, `getConnInfo` and the types;
|
|
16
|
+
- `../plugins/acryl-router/src/middleware/index.ts` for every middleware (`cors`, `bodyLimit`, `trimTrailingSlash`, `loopbackOnly`, ...);
|
|
17
|
+
- `../plugins/acryl-router/src/helpers/index.ts` for the helpers and validators (`streamSSE`, `schemaValidator`, `validator`, cookies, `testClient`, ...).
|
|
18
|
+
- `../plugins/acryl-router/src/core/jsx/index.ts` for `acrylblends/jsx` (`jsx`, `Fragment`, the types), and `.../core/jsx/streaming.ts` for `acrylblends/jsx/streaming`.
|
|
19
19
|
|
|
20
|
-
**JSX: write `.tsx`.** The scripts in `package.json` run Node.js with `--import ./
|
|
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 written into `
|
|
22
|
+
A plugin written into `plugins/` while the app runs may use the docs' own names instead: `acrylblends` and `acrylblends/<subpath>` resolve to the same files. `App` is the
|
|
23
23
|
deprecated name of `Acryl`.
|
|
24
24
|
|
|
25
25
|
## How a feature is built
|
|
@@ -28,8 +28,8 @@ Routes are an ordinary `Acryl` app that a plugin mounts once with `ctx.router.ro
|
|
|
28
28
|
|
|
29
29
|
```ts
|
|
30
30
|
import type { Context } from '@deepseek-ai/cordis'
|
|
31
|
-
import { Acryl, HTTPException } from '
|
|
32
|
-
import { schemaValidator, type StandardSchema } from '
|
|
31
|
+
import { Acryl, HTTPException } from '../plugins/acryl-router/src/index.ts'
|
|
32
|
+
import { schemaValidator, type StandardSchema } from '../plugins/acryl-router/src/helpers/index.ts'
|
|
33
33
|
|
|
34
34
|
export const name = 'notes' // a plugin's name; blend.yaml refers to it
|
|
35
35
|
export const inject = ['router'] // services it needs
|
|
@@ -44,7 +44,7 @@ export function apply(ctx: Context) {
|
|
|
44
44
|
|
|
45
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
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 `
|
|
47
|
+
- State, when `plugins/acryl-state` exists: `const cell = ctx.state.cell<T>('notes.list', initial)` (add `'state'` to `inject`), then
|
|
48
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
49
|
and streamed at `/api/state/<key>/events`.
|
|
50
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`.
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
|
|
2
|
+
## Defaults you can swap
|
|
3
|
+
|
|
4
|
+
The framework is not rigid. Every choice is a plugin behind a port, and what the app starts with is only a fallback. Name the default before you change it,
|
|
5
|
+
and swap it at the one place that mounts it.
|
|
6
|
+
|
|
7
|
+
| Choice | Default | To swap it |
|
|
8
|
+
| --- | --- | --- |
|
|
9
|
+
| Records | none until you add one: `npx acrylblends add db` mounts the `db` port on SQLite | mount another provider of `db` in `src/app.ts`; plugins use `ctx.db` and do not change |
|
|
10
|
+
| Small state | none until you add one: `npx acrylblends add kv` mounts the `kv` port on one JSON file | mount `acryl-kv-db` (the same port over `db`) or a provider of your own |
|
|
11
|
+
| Server | Node (`acryl-node-server`) | mount the adapter of another runtime in `src/app.ts` |
|
|
12
|
+
| Window | the browser | `npx acrylblends add desktop` adds an Electron window on the same server |
|
|
13
|
+
|
|
14
|
+
- **An outside tool** (a database server, a bundler, a router such as Vike) joins the app as a provider: a plugin in `plugins/<name>/` that implements a port and
|
|
15
|
+
passes that port's conformance tests, then `src/app.ts` mounts it. Postgres is such a provider and is not part of this package; add it when the app needs it.
|
|
16
|
+
- **The dependency rule:** only the file that mounts a provider imports it; everything else takes the port as an argument or injects it. `npx acrylblends check-deps` enforces it.
|
|
17
|
+
|
|
18
|
+
## Plugins
|
|
19
|
+
|
|
20
|
+
There is one plugin folder: `plugins/` at the root of the app. A plugin is a folder `plugins/<name>/` with an `index.ts`. `npx acrylblends add <name>` copies a library
|
|
21
|
+
plugin there; a plugin you or an agent write goes there too (the `extensions` plugin installs it into the running app, and `plugins/library.json` lists the copies it must not
|
|
22
|
+
install a second time). Do not create `src/plugins/` or `extensions/`.
|
|
23
|
+
|
|
24
|
+
## The door
|
|
25
|
+
|
|
26
|
+
The door is a command in this folder, `.acryl/bin/acryl`, that lets you look at and change the running app; it reaches only the app running from this folder. Its state (a secret and an audit log) lives under `.acryl/`, which git ignores: never commit it. If `.acryl/bin/acryl` is not here, this app has no door yet: run it with its scripts and check it with `npm test`.
|
|
27
|
+
|
|
28
|
+
```
|
|
29
|
+
.acryl/bin/acryl brief what this app is: its plugins, where the docs are, the verbs
|
|
30
|
+
.acryl/bin/acryl look a snapshot of the page, with refs (r1, r2, ...)
|
|
31
|
+
.acryl/bin/acryl act click --ref <r>
|
|
32
|
+
.acryl/bin/acryl act type --ref <r> --text <t> [--submit] [--no-clear]
|
|
33
|
+
.acryl/bin/acryl act select --ref <r> --option <o>
|
|
34
|
+
.acryl/bin/acryl act press --key <Enter|Escape|...>
|
|
35
|
+
.acryl/bin/acryl act scroll [--ref <r>] --direction up|down [--amount <n>]
|
|
36
|
+
.acryl/bin/acryl act wait (--text <t> | --role <role> [--name <n>]) [--gone] [--timeout-ms <ms>]
|
|
37
|
+
.acryl/bin/acryl plugin list | add <folder> | remove <name> | reload
|
|
38
|
+
.acryl/bin/acryl tool list | call --tool <name> --args '<json>'
|
|
39
|
+
```
|
|
40
|
+
Every verb takes `--json`. `plugin add plugins/<name>` checks the plugin, installs it and makes it live without a restart. `tool call` reaches only the tools on the door's allowlist (empty by default). On Windows the command is `.acryl\bin\acryl.cmd`.
|
package/shared/SKILL.md
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: app-guide
|
|
3
|
+
description: How to change, run and check the __APP_NAME__ app. Use it 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` in this folder says how the app is laid out. The framework's API is in `docs/framework/`: start at `docs/framework/README.md`, and
|
|
9
|
+
`docs/framework/api/summary.md` is the one-page summary. Parts the docs mark "not yet available" are not in this app.
|
|
10
|
+
2. **Make the change small.** Your own routes are a plugin (`src/routes.ts`) that builds an `Acryl` app and mounts it; test a route with `await app.request('/path')`.
|
|
11
|
+
3. **One plugin folder.** A plugin lives in `plugins/<name>/` (`npx acrylblends add <name>` copies a library plugin there). There is no `src/plugins/` and no `extensions/`.
|
|
12
|
+
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.
|
|
13
|
+
5. **Look at the running app.** The door is a command in this folder, `.acryl/bin/acryl`, that lets you look at and change the running app; it reaches only the app running from this folder. Its state (a secret and an audit log) lives under `.acryl/`, which git ignores: never commit it. If `.acryl/bin/acryl` is not here, this app has no door yet: run it with its scripts and check it with `npm test`. Start with `.acryl/bin/acryl brief`, then `look` for the page and `act` to click or type on it; the full list is in `AGENTS.md`.
|
|
14
|
+
6. **Done means checked.** `npm test` passes, and the change shows in the running app after a restart. No secret goes in a file or in `git status`: keys come from environment variables.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
|
|
2
2
|
## This app (agent)
|
|
3
3
|
|
|
4
|
-
Besides `src/routes.ts`, the app runs an agent (`POST /ask`) that can write plugins into `
|
|
4
|
+
Besides `src/routes.ts`, the app runs an agent (`POST /ask`) that can write plugins into `plugins/` while the app runs and install them with
|
|
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`.
|
|
@@ -13,14 +13,14 @@ curl -X POST <url>/ask -H 'content-type: application/json' -d '{"question":"add
|
|
|
13
13
|
# {"status":"completed","output":"The answer is 5.","steps":2}
|
|
14
14
|
```
|
|
15
15
|
|
|
16
|
-
- `src/main.ts` chooses where the app lives (`
|
|
16
|
+
- `src/main.ts` chooses where the app lives (`plugins/instance`) and starts it.
|
|
17
17
|
- `blend.yaml` is the app as data: which plugins it mounts, in what order, with what config and parameters. Switch
|
|
18
18
|
one off with `disabled: true`. `npm run lock` writes `blend.lock.json`: the manifest's digest and a digest of
|
|
19
19
|
every plugin folder, so a diff of the lock shows what changed.
|
|
20
20
|
- `src/app.ts` holds the registry of plugins the code provides, and mounts the manifest's rows.
|
|
21
|
-
- `
|
|
22
|
-
- `
|
|
23
|
-
version running. `
|
|
21
|
+
- `plugins/agent` is the offline agent: a scripted model, a tool registry, memory, and one child fiber per run.
|
|
22
|
+
- `plugins/extensions` installs, updates and removes plugins while the app runs; a failed update keeps the previous
|
|
23
|
+
version running. `plugins/builder` gives the agent tools to do that itself, writing into `plugins/`.
|
|
24
24
|
|
|
25
25
|
Give it a real model and it builds any Cordis plugin you describe (any OpenAI-compatible API; nothing is installed):
|
|
26
26
|
|
|
@@ -40,9 +40,9 @@ curl <url>/tasks # every run as a durable task, with the tool calls
|
|
|
40
40
|
```
|
|
41
41
|
|
|
42
42
|
Other commands: `update route <name> <path> <text>`, `break plugin <name>` (shows the rollback), `remove plugin <name>`,
|
|
43
|
-
`list plugins`. What the agent writes lands in `
|
|
43
|
+
`list plugins`. What the agent writes lands in `plugins/`, is part of your app, and starts again at the next start.
|
|
44
44
|
- `src/routes.ts` is your own plugin: it builds an `App` (`import { App } from './plugins/acryl-router/src/index.ts'`) and mounts it with `ctx.router.route(prefix, app)`. Routes added to the app after it is mounted are not served. A page mounted at `/x` answers `/x`, not `/x/` (trailing slashes are strict). The router API is documented at https://acrylblends.github.io/docs/framework/the-acryl-framework and in `docs/framework` of the acrylblends repository.
|
|
45
|
-
- `
|
|
45
|
+
- `plugins/` holds the plugins copied from the ACRYLBLENDS library. They are yours now; edit them freely.
|
|
46
46
|
Add more with `acrylblends add <plugin>`.
|
|
47
47
|
|
|
48
48
|
`npm run save -- "message"` commits the app with git (and pushes when it has a remote). It refuses a file that looks like
|
|
@@ -4,20 +4,20 @@
|
|
|
4
4
|
"private": true,
|
|
5
5
|
"type": "module",
|
|
6
6
|
"imports": {
|
|
7
|
-
"#acryl-jsx/jsx-runtime": "./
|
|
8
|
-
"#acryl-jsx/jsx-dev-runtime": "./
|
|
7
|
+
"#acryl-jsx/jsx-runtime": "./plugins/acryl-router/src/core/jsx/jsx-runtime.ts",
|
|
8
|
+
"#acryl-jsx/jsx-dev-runtime": "./plugins/acryl-router/src/core/jsx/jsx-dev-runtime.ts"
|
|
9
9
|
},
|
|
10
10
|
"engines": {
|
|
11
11
|
"node": ">=22.19.0"
|
|
12
12
|
},
|
|
13
13
|
"scripts": {
|
|
14
|
-
"start": "node --import ./
|
|
15
|
-
"dev": "node --watch --import ./
|
|
16
|
-
"test": "node --import ./
|
|
14
|
+
"start": "node --import ./plugins/acryl-node-server/src/register.ts src/main.ts",
|
|
15
|
+
"dev": "node --watch --import ./plugins/acryl-node-server/src/register.ts src/main.ts",
|
|
16
|
+
"test": "node --import ./plugins/acryl-node-server/src/register.ts --test \"test/**/*.test.{ts,tsx}\"",
|
|
17
17
|
"check": "tsc --noEmit",
|
|
18
|
-
"lock": "node
|
|
19
|
-
"save": "node
|
|
20
|
-
"upgrade": "node
|
|
18
|
+
"lock": "node plugins/manifest/lock.ts",
|
|
19
|
+
"save": "node plugins/save/cli.ts",
|
|
20
|
+
"upgrade": "node plugins/manifest/upgrade-cli.ts"
|
|
21
21
|
},
|
|
22
22
|
"dependencies": {
|
|
23
23
|
"typescript": "6.0.3"
|