acrylblends 0.7.7 → 0.8.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/dist/index.d.ts +14 -6
- package/dist/index.js +130 -31
- package/dist/index.js.map +1 -1
- package/docs/framework/README.md +3 -3
- package/docs/framework/UPSTREAM.md +1 -1
- package/docs/framework/agents/index.md +18 -3
- package/docs/framework/getting-started/basic.md +1 -2
- package/docs/framework/getting-started/desktop.md +3 -3
- package/docs/framework/guides/bring-your-own-tool.md +216 -0
- package/docs/framework/guides/client-modules.md +4 -3
- package/docs/framework/guides/create-acrylblends.md +5 -4
- package/docs/framework/guides/data.md +7 -6
- package/docs/framework/helpers/streaming.md +12 -27
- package/docs/framework/upstream.json +3 -3
- package/lib/browser/read-sse.js +84 -0
- package/lib/db/sqlite.d.ts +1 -1
- package/lib/db.d.ts +1 -84
- package/lib/index-BLTYsyc1.d.ts +93 -0
- package/lib/index-D4FAsBZu.d.ts +85 -0
- package/lib/kv/db.d.ts +2 -2
- package/lib/kv/json.d.ts +1 -1
- package/lib/kv/json.js +87 -24
- package/lib/kv.d.ts +1 -92
- package/lib/node-server.d.ts +3 -3
- package/lib/node-server.js +14 -8
- package/lib/read-sse.d.ts +33 -0
- package/lib/read-sse.js +84 -0
- package/package.json +5 -1
- package/plugins/acryl-db/plugin.json +7 -2
- package/plugins/acryl-db/src/conformance.ts +224 -0
- package/plugins/acryl-db/upstream.json +2 -2
- package/plugins/acryl-db-sqlite/plugin.json +4 -1
- package/plugins/acryl-db-sqlite/upstream.json +1 -1
- package/plugins/acryl-door/LICENSE +47 -0
- package/plugins/acryl-door/plugin.json +14 -0
- package/plugins/acryl-door/src/cli/acryl.ts +207 -0
- package/plugins/acryl-door/src/host/api.ts +9 -0
- package/plugins/acryl-door/src/host/audit.ts +64 -0
- package/plugins/acryl-door/src/host/channel.ts +142 -0
- package/plugins/acryl-door/src/host/chat-tools.ts +39 -0
- package/plugins/acryl-door/src/host/door-file.ts +94 -0
- package/plugins/acryl-door/src/host/door.ts +195 -0
- package/plugins/acryl-door/src/host/http.ts +80 -0
- package/plugins/acryl-door/src/host/mcp-routes.ts +83 -0
- package/plugins/acryl-door/src/host/operator-routes.ts +38 -0
- package/plugins/acryl-door/src/host/page-routes.ts +65 -0
- package/plugins/acryl-door/src/host/shim.ts +65 -0
- package/plugins/acryl-door/src/host/verbs.ts +109 -0
- package/plugins/acryl-door/src/index.ts +133 -0
- package/plugins/acryl-door/src/page/api.ts +9 -0
- package/plugins/acryl-door/src/page/channel.ts +135 -0
- package/plugins/acryl-door/src/page/driver/accessibility.ts +163 -0
- package/plugins/acryl-door/src/page/driver/actions.ts +186 -0
- package/plugins/acryl-door/src/page/driver/driver.ts +153 -0
- package/plugins/acryl-door/src/page/driver/ref-table.ts +78 -0
- package/plugins/acryl-door/src/page/driver/sensitivity.ts +49 -0
- package/plugins/acryl-door/src/page/driver/snapshot.ts +132 -0
- package/plugins/acryl-door/src/page/index.ts +40 -0
- package/plugins/acryl-door/src/page/indicator.ts +72 -0
- package/plugins/acryl-door/src/page/main.ts +7 -0
- package/plugins/acryl-door/src/page/protocol.ts +213 -0
- package/plugins/acryl-door/src/page/snapshot-text.ts +18 -0
- package/plugins/acryl-door/src/page/user-input.ts +24 -0
- package/plugins/acryl-door/upstream.json +28 -0
- package/plugins/acryl-inspect/upstream.json +1 -1
- package/plugins/acryl-kv/plugin.json +7 -2
- package/plugins/acryl-kv/src/conformance.ts +226 -0
- package/plugins/acryl-kv/upstream.json +1 -1
- package/plugins/acryl-kv-db/plugin.json +4 -1
- package/plugins/acryl-kv-db/upstream.json +1 -1
- package/plugins/acryl-kv-json/plugin.json +4 -1
- package/plugins/acryl-kv-json/src/open.ts +86 -22
- package/plugins/acryl-kv-json/upstream.json +2 -2
- package/plugins/acryl-node-server/src/adapter/listener.ts +5 -0
- package/plugins/acryl-node-server/src/client-modules.ts +10 -9
- 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/src/service.ts +3 -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/agent-panel/client/main.ts +159 -0
- package/plugins/agent-panel/index.ts +232 -0
- package/plugins/agent-panel/plugin.json +21 -0
- 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 +17 -3
- 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 +6 -0
- package/shared/AGENTS.core.md +10 -10
- package/shared/DEFAULTS.md +45 -0
- package/shared/SKILL.md +14 -0
- package/templates/agent/AGENTS.md +1 -1
- package/templates/agent/README.md +6 -6
- package/templates/agent/blend.yaml +3 -0
- package/templates/agent/gitignore +1 -0
- package/templates/agent/package.json +8 -8
- package/templates/agent/src/app.ts +46 -17
- package/templates/agent/src/main.ts +2 -2
- package/templates/agent/src/routes.ts +19 -6
- package/templates/agent/template.json +2 -1
- package/templates/agent/test/agent.test.ts +11 -5
- package/templates/agent/test/app.test.ts +30 -9
- package/templates/agent/test/builder.test.ts +35 -24
- package/templates/agent/test/model.test.ts +25 -20
- 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 +29 -9
- package/templates/blank/src/main.ts +1 -1
- package/templates/blank/src/routes.ts +1 -1
- package/templates/blank/test/app.test.ts +29 -10
- package/templates/blank/tsconfig.json +1 -1
- package/templates/chat/AGENTS.md +16 -0
- package/templates/chat/blend.yaml +29 -0
- package/templates/chat/gitignore +6 -0
- package/templates/chat/migrations/001-chat.sql +12 -0
- package/templates/chat/package.json +33 -0
- package/templates/chat/src/app.ts +106 -0
- package/templates/chat/src/chat-app.ts +101 -0
- package/templates/chat/src/client/chat.tsx +115 -0
- package/templates/chat/src/client/tsconfig.json +26 -0
- package/templates/chat/src/db.ts +43 -0
- package/templates/chat/src/door.ts +7 -0
- package/templates/chat/src/llm.ts +42 -0
- package/templates/chat/src/main.ts +28 -0
- package/templates/chat/src/page.tsx +40 -0
- package/templates/chat/src/routes.ts +15 -0
- package/templates/chat/src/runtime.ts +17 -0
- package/templates/chat/template.json +11 -0
- package/templates/chat/test/chat.test.ts +198 -0
- package/templates/chat/test/server.test.ts +78 -0
- package/templates/chat/tsconfig.json +20 -0
- package/templates/nodejs/AGENTS.md +9 -0
- package/templates/nodejs/blend.yaml +26 -0
- package/templates/nodejs/gitignore +3 -0
- package/templates/nodejs/package.json +30 -0
- package/templates/nodejs/src/app.ts +92 -0
- package/templates/nodejs/src/client/counter.tsx +12 -0
- package/templates/nodejs/src/client/tsconfig.json +26 -0
- package/templates/nodejs/src/door.ts +7 -0
- package/templates/nodejs/src/main.ts +25 -0
- package/templates/nodejs/src/page.tsx +29 -0
- package/templates/nodejs/src/routes.ts +40 -0
- package/templates/nodejs/template.json +3 -0
- package/templates/nodejs/test/app.test.ts +78 -0
- package/templates/nodejs/test/server.test.ts +83 -0
- package/templates/nodejs/tsconfig.json +20 -0
- package/templates/notes/AGENTS.md +26 -0
- package/templates/notes/blend.yaml +29 -0
- package/templates/notes/gitignore +6 -0
- package/templates/notes/migrations/001-notes.sql +18 -0
- package/templates/notes/package.json +33 -0
- package/templates/notes/src/app.ts +106 -0
- package/templates/notes/src/auth.ts +57 -0
- package/templates/notes/src/client/counter.tsx +21 -0
- package/templates/notes/src/client/tsconfig.json +26 -0
- package/templates/notes/src/db.ts +50 -0
- package/templates/notes/src/door.ts +7 -0
- package/templates/notes/src/main.ts +28 -0
- package/templates/notes/src/notes-app.ts +205 -0
- package/templates/notes/src/routes.ts +14 -0
- package/templates/notes/src/runtime.ts +17 -0
- package/templates/notes/src/schema.ts +38 -0
- package/templates/notes/src/views.tsx +136 -0
- package/templates/notes/template.json +11 -0
- package/templates/notes/test/notes.test.ts +280 -0
- package/templates/notes/test/server.test.ts +78 -0
- package/templates/notes/tsconfig.json +20 -0
- 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 +32 -12
- package/templates/todo/src/main.ts +1 -1
- package/templates/todo/src/todo.ts +4 -4
- package/templates/todo/test/todo.test.ts +48 -23
- package/templates/todo/tsconfig.json +1 -1
- package/templates/todo/upstream.json +2 -2
- package/lib/database-D_UN6We8.d.ts +0 -44
- package/lib/kv-DWDm0Lja.d.ts +0 -39
|
@@ -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
|
+
}
|
|
@@ -198,6 +198,9 @@ export class Router extends Service {
|
|
|
198
198
|
}
|
|
199
199
|
;(handler as unknown as Record<string, unknown>)[COMPOSED_HANDLER] = inner
|
|
200
200
|
tagged.on(route.method, route.path, handler)
|
|
201
|
+
// `on` registers under the base path of `tagged` (`/`), which would hide the prefix of a nested app from `basePath(c)`; keep the route's own.
|
|
202
|
+
const registered = tagged.routes.at(-1)
|
|
203
|
+
if (registered !== undefined) registered.basePath = route.basePath
|
|
201
204
|
}
|
|
202
205
|
return mount
|
|
203
206
|
}
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"source": {
|
|
3
3
|
"repo": "acryldev/acryl",
|
|
4
4
|
"path": "plugins/acryl-router",
|
|
5
|
-
"commit": "
|
|
5
|
+
"commit": "20bdc263d6f0f8febbd967007bda4cf4a802efd2"
|
|
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:f9679acf23d0a647a3f8bf9bb3cb9107eb03d25192ab401506e3a4ee570e305e",
|
|
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
|
},
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
// The panel in the page: a button at the corner opens a side panel (a shadow root, so the app's CSS cannot touch it and it cannot touch the app's). It asks the app's
|
|
2
|
+
// agent through POST /api/agent-panel/ask, shows the answer as it arrives, and reloads the page after a run that installed or removed a plugin.
|
|
3
|
+
const PREFIX = '/api/agent-panel'
|
|
4
|
+
|
|
5
|
+
interface Session {
|
|
6
|
+
token: string
|
|
7
|
+
drivers: string[]
|
|
8
|
+
}
|
|
9
|
+
interface PanelEvent {
|
|
10
|
+
event: string
|
|
11
|
+
data: Record<string, unknown>
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/** The events of a server-sent stream, as they arrive. */
|
|
15
|
+
async function* events(response: Response): AsyncGenerator<PanelEvent> {
|
|
16
|
+
const reader = response.body!.getReader()
|
|
17
|
+
const decoder = new TextDecoder()
|
|
18
|
+
let buffer = ''
|
|
19
|
+
for (;;) {
|
|
20
|
+
const { value, done } = await reader.read()
|
|
21
|
+
if (done) return
|
|
22
|
+
buffer += decoder.decode(value, { stream: true }).replace(/\r\n/gu, '\n')
|
|
23
|
+
for (let end = buffer.indexOf('\n\n'); end !== -1; end = buffer.indexOf('\n\n')) {
|
|
24
|
+
const block = buffer.slice(0, end)
|
|
25
|
+
buffer = buffer.slice(end + 2)
|
|
26
|
+
let event = 'message'
|
|
27
|
+
let data = ''
|
|
28
|
+
for (const line of block.split('\n')) {
|
|
29
|
+
if (line.startsWith('event:')) event = line.slice(6).trim()
|
|
30
|
+
else if (line.startsWith('data:')) data += line.slice(5).trim()
|
|
31
|
+
}
|
|
32
|
+
if (data !== '') yield { event, data: JSON.parse(data) as Record<string, unknown> }
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const css = `
|
|
38
|
+
:host { all: initial }
|
|
39
|
+
* { box-sizing: border-box; font: 14px system-ui, sans-serif }
|
|
40
|
+
button.open { position: fixed; right: 16px; bottom: 16px; z-index: 2147483000; border: 0; border-radius: 24px; padding: 10px 16px; background: #1f2937; color: #fff; cursor: pointer; box-shadow: 0 2px 8px #0004 }
|
|
41
|
+
section { position: fixed; top: 0; right: 0; bottom: 0; width: min(380px, 100vw); z-index: 2147483001; display: none; flex-direction: column; background: #fff; color: #111827; border-left: 1px solid #d1d5db; box-shadow: -2px 0 12px #0003 }
|
|
42
|
+
section.shown { display: flex }
|
|
43
|
+
header { display: flex; gap: 8px; align-items: center; padding: 8px 12px; border-bottom: 1px solid #e5e7eb }
|
|
44
|
+
header strong { flex: 1 }
|
|
45
|
+
#log { flex: 1; overflow: auto; padding: 12px; display: flex; flex-direction: column; gap: 8px }
|
|
46
|
+
.msg { white-space: pre-wrap; padding: 8px 10px; border-radius: 8px; background: #f3f4f6 }
|
|
47
|
+
.me { background: #dbeafe; align-self: flex-end }
|
|
48
|
+
.tool { font: 12px ui-monospace, monospace; color: #4b5563; background: none; padding: 0 }
|
|
49
|
+
.err { background: #fee2e2 }
|
|
50
|
+
form { display: flex; flex-direction: column; gap: 6px; padding: 10px 12px; border-top: 1px solid #e5e7eb }
|
|
51
|
+
textarea { width: 100%; min-height: 64px; resize: vertical; padding: 8px; border: 1px solid #d1d5db; border-radius: 6px }
|
|
52
|
+
.row { display: flex; gap: 8px; align-items: center }
|
|
53
|
+
.row button, header button { padding: 6px 12px; border: 1px solid #d1d5db; border-radius: 6px; background: #fff; cursor: pointer }
|
|
54
|
+
.row button.send { background: #1f2937; color: #fff; border-color: #1f2937 }
|
|
55
|
+
`
|
|
56
|
+
|
|
57
|
+
async function start(): Promise<void> {
|
|
58
|
+
const reply = await fetch(`${PREFIX}/session`)
|
|
59
|
+
if (!reply.ok) return
|
|
60
|
+
const session = (await reply.json()) as Session
|
|
61
|
+
|
|
62
|
+
const host = document.createElement('div')
|
|
63
|
+
host.id = 'agent-panel'
|
|
64
|
+
const shadow = host.attachShadow({ mode: 'open' })
|
|
65
|
+
shadow.innerHTML = `<style>${css}</style>
|
|
66
|
+
<button class="open" type="button">Ask the agent</button>
|
|
67
|
+
<section aria-label="Agent panel">
|
|
68
|
+
<header><strong>Agent</strong><select aria-label="Agent">${session.drivers.map((d) => `<option>${d}</option>`).join('')}</select><button class="close" type="button" aria-label="Close">×</button></header>
|
|
69
|
+
<div id="log" role="log"></div>
|
|
70
|
+
<form><textarea aria-label="Message" placeholder="What should the app do?"></textarea>
|
|
71
|
+
<div class="row"><button class="send" type="submit">Send</button><button class="stop" type="button" disabled>Stop</button></div></form>
|
|
72
|
+
</section>`
|
|
73
|
+
const $ = <T extends Element>(selector: string): T => shadow.querySelector<T>(selector)!
|
|
74
|
+
const panel = $<HTMLElement>('section')
|
|
75
|
+
const log = $<HTMLElement>('#log')
|
|
76
|
+
const input = $<HTMLTextAreaElement>('textarea')
|
|
77
|
+
const send = $<HTMLButtonElement>('.send')
|
|
78
|
+
const stop = $<HTMLButtonElement>('.stop')
|
|
79
|
+
const driver = $<HTMLSelectElement>('select')
|
|
80
|
+
const show = (open: boolean): void => {
|
|
81
|
+
panel.classList.toggle('shown', open)
|
|
82
|
+
if (open) input.focus()
|
|
83
|
+
}
|
|
84
|
+
$('button.open').addEventListener('click', () => show(true))
|
|
85
|
+
$('button.close').addEventListener('click', () => show(false))
|
|
86
|
+
const line = (text: string, kind = ''): HTMLElement => {
|
|
87
|
+
const el = document.createElement('div')
|
|
88
|
+
el.className = `msg ${kind}`
|
|
89
|
+
el.textContent = text
|
|
90
|
+
log.append(el)
|
|
91
|
+
log.scrollTop = log.scrollHeight
|
|
92
|
+
return el
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
let controller: AbortController | undefined
|
|
96
|
+
stop.addEventListener('click', () => controller?.abort())
|
|
97
|
+
shadow.querySelector('form')!.addEventListener('submit', async (submitted) => {
|
|
98
|
+
submitted.preventDefault()
|
|
99
|
+
const message = input.value.trim()
|
|
100
|
+
if (message === '' || controller) return
|
|
101
|
+
input.value = ''
|
|
102
|
+
line(message, 'me')
|
|
103
|
+
controller = new AbortController()
|
|
104
|
+
send.disabled = true
|
|
105
|
+
stop.disabled = false
|
|
106
|
+
let changed = false
|
|
107
|
+
let answer: HTMLElement | undefined
|
|
108
|
+
try {
|
|
109
|
+
const response = await fetch(`${PREFIX}/ask`, {
|
|
110
|
+
method: 'POST',
|
|
111
|
+
headers: { 'content-type': 'application/json', 'x-agent-panel-token': session.token },
|
|
112
|
+
body: JSON.stringify({ message, driver: driver.value }),
|
|
113
|
+
signal: controller.signal,
|
|
114
|
+
})
|
|
115
|
+
if (!response.ok) {
|
|
116
|
+
line(
|
|
117
|
+
((await response.json().catch(() => ({}))) as { error?: string }).error ??
|
|
118
|
+
`The panel answered ${response.status}.`,
|
|
119
|
+
'err',
|
|
120
|
+
)
|
|
121
|
+
return
|
|
122
|
+
}
|
|
123
|
+
for await (const { event, data } of events(response)) {
|
|
124
|
+
if (event === 'text') {
|
|
125
|
+
answer ??= line('')
|
|
126
|
+
answer.textContent += String(data.text ?? '')
|
|
127
|
+
} else if (event === 'tool') {
|
|
128
|
+
line(`${String(data.tool)}${data.error ? ` failed: ${String(data.error)}` : ''}`, 'tool')
|
|
129
|
+
} else if (event === 'change') {
|
|
130
|
+
changed = true
|
|
131
|
+
line(`plugin ${String(data.extension)}: ${String(data.action)}`, 'tool')
|
|
132
|
+
} else if (event === 'error') {
|
|
133
|
+
line(String(data.message ?? 'The run failed.'), 'err')
|
|
134
|
+
} else if (event === 'done') {
|
|
135
|
+
changed = changed || data.changed === true
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
} catch (error) {
|
|
139
|
+
line(
|
|
140
|
+
controller?.signal.aborted
|
|
141
|
+
? 'Stopped.'
|
|
142
|
+
: `The request failed: ${error instanceof Error ? error.message : String(error)}`,
|
|
143
|
+
'err',
|
|
144
|
+
)
|
|
145
|
+
} finally {
|
|
146
|
+
controller = undefined
|
|
147
|
+
send.disabled = false
|
|
148
|
+
stop.disabled = true
|
|
149
|
+
}
|
|
150
|
+
// A plugin was installed or removed: the new feature is served by the running app, so the page only needs to be loaded again (unless something is being typed).
|
|
151
|
+
if (changed && input.value.trim() === '') {
|
|
152
|
+
line('Reloading the page to show it.', 'tool')
|
|
153
|
+
setTimeout(() => location.reload(), 900)
|
|
154
|
+
}
|
|
155
|
+
})
|
|
156
|
+
document.body.append(host)
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
void start()
|
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The built-in agent (spec 049 slice 14): a chat panel inside the running app. The person says what they want; the app's own agent (`driver: 'agent'`) or a coding
|
|
3
|
+
* agent started in the app folder (`'claude'`, `'codex'`, `'pi'`) builds it through the door, and when a plugin was installed or removed the page reloads, so the new feature
|
|
4
|
+
* shows with no restart.
|
|
5
|
+
*
|
|
6
|
+
* - A middleware appends one script tag (`/api/agent-panel/panel.js`) to every HTML response of the routes mounted after it, so no page is edited.
|
|
7
|
+
* - `GET /api/agent-panel/session` gives the page the drivers it may use and a token; `POST /api/agent-panel/ask` runs one message and answers server-sent events:
|
|
8
|
+
* `text`, `tool`, `change`, `done`, `error`.
|
|
9
|
+
* - Only the app's own page may ask (a same-origin request carrying the token), one run at a time, and every ask is written to `.acryl/audit/agent-panel.jsonl`.
|
|
10
|
+
* - Development only: the plugin does nothing with `NODE_ENV=production` or `ACRYL_DOOR=off`.
|
|
11
|
+
*/
|
|
12
|
+
import { spawn, spawnSync } from 'node:child_process'
|
|
13
|
+
import { randomBytes } from 'node:crypto'
|
|
14
|
+
import { appendFileSync, mkdirSync } from 'node:fs'
|
|
15
|
+
import { dirname, join } from 'node:path'
|
|
16
|
+
import type { Context } from '@deepseek-ai/cordis'
|
|
17
|
+
import { clientModules } from '../acryl-node-server/src/client-modules.ts'
|
|
18
|
+
import { streamSSE } from '../acryl-router/src/helpers/index.ts'
|
|
19
|
+
import { Acryl } from '../acryl-router/src/index.ts'
|
|
20
|
+
import type {} from '../agent/index.ts'
|
|
21
|
+
import type {} from '../extensions/index.ts'
|
|
22
|
+
|
|
23
|
+
export const name = 'agent-panel'
|
|
24
|
+
export const inject = ['router', 'agent', 'extensions', 'appInstance']
|
|
25
|
+
|
|
26
|
+
const PREFIX = '/api/agent-panel'
|
|
27
|
+
const CLIENT_DIR = join(import.meta.dirname, 'client')
|
|
28
|
+
|
|
29
|
+
export type DriverName = 'agent' | 'claude' | 'codex' | 'pi'
|
|
30
|
+
|
|
31
|
+
export interface PanelConfig {
|
|
32
|
+
/** Driver programs to offer besides the app's own agent; default: the ones found on the machine. */
|
|
33
|
+
drivers?: readonly Exclude<DriverName, 'agent'>[]
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** The command line of each coding agent started headless in the app folder (the same ones the product test uses). */
|
|
37
|
+
function command(
|
|
38
|
+
driver: Exclude<DriverName, 'agent'>,
|
|
39
|
+
message: string,
|
|
40
|
+
root: string,
|
|
41
|
+
): { file: string; args: string[] } {
|
|
42
|
+
switch (driver) {
|
|
43
|
+
case 'claude':
|
|
44
|
+
return {
|
|
45
|
+
file: 'claude',
|
|
46
|
+
args: [
|
|
47
|
+
'-p',
|
|
48
|
+
message,
|
|
49
|
+
'--permission-mode',
|
|
50
|
+
'acceptEdits',
|
|
51
|
+
'--allowedTools',
|
|
52
|
+
'Read,Edit,Write,Glob,Grep,Bash(npm *),Bash(npx *),Bash(node *),Bash(.acryl/bin/acryl *),Bash(curl *)',
|
|
53
|
+
],
|
|
54
|
+
}
|
|
55
|
+
case 'codex':
|
|
56
|
+
return {
|
|
57
|
+
file: 'codex',
|
|
58
|
+
args: [
|
|
59
|
+
'exec',
|
|
60
|
+
'--skip-git-repo-check',
|
|
61
|
+
'-s',
|
|
62
|
+
'workspace-write',
|
|
63
|
+
'-c',
|
|
64
|
+
'sandbox_workspace_write.network_access=true',
|
|
65
|
+
'-C',
|
|
66
|
+
root,
|
|
67
|
+
message,
|
|
68
|
+
],
|
|
69
|
+
}
|
|
70
|
+
case 'pi':
|
|
71
|
+
return {
|
|
72
|
+
file: 'pi',
|
|
73
|
+
args: [
|
|
74
|
+
'-p',
|
|
75
|
+
message,
|
|
76
|
+
'--provider',
|
|
77
|
+
'deepseek',
|
|
78
|
+
'--model',
|
|
79
|
+
'deepseek-v4-pro',
|
|
80
|
+
'--no-session',
|
|
81
|
+
],
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function onPath(program: string): boolean {
|
|
87
|
+
const found = spawnSync(process.platform === 'win32' ? 'where' : 'which', [program], {
|
|
88
|
+
encoding: 'utf8',
|
|
89
|
+
})
|
|
90
|
+
return found.status === 0
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/** The drivers this machine can offer: the app's own agent always; a program when it is installed (pi also needs its key in the environment). */
|
|
94
|
+
export function availableDrivers(wanted?: readonly Exclude<DriverName, 'agent'>[]): DriverName[] {
|
|
95
|
+
const found: DriverName[] = ['agent']
|
|
96
|
+
for (const driver of wanted ?? (['claude', 'codex', 'pi'] as const)) {
|
|
97
|
+
if (driver === 'pi' && !process.env.DEEPSEEK_API_KEY) continue
|
|
98
|
+
if (onPath(driver)) found.push(driver)
|
|
99
|
+
}
|
|
100
|
+
return found
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/** Whether a request comes from the app's own page: a browser says so itself (`Sec-Fetch-Site: same-origin`), or the Origin is the Host. */
|
|
104
|
+
export function fromOwnPage(headers: Headers): boolean {
|
|
105
|
+
const site = headers.get('sec-fetch-site')
|
|
106
|
+
if (site !== null) return site === 'same-origin'
|
|
107
|
+
const origin = headers.get('origin')
|
|
108
|
+
const host = headers.get('host')
|
|
109
|
+
return origin !== null && host !== null && new URL(origin).host === host
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
export function apply(ctx: Context, config: PanelConfig = {}): void {
|
|
113
|
+
if (process.env.NODE_ENV === 'production' || process.env.ACRYL_DOOR === 'off') return
|
|
114
|
+
const root = ctx.appInstance.root
|
|
115
|
+
const token = randomBytes(24).toString('hex')
|
|
116
|
+
const auditFile = join(root, '.acryl', 'audit', 'agent-panel.jsonl')
|
|
117
|
+
const audit = (entry: Record<string, unknown>): void => {
|
|
118
|
+
try {
|
|
119
|
+
mkdirSync(dirname(auditFile), { recursive: true })
|
|
120
|
+
appendFileSync(auditFile, `${JSON.stringify({ at: new Date().toISOString(), ...entry })}\n`)
|
|
121
|
+
} catch {
|
|
122
|
+
// The audit never stops a run.
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
const drivers = availableDrivers(config.drivers)
|
|
126
|
+
let running = false
|
|
127
|
+
|
|
128
|
+
const tag = `<script type="module" src="${PREFIX}/panel.js"></script>`
|
|
129
|
+
const app = new Acryl()
|
|
130
|
+
// The script tag, on every HTML page the routes mounted after this plugin answer.
|
|
131
|
+
.use('*', async (c, next) => {
|
|
132
|
+
await next()
|
|
133
|
+
if (!(c.res.headers.get('content-type') ?? '').includes('text/html')) return
|
|
134
|
+
const html = await c.res.text()
|
|
135
|
+
const headers = new Headers(c.res.headers)
|
|
136
|
+
headers.delete('content-length')
|
|
137
|
+
c.res = new Response(
|
|
138
|
+
html.includes('</body>') ? html.replace('</body>', `${tag}</body>`) : html + tag,
|
|
139
|
+
{ status: c.res.status, headers },
|
|
140
|
+
)
|
|
141
|
+
})
|
|
142
|
+
.get(`${PREFIX}/panel.js`, (c) =>
|
|
143
|
+
c.body(`import '${PREFIX}/client/main.ts'\n`, 200, {
|
|
144
|
+
'content-type': 'text/javascript; charset=utf-8',
|
|
145
|
+
'cache-control': 'no-cache',
|
|
146
|
+
}),
|
|
147
|
+
)
|
|
148
|
+
.route(`${PREFIX}/client`, clientModules({ root: CLIENT_DIR }))
|
|
149
|
+
.get(`${PREFIX}/session`, (c) => {
|
|
150
|
+
if (!fromOwnPage(c.req.raw.headers))
|
|
151
|
+
return c.json({ error: "only the app's own page may use the panel" }, 403)
|
|
152
|
+
return c.json({ token, drivers })
|
|
153
|
+
})
|
|
154
|
+
.post(`${PREFIX}/ask`, async (c) => {
|
|
155
|
+
if (!fromOwnPage(c.req.raw.headers) || c.req.header('x-agent-panel-token') !== token)
|
|
156
|
+
return c.json({ error: "only the app's own page may ask" }, 403)
|
|
157
|
+
const body = (await c.req.json().catch(() => ({}))) as { message?: unknown; driver?: unknown }
|
|
158
|
+
const message = typeof body.message === 'string' ? body.message.trim() : ''
|
|
159
|
+
const driver = (typeof body.driver === 'string' ? body.driver : 'agent') as DriverName
|
|
160
|
+
if (message === '' || message.length > 8000)
|
|
161
|
+
return c.json({ error: 'write a message of at most 8000 characters' }, 400)
|
|
162
|
+
if (!drivers.includes(driver))
|
|
163
|
+
return c.json(
|
|
164
|
+
{ error: `the driver "${driver}" is not available here (${drivers.join(', ')})` },
|
|
165
|
+
400,
|
|
166
|
+
)
|
|
167
|
+
if (running) return c.json({ error: 'a run is already going; wait for it to end' }, 409)
|
|
168
|
+
running = true
|
|
169
|
+
return streamSSE(c, async (stream) => {
|
|
170
|
+
const send = (event: string, data: unknown): Promise<void> =>
|
|
171
|
+
stream.writeSSE({ event, data: JSON.stringify(data) })
|
|
172
|
+
let changed = false
|
|
173
|
+
const off = ctx.on('extensions/changed', (extension: string, action: string) => {
|
|
174
|
+
changed = true
|
|
175
|
+
void send('change', { extension, action })
|
|
176
|
+
})
|
|
177
|
+
audit({ ask: 'start', driver, characters: message.length })
|
|
178
|
+
try {
|
|
179
|
+
if (driver === 'agent') {
|
|
180
|
+
const result = await ctx.agent.run(message)
|
|
181
|
+
for (const call of result.trace)
|
|
182
|
+
await send('tool', {
|
|
183
|
+
tool: call.tool,
|
|
184
|
+
input: call.input,
|
|
185
|
+
...(call.error ? { error: call.error } : { result: call.result }),
|
|
186
|
+
})
|
|
187
|
+
await send('text', {
|
|
188
|
+
text:
|
|
189
|
+
result.status === 'completed'
|
|
190
|
+
? result.output
|
|
191
|
+
: result.status === 'cancelled'
|
|
192
|
+
? 'The run was cancelled.'
|
|
193
|
+
: `The run stopped after ${result.steps} steps without an answer.`,
|
|
194
|
+
})
|
|
195
|
+
} else {
|
|
196
|
+
const { file, args } = command(driver, message, root)
|
|
197
|
+
const child = spawn(file, args, {
|
|
198
|
+
cwd: root,
|
|
199
|
+
env: process.env,
|
|
200
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
201
|
+
})
|
|
202
|
+
stream.onAbort(() => {
|
|
203
|
+
child.kill('SIGTERM')
|
|
204
|
+
})
|
|
205
|
+
child.stdout.on(
|
|
206
|
+
'data',
|
|
207
|
+
(chunk: Buffer) => void send('text', { text: chunk.toString() }),
|
|
208
|
+
)
|
|
209
|
+
child.stderr.on(
|
|
210
|
+
'data',
|
|
211
|
+
(chunk: Buffer) =>
|
|
212
|
+
void send('tool', { tool: driver, info: chunk.toString().slice(0, 500) }),
|
|
213
|
+
)
|
|
214
|
+
const code = await new Promise<number>((done) => {
|
|
215
|
+
child.on('error', () => done(1))
|
|
216
|
+
child.on('exit', (exit) => done(exit ?? 1))
|
|
217
|
+
})
|
|
218
|
+
if (code !== 0)
|
|
219
|
+
await send('error', { message: `${driver} ended with exit code ${code}` })
|
|
220
|
+
}
|
|
221
|
+
await send('done', { changed })
|
|
222
|
+
} catch (error) {
|
|
223
|
+
await send('error', { message: error instanceof Error ? error.message : String(error) })
|
|
224
|
+
} finally {
|
|
225
|
+
off()
|
|
226
|
+
running = false
|
|
227
|
+
audit({ ask: 'end', driver, changed })
|
|
228
|
+
}
|
|
229
|
+
})
|
|
230
|
+
})
|
|
231
|
+
ctx.router.route('/', app)
|
|
232
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "agent-panel",
|
|
3
|
+
"description": "A chat panel inside the running app: the person writes what they want, the app's own agent (or Claude Code, Codex or pi started in the app folder) builds it through the same door, and the page reloads when a plugin was installed, with no restart. Development only; opt-in with acrylblends add agent-panel.",
|
|
4
|
+
"requires": [
|
|
5
|
+
"acryl-router",
|
|
6
|
+
"acryl-node-server",
|
|
7
|
+
"agent",
|
|
8
|
+
"builder",
|
|
9
|
+
"extensions",
|
|
10
|
+
"sandbox",
|
|
11
|
+
"tasks",
|
|
12
|
+
"instance"
|
|
13
|
+
],
|
|
14
|
+
"inject": ["router", "agent", "extensions", "appInstance"],
|
|
15
|
+
"provides": [],
|
|
16
|
+
"dependencies": {
|
|
17
|
+
"@deepseek-ai/cordis": "4.0.4"
|
|
18
|
+
},
|
|
19
|
+
"mount": "add a row `- id: agent-panel` / `name: agent-panel` to blend.yaml BEFORE the `routes` row (its middleware adds one script tag to every HTML page the routes after it answer) and register it in src/app.ts: 'agent-panel': panel; open the app in a browser and use the button at the corner of the page",
|
|
20
|
+
"gitignore": [".acryl/"]
|
|
21
|
+
}
|
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": "20bdc263d6f0f8febbd967007bda4cf4a802efd2"
|
|
9
9
|
},
|
|
10
10
|
"rewrittenSpecifiers": 0,
|
|
11
11
|
"digest": "sha256:7dfe960a8bbb476c35540d2202a7aaa62b33492c2ac3e34314e2ecfca45f32f2"
|