acrylblends 0.11.0 → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (93) hide show
  1. package/README.md +131 -53
  2. package/dist/index.d.ts +29 -2
  3. package/dist/index.js +450 -69
  4. package/dist/index.js.map +1 -1
  5. package/docs/framework/README.md +5 -3
  6. package/docs/framework/agents/index.md +1 -1
  7. package/docs/framework/api/acryl.md +5 -17
  8. package/docs/framework/api/context.md +12 -61
  9. package/docs/framework/api/presets.md +1 -1
  10. package/docs/framework/api/request.md +3 -3
  11. package/docs/framework/api/summary.md +1 -1
  12. package/docs/framework/concepts/web-standard.md +6 -16
  13. package/docs/framework/cordis/acryl-in-a-cordis-app.md +13 -8
  14. package/docs/framework/cordis/migration-from-0-4.md +1 -1
  15. package/docs/framework/cordis/migration-to-1-0.md +45 -0
  16. package/docs/framework/cordis/node-server.md +2 -2
  17. package/docs/framework/getting-started/basic.md +15 -10
  18. package/docs/framework/getting-started/desktop.md +1 -1
  19. package/docs/framework/getting-started/other-runtimes.md +10 -14
  20. package/docs/framework/guides/create-acrylblends.md +4 -2
  21. package/docs/framework/guides/data.md +1 -1
  22. package/docs/framework/guides/faq.md +1 -1
  23. package/docs/framework/guides/jsx-dom.md +1 -1
  24. package/docs/framework/guides/production.md +43 -0
  25. package/docs/framework/guides/rpc.md +3 -11
  26. package/docs/framework/helpers/adapter.md +13 -43
  27. package/docs/framework/helpers/conninfo.md +1 -36
  28. package/docs/framework/helpers/streaming.md +1 -1
  29. package/docs/framework/helpers/websocket.md +4 -11
  30. package/docs/framework/middleware/builtin/basic-auth.md +1 -1
  31. package/docs/framework/middleware/builtin/cache.md +2 -22
  32. package/docs/framework/middleware/builtin/compress.md +1 -1
  33. package/docs/framework/middleware/builtin/context-storage.md +10 -14
  34. package/docs/framework/middleware/builtin/cors.md +1 -2
  35. package/docs/framework/middleware/builtin/ip-restriction.md +1 -1
  36. package/docs/framework/middleware/builtin/jsx-renderer.md +1 -1
  37. package/docs/framework/middleware/builtin/logger.md +1 -1
  38. package/docs/framework/middleware/builtin/loopback-only.md +1 -1
  39. package/docs/framework/middleware/builtin/mount.md +1 -1
  40. package/docs/framework/middleware/builtin/request-id.md +13 -20
  41. package/docs/framework/middleware/builtin/timing.md +1 -2
  42. package/lib/cordis/door.d.ts +5 -0
  43. package/lib/cordis/door.js +8 -4
  44. package/lib/cordis/inspect.js +1 -1
  45. package/lib/cordis/router-http.js +1 -1
  46. package/lib/cordis/router.d.ts +1 -2
  47. package/lib/cordis/router.js +2 -3
  48. package/lib/cordis/state.js +1 -1
  49. package/lib/door/host.js +1 -1
  50. package/lib/drivers.d.ts +66 -0
  51. package/lib/drivers.js +16 -0
  52. package/lib/index.d.ts +1 -2
  53. package/lib/index.js +1 -2
  54. package/lib/{snapshot-text-yaWlezbd.js → snapshot-text-DmE0YfeZ.js} +2 -2
  55. package/lib/{src-DNzw0Qsb.js → src-2Z4mJg0x.js} +0 -1
  56. package/package.json +5 -121
  57. package/plugins/acryl-door/src/index.ts +8 -0
  58. package/plugins/acryl-router/src/index.ts +0 -1
  59. package/plugins/agent-panel/drivers.ts +1 -16
  60. package/plugins/agent-panel/index.ts +1 -1
  61. package/plugins/agent-panel/registry.ts +16 -0
  62. package/plugins/driver-acp/README.md +53 -0
  63. package/plugins/driver-acp/chat.ts +258 -0
  64. package/plugins/driver-acp/connection.ts +239 -0
  65. package/plugins/driver-acp/index.ts +65 -0
  66. package/plugins/driver-acp/plugin.json +12 -0
  67. package/plugins/driver-acp/provenance.json +20 -0
  68. package/plugins/driver-pty/index.ts +1 -0
  69. package/shared/library-history.json +464 -0
  70. package/docs/framework/getting-started/aws-lambda.md +0 -115
  71. package/docs/framework/getting-started/cloudflare-pages.md +0 -53
  72. package/docs/framework/getting-started/cloudflare-workers.md +0 -67
  73. package/docs/framework/getting-started/lambda-edge.md +0 -41
  74. package/docs/framework/getting-started/netlify.md +0 -25
  75. package/docs/framework/getting-started/service-worker.md +0 -33
  76. package/docs/framework/getting-started/vercel.md +0 -28
  77. package/lib/aws-lambda.d.ts +0 -324
  78. package/lib/aws-lambda.js +0 -357
  79. package/lib/cloudflare-pages.d.ts +0 -58
  80. package/lib/cloudflare-pages.js +0 -78
  81. package/lib/cloudflare-workers.d.ts +0 -21
  82. package/lib/cloudflare-workers.js +0 -80
  83. package/lib/deprecated-BP4fPt38.js +0 -10
  84. package/lib/deprecated-CTy4N7ng.d.ts +0 -12
  85. package/lib/lambda-edge.d.ts +0 -92
  86. package/lib/lambda-edge.js +0 -105
  87. package/lib/netlify.d.ts +0 -29
  88. package/lib/netlify.js +0 -30
  89. package/lib/service-worker.d.ts +0 -48
  90. package/lib/service-worker.js +0 -38
  91. package/lib/vercel.d.ts +0 -10
  92. package/lib/vercel.js +0 -9
  93. package/plugins/acryl-router/src/deprecated.ts +0 -15
@@ -0,0 +1,239 @@
1
+ /**
2
+ * One ACP agent process and the JSON-RPC 2.0 connection to it: newline-delimited messages over its stdin and stdout (the ACP stdio transport). The driver needs a handful of
3
+ * calls, so this is the whole client: requests with their answers, notifications both ways, and the agent's own requests answered by a handler. `close()` ends the process
4
+ * the way an ACP agent expects: stdin closed first, then SIGTERM, then a hard kill, and it resolves only once the process is gone.
5
+ */
6
+ import { type ChildProcessWithoutNullStreams, spawn, spawnSync } from 'node:child_process'
7
+
8
+ /** How to start an ACP agent: a program and its arguments, and what to add to the app's environment. */
9
+ export interface AcpAgent {
10
+ readonly command: string
11
+ readonly args: readonly string[]
12
+ readonly env?: Readonly<Record<string, string>>
13
+ }
14
+
15
+ /** An error the agent answered with, or the reason the connection ended. */
16
+ export class AcpError extends Error {
17
+ /** The JSON-RPC error code, for an error the agent answered with. */
18
+ readonly code: number | undefined
19
+
20
+ constructor(message: string, code?: number) {
21
+ super(message)
22
+ this.name = 'AcpError'
23
+ this.code = code
24
+ }
25
+ }
26
+
27
+ /** Answers a request the agent sends (`session/request_permission`); throwing answers with an error. */
28
+ export type AgentRequestHandler = (method: string, params: unknown) => unknown
29
+
30
+ /** How long each step of `close()` waits before the next, harder one. */
31
+ const GRACE_MS = 2000
32
+ /** The most of the agent's stderr kept, to say why it ended. */
33
+ const STDERR_TAIL = 4000
34
+
35
+ interface Pending {
36
+ resolve(value: unknown): void
37
+ reject(error: Error): void
38
+ }
39
+
40
+ export class AcpConnection {
41
+ readonly #child: ChildProcessWithoutNullStreams
42
+ readonly #pending = new Map<number, Pending>()
43
+ readonly #listeners = new Set<(method: string, params: unknown) => void>()
44
+ #nextId = 1
45
+ #buffer = ''
46
+ #stderr = ''
47
+ #ended: AcpError | undefined
48
+ /** Resolves with the reason once the process is gone. */
49
+ readonly ended: Promise<AcpError>
50
+
51
+ constructor(agent: AcpAgent, cwd: string, handle: AgentRequestHandler) {
52
+ this.#child = spawn(agent.command, [...agent.args], {
53
+ cwd,
54
+ env: { ...process.env, ...agent.env },
55
+ stdio: ['pipe', 'pipe', 'pipe'],
56
+ // An npm-installed agent is a `.cmd` shim on Windows, which only a shell can start.
57
+ shell: process.platform === 'win32',
58
+ // Its own process group on POSIX, so closing the connection also ends what the agent started (its tools, a shell): nothing is left behind when the plugin unloads.
59
+ detached: process.platform !== 'win32',
60
+ windowsHide: true,
61
+ })
62
+ this.ended = new Promise((resolve) => {
63
+ const end = (reason: AcpError): void => {
64
+ if (this.#ended) return
65
+ this.#ended = reason
66
+ for (const pending of this.#pending.values()) pending.reject(reason)
67
+ this.#pending.clear()
68
+ resolve(reason)
69
+ }
70
+ this.#child.on('error', (error) =>
71
+ end(new AcpError(`${agent.command} could not be started: ${error.message}`)),
72
+ )
73
+ this.#child.on('exit', (code, signal) => {
74
+ const tail = this.#stderr.trim().split('\n').slice(-3).join(' ').slice(-500)
75
+ end(
76
+ new AcpError(
77
+ `${agent.command} ended (${signal ?? `exit code ${String(code)}`})${tail ? `: ${tail}` : ''}`,
78
+ ),
79
+ )
80
+ })
81
+ })
82
+ this.#child.stderr.setEncoding('utf8')
83
+ this.#child.stderr.on('data', (chunk: string) => {
84
+ this.#stderr = (this.#stderr + chunk).slice(-STDERR_TAIL)
85
+ })
86
+ this.#child.stdout.setEncoding('utf8')
87
+ this.#child.stdout.on('data', (chunk: string) => {
88
+ this.#buffer += chunk
89
+ for (let end = this.#buffer.indexOf('\n'); end !== -1; end = this.#buffer.indexOf('\n')) {
90
+ const line = this.#buffer.slice(0, end).trim()
91
+ this.#buffer = this.#buffer.slice(end + 1)
92
+ if (line) this.#receive(line, handle)
93
+ }
94
+ })
95
+ // A write after the agent went away fails here instead of throwing out of the process.
96
+ this.#child.stdin.on('error', () => {})
97
+ }
98
+
99
+ /** Sends a request and resolves with its result; rejects with the agent's error, or when the process ends first. */
100
+ request<T>(method: string, params: unknown): Promise<T> {
101
+ if (this.#ended) return Promise.reject(this.#ended)
102
+ const id = this.#nextId++
103
+ return new Promise<T>((resolve, reject) => {
104
+ this.#pending.set(id, { resolve: resolve as (value: unknown) => void, reject })
105
+ this.#send({ jsonrpc: '2.0', id, method, params })
106
+ })
107
+ }
108
+
109
+ notify(method: string, params: unknown): void {
110
+ if (!this.#ended) this.#send({ jsonrpc: '2.0', method, params })
111
+ }
112
+
113
+ /** Hears every notification the agent sends; returns the function that stops listening. */
114
+ onNotification(listener: (method: string, params: unknown) => void): () => void {
115
+ this.#listeners.add(listener)
116
+ return () => void this.#listeners.delete(listener)
117
+ }
118
+
119
+ /**
120
+ * Ends the process and what it started: stdin closed, then SIGTERM, then a hard kill, each to the whole process group on POSIX (the tree, with `taskkill /T`, on Windows).
121
+ * Resolves once they are gone. Safe to call again, and after the agent ended on its own (its leftovers are still cleared).
122
+ */
123
+ async close(): Promise<void> {
124
+ if (!this.#ended) {
125
+ const gone = (ms: number) =>
126
+ Promise.race([
127
+ this.ended.then(() => true),
128
+ new Promise<boolean>((done) => setTimeout(() => done(false), ms).unref()),
129
+ ])
130
+ this.#child.stdin.end()
131
+ if (!(await gone(GRACE_MS))) {
132
+ this.#signal('SIGTERM')
133
+ if (!(await gone(GRACE_MS))) this.#signal('SIGKILL')
134
+ await this.ended
135
+ }
136
+ }
137
+ await this.#reapGroup()
138
+ }
139
+
140
+ /** A signal to the agent and, on POSIX, everything in its group. */
141
+ #signal(signal: 'SIGTERM' | 'SIGKILL'): void {
142
+ const pid = this.#child.pid
143
+ if (pid === undefined) return
144
+ if (process.platform === 'win32') {
145
+ if (signal === 'SIGKILL')
146
+ spawnSync('taskkill', ['/pid', String(pid), '/T', '/F'], { windowsHide: true })
147
+ else this.#child.kill(signal)
148
+ return
149
+ }
150
+ try {
151
+ process.kill(-pid, signal)
152
+ } catch {
153
+ this.#child.kill(signal)
154
+ }
155
+ }
156
+
157
+ /** On POSIX, processes the agent left in its group are ended too: SIGTERM, then SIGKILL after the grace. */
158
+ async #reapGroup(): Promise<void> {
159
+ const pid = this.#child.pid
160
+ if (process.platform === 'win32' || pid === undefined) return
161
+ const groupAlive = (): boolean => {
162
+ try {
163
+ process.kill(-pid, 0)
164
+ return true
165
+ } catch (error) {
166
+ return (error as NodeJS.ErrnoException).code === 'EPERM'
167
+ }
168
+ }
169
+ for (const signal of ['SIGTERM', 'SIGKILL'] as const) {
170
+ if (!groupAlive()) return
171
+ try {
172
+ process.kill(-pid, signal)
173
+ } catch {
174
+ return
175
+ }
176
+ for (let waited = 0; waited < GRACE_MS && groupAlive(); waited += 25)
177
+ await new Promise((done) => setTimeout(done, 25))
178
+ }
179
+ }
180
+
181
+ #send(message: unknown): void {
182
+ this.#child.stdin.write(`${JSON.stringify(message)}\n`)
183
+ }
184
+
185
+ #receive(line: string, handle: AgentRequestHandler): void {
186
+ let message: {
187
+ id?: number | string
188
+ method?: string
189
+ params?: unknown
190
+ result?: unknown
191
+ error?: { code?: number; message?: string }
192
+ }
193
+ try {
194
+ message = JSON.parse(line)
195
+ } catch {
196
+ return // Not a protocol frame: an agent may print a stray line; it carries nothing to act on.
197
+ }
198
+ if (typeof message.method === 'string') {
199
+ if (message.id === undefined) {
200
+ for (const listener of this.#listeners) listener(message.method, message.params)
201
+ return
202
+ }
203
+ const id = message.id
204
+ Promise.resolve()
205
+ .then(() => handle(message.method as string, message.params))
206
+ .then(
207
+ (result) => this.#answer(id, result),
208
+ (error: unknown) =>
209
+ this.#send({
210
+ jsonrpc: '2.0',
211
+ id,
212
+ error:
213
+ error instanceof AcpError && error.code !== undefined
214
+ ? { code: error.code, message: error.message }
215
+ : {
216
+ code: -32603,
217
+ message: error instanceof Error ? error.message : String(error),
218
+ },
219
+ }),
220
+ )
221
+ return
222
+ }
223
+ const pending = typeof message.id === 'number' ? this.#pending.get(message.id) : undefined
224
+ if (!pending) return
225
+ this.#pending.delete(message.id as number)
226
+ if (message.error)
227
+ pending.reject(
228
+ new AcpError(
229
+ message.error.message ?? 'the agent answered with an error',
230
+ message.error.code,
231
+ ),
232
+ )
233
+ else pending.resolve(message.result)
234
+ }
235
+
236
+ #answer(id: number | string, result: unknown): void {
237
+ if (!this.#ended) this.#send({ jsonrpc: '2.0', id, result: result ?? null })
238
+ }
239
+ }
@@ -0,0 +1,65 @@
1
+ /**
2
+ * The ACP driver (M8, spec 046 T028): any agent that speaks the Agent Client Protocol (ACP v1 over stdio) as a chat driver of the agent panel, started in the app folder.
3
+ * No agent gets code of its own here: an agent is a preset, a command line that starts its ACP server. `dsh` (DeepSeek Harness, `dsh --profile acp`) is the first preset;
4
+ * other ACP agents are added by config.
5
+ *
6
+ * - The drivers are `chat` entries of the panel's `drivers` service (`acp:dsh`, ...). A panel without this plugin lists none; this plugin without a panel is pending.
7
+ * - An agent whose command is not on the machine gets no driver, and the plugin says so in one line (as `driver-pty` does without node-pty).
8
+ * - Every agent process is acquired inside one `ctx.effect`; its disposer ends the process. Unloading or reloading the plugin leaves no agent running.
9
+ * - Development only, like the panel: with `NODE_ENV=production` (or `ACRYL_DOOR=off`) it starts nothing.
10
+ */
11
+ import { existsSync } from 'node:fs'
12
+ import { isAbsolute } from 'node:path'
13
+ import type { Context } from '@deepseek-ai/cordis'
14
+ import { onPath } from '../agent-panel/headless.ts'
15
+ import type {} from '../agent-panel/registry.ts'
16
+ import { type AcpPermissions, acpDriver, type OpenAgent } from './chat.ts'
17
+ import { type AcpAgent, AcpConnection } from './connection.ts'
18
+
19
+ export const name = 'driver-acp'
20
+ export const inject = ['drivers', 'appInstance']
21
+
22
+ export type { AcpPermissions } from './chat.ts'
23
+ export { acpDriver } from './chat.ts'
24
+ export { type AcpAgent, AcpConnection, AcpError } from './connection.ts'
25
+
26
+ /** The ACP agents this plugin knows by name: the command line that starts each one's ACP server. */
27
+ export const PRESETS: Readonly<Record<string, AcpAgent>> = {
28
+ dsh: { command: 'dsh', args: ['--profile', 'acp'] },
29
+ }
30
+
31
+ export interface DriverAcpConfig {
32
+ /** The agents to offer, by name (the driver is `acp:<name>`). Default: the presets whose command is on the machine. */
33
+ agents?: Record<string, AcpAgent>
34
+ /** The answer to an agent's permission request: its "allow once" or its "reject once" option. Default `reject`. */
35
+ permissions?: AcpPermissions
36
+ }
37
+
38
+ export function apply(ctx: Context, config: DriverAcpConfig = {}): void {
39
+ if (process.env.NODE_ENV === 'production' || process.env.ACRYL_DOOR === 'off') return
40
+ const root = ctx.appInstance.root
41
+ const open: OpenAgent = (agent, handle) => {
42
+ let connection!: AcpConnection
43
+ const close = ctx.effect(() => {
44
+ connection = new AcpConnection(agent, root, handle)
45
+ return () => connection.close()
46
+ }) as () => Promise<void>
47
+ // An agent that ended on its own gives its effect back at once instead of at unload.
48
+ void connection.ended.then(() => close())
49
+ return { connection, close }
50
+ }
51
+ for (const [agentName, agent] of Object.entries(config.agents ?? PRESETS)) {
52
+ if (!(isAbsolute(agent.command) ? existsSync(agent.command) : onPath(agent.command))) {
53
+ console.warn(`driver-acp: no acp:${agentName} driver, "${agent.command}" is not on the PATH`)
54
+ continue
55
+ }
56
+ const driver = acpDriver({
57
+ name: `acp:${agentName}`,
58
+ agent,
59
+ cwd: root,
60
+ permissions: config.permissions ?? 'reject',
61
+ open,
62
+ })
63
+ ctx.effect(() => ctx.drivers.register(driver))
64
+ }
65
+ }
@@ -0,0 +1,12 @@
1
+ {
2
+ "name": "driver-acp",
3
+ "description": "Any agent that speaks the Agent Client Protocol (ACP) as a chat in the agent panel, started in the app folder: DeepSeek Harness (dsh --profile acp) is the first preset, other ACP agents are added by config. Development only; needs the agent panel; opt-in with acrylblends add driver-acp.",
4
+ "requires": ["agent-panel", "instance"],
5
+ "inject": ["drivers", "appInstance"],
6
+ "provides": [],
7
+ "dependencies": {
8
+ "@deepseek-ai/cordis": "4.0.4"
9
+ },
10
+ "mount": "register it in src/app.ts ('driver-acp': driverAcp) and add a row `- id: driver-acp` / `name: driver-acp` to blend.yaml AFTER the agent-panel row; with dsh on the PATH the panel offers acp:dsh",
11
+ "gitignore": []
12
+ }
@@ -0,0 +1,20 @@
1
+ {
2
+ "written": "from the specification, not ported: connection.ts and chat.ts are this repository's own code",
3
+ "protocol": {
4
+ "name": "Agent Client Protocol",
5
+ "version": 1,
6
+ "site": "https://agentclientprotocol.com",
7
+ "transport": "stdio, newline-delimited JSON-RPC 2.0"
8
+ },
9
+ "schema": {
10
+ "file": "test/acp-v1/schema.json",
11
+ "from": "npm @agentclientprotocol/sdk@2.0.2, schema/v1/schema.json (the stable v1 schema, not schema.unstable.json)",
12
+ "repository": "https://github.com/agentclientprotocol/typescript-sdk",
13
+ "license": "Apache-2.0, Copyright 2025 Zed Industries, Inc. and contributors (test/acp-v1/LICENSE)",
14
+ "sha256": "4bcd1d2051ff7ed379bd9b234fb9b09c5ab9245478698e9fab805bd9b52a33a6",
15
+ "modified": false,
16
+ "used by": "test/conformance.test.ts; the schema is a test fixture and is not copied into apps"
17
+ },
18
+ "checked against": "dsh (DeepSeek Harness) 0.2.1-rc.2, dsh --profile acp, 2026-10-28: scripts/verify-agent-panel.mjs --driver acp:dsh",
19
+ "update": "take schema/v1/schema.json from a newer @agentclientprotocol/sdk, update sha256 and the version here, and run test/conformance.test.ts"
20
+ }
@@ -16,6 +16,7 @@ import { streamSSE } from '../acryl-router/src/helpers/index.ts'
16
16
  import { Acryl } from '../acryl-router/src/index.ts'
17
17
  import type { TerminalDriver, TerminalSession } from '../agent-panel/drivers.ts'
18
18
  import { onPath } from '../agent-panel/headless.ts'
19
+ import type {} from '../agent-panel/registry.ts'
19
20
  import { loadNodePty, type PtySpawn } from './spawn.ts'
20
21
  import { terminalDriver } from './terminal.ts'
21
22