acrylblends 1.1.0 → 1.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +9 -0
- package/dist/index.d.ts +47 -2
- package/dist/index.js +2017 -32
- package/dist/index.js.map +1 -1
- package/docs/framework/agents/index.md +1 -1
- package/docs/framework/getting-started/desktop.md +23 -4
- package/docs/framework/guides/create-acrylblends.md +5 -3
- package/docs/framework/workshop/README.md +139 -153
- package/docs/framework/workshop/facilitator.md +69 -101
- package/legacy/README.md +9 -0
- package/legacy/agent/157ce3db1112/src/app.ts +98 -0
- package/legacy/agent/157ce3db1112/src/routes.ts +63 -0
- package/legacy/agent/24c681a00fca/src/app.ts +160 -0
- package/legacy/agent/24c681a00fca/src/routes.ts +83 -0
- package/legacy/agent/2e790da977b2/src/app.ts +98 -0
- package/legacy/agent/2e790da977b2/src/routes.ts +63 -0
- package/legacy/agent/6307045545dc/src/app.ts +168 -0
- package/legacy/agent/6307045545dc/src/routes.ts +83 -0
- package/legacy/agent/6a5fb9c15c53/src/app.ts +158 -0
- package/legacy/agent/6a5fb9c15c53/src/routes.ts +83 -0
- package/legacy/agent/71fb54a5a942/src/app.ts +111 -0
- package/legacy/agent/71fb54a5a942/src/routes.ts +63 -0
- package/legacy/agent/74ece148269c/src/app.ts +168 -0
- package/legacy/agent/74ece148269c/src/routes.ts +93 -0
- package/legacy/agent/855359304707/src/app.ts +168 -0
- package/legacy/agent/855359304707/src/routes.ts +126 -0
- package/legacy/agent/b5ef3c786ae4/src/app.ts +127 -0
- package/legacy/agent/b5ef3c786ae4/src/routes.ts +76 -0
- package/legacy/agent/c2f30958043b/src/app.ts +98 -0
- package/legacy/agent/c2f30958043b/src/routes.ts +53 -0
- package/legacy/blank/5a91c558af22/src/routes.ts +18 -0
- package/legacy/blank/9c650cf52bf3/src/routes.ts +14 -0
- package/legacy/blank/aea589283445/src/routes.ts +16 -0
- package/legacy/blank/b2cbb759c889/src/routes.ts +16 -0
- package/legacy/blank/d6d371c9a65e/src/routes.ts +18 -0
- package/legacy/chat/7076cc821e2c/src/app.ts +72 -0
- package/legacy/chat/7076cc821e2c/src/routes.ts +13 -0
- package/legacy/chat/7dbe6d5170df/src/app.ts +107 -0
- package/legacy/chat/7dbe6d5170df/src/routes.ts +19 -0
- package/legacy/chat/a9a9ab44c58e/src/app.ts +107 -0
- package/legacy/chat/a9a9ab44c58e/src/routes.ts +15 -0
- package/legacy/chat/f20ad9a51b06/src/app.ts +106 -0
- package/legacy/chat/f20ad9a51b06/src/routes.ts +15 -0
- package/legacy/layouts.json +524 -0
- package/legacy/nodejs/1d477f37aaf4/src/app.ts +92 -0
- package/legacy/nodejs/1d477f37aaf4/src/routes.ts +40 -0
- package/legacy/nodejs/b1bd9462496c/src/app.ts +61 -0
- package/legacy/nodejs/b1bd9462496c/src/routes.ts +37 -0
- package/legacy/nodejs/d5bab75076ef/src/app.ts +95 -0
- package/legacy/nodejs/d5bab75076ef/src/routes.ts +40 -0
- package/legacy/notes/4edc36949c15/src/app.ts +107 -0
- package/legacy/notes/4edc36949c15/src/routes.ts +14 -0
- package/legacy/notes/6cab2d09747c/src/app.ts +72 -0
- package/legacy/notes/6cab2d09747c/src/routes.ts +12 -0
- package/legacy/notes/7ca2dfed623a/src/app.ts +106 -0
- package/legacy/notes/7ca2dfed623a/src/routes.ts +14 -0
- package/legacy/todo/02e613b3625c/src/app.ts +71 -0
- package/legacy/todo/02e613b3625c/src/todo.ts +123 -0
- package/legacy/todo/45694a8d9e15/src/app.ts +71 -0
- package/legacy/todo/45694a8d9e15/src/todo.ts +96 -0
- package/legacy/todo/5923d3a6099f/src/app.ts +71 -0
- package/legacy/todo/5923d3a6099f/src/todo.ts +125 -0
- package/legacy/todo/849a18d66dc1/src/app.ts +91 -0
- package/legacy/todo/849a18d66dc1/src/todo.ts +125 -0
- package/legacy/todo/8e64211312f3/src/app.ts +71 -0
- package/legacy/todo/8e64211312f3/src/todo.ts +125 -0
- package/legacy/todo/b79078a87565/src/app.ts +71 -0
- package/legacy/todo/b79078a87565/src/todo.ts +125 -0
- package/lib/{audit-BFpULBCj.d.ts → audit-Bok_UNCn.d.ts} +26 -3
- package/lib/{client-modules-drj4mjhj.js → client-modules-Bkdr83jO.js} +1 -1
- package/lib/cordis/door.d.ts +87 -9
- package/lib/cordis/door.js +324 -60
- package/lib/cordis/inspect.js +1 -1
- package/lib/cordis/node-server.d.ts +101 -3
- package/lib/cordis/node-server.js +3 -3
- package/lib/cordis/router-http.d.ts +7 -1
- package/lib/cordis/router-http.js +44 -3
- package/lib/cordis/router.d.ts +1 -1
- package/lib/cordis/router.js +1 -1
- package/lib/cordis/state.js +1 -1
- package/lib/door/host.d.ts +2 -2
- package/lib/door/host.js +1 -1
- package/lib/door/page.d.ts +10 -1
- package/lib/door/page.js +46 -7
- package/lib/door/protocol.d.ts +1 -1
- package/lib/drivers.d.ts +10 -1
- package/lib/{index-DMOtA-NJ.d.ts → index-DGq7H0lb.d.ts} +11 -1
- package/lib/node-server.d.ts +7 -1
- package/lib/node-server.js +2 -2
- package/lib/{protocol-a2aqrp2X.d.ts → protocol-BeIYCgTo.d.ts} +4 -0
- package/lib/{snapshot-text-BwM6T2nG.js → snapshot-text-CXTnjxGx.js} +87 -7
- package/lib/src-CtkIX_Gg.js +427 -0
- package/lib/{src-BTBm4OPD.js → src-iBJ2ysV9.js} +77 -14
- package/lib/{test-websocket-BLeEvybj.js → test-websocket-CQOSvBth.js} +17 -7
- package/lib/{test-websocket-DyeS50Tr.d.ts → test-websocket-GUTAkFQT.d.ts} +3 -1
- package/package.json +2 -1
- package/plugins/acryl-door/plugin.json +3 -0
- package/plugins/acryl-door/src/cli/acryl.ts +63 -6
- package/plugins/acryl-door/src/host/channel.ts +86 -9
- package/plugins/acryl-door/src/host/chat-tools.ts +8 -6
- package/plugins/acryl-door/src/host/door-file.ts +7 -2
- package/plugins/acryl-door/src/host/door.ts +128 -24
- package/plugins/acryl-door/src/host/operator-routes.ts +21 -1
- package/plugins/acryl-door/src/host/page-routes.ts +3 -1
- package/plugins/acryl-door/src/host/page-socket.ts +18 -8
- package/plugins/acryl-door/src/host/verbs.ts +14 -1
- package/plugins/acryl-door/src/index.ts +168 -30
- package/plugins/acryl-door/src/page/base.ts +32 -0
- package/plugins/acryl-door/src/page/channel.ts +3 -1
- package/plugins/acryl-door/src/page/driver/accessibility.ts +11 -0
- package/plugins/acryl-door/src/page/driver/actions.ts +4 -0
- package/plugins/acryl-door/src/page/driver/driver.ts +32 -4
- package/plugins/acryl-door/src/page/driver/snapshot.ts +5 -2
- package/plugins/acryl-door/src/page/index.ts +41 -2
- package/plugins/acryl-door/src/page/outline.ts +56 -0
- package/plugins/acryl-door/src/page/protocol.ts +4 -0
- package/plugins/acryl-door/src/page/snapshot-text.ts +2 -1
- package/plugins/acryl-node-server/src/adapter/listener.ts +4 -0
- package/plugins/acryl-node-server/src/adapter/raw.ts +18 -0
- package/plugins/acryl-node-server/src/adapter/websocket.ts +15 -9
- package/plugins/acryl-node-server/src/client-modules.ts +7 -1
- package/plugins/acryl-node-server/src/index.ts +304 -16
- package/plugins/acryl-node-server/src/upgrade-hub.ts +2 -2
- package/plugins/acryl-page/plugin.json +12 -0
- package/plugins/acryl-page/src/index.ts +163 -0
- package/plugins/acryl-router/src/service.ts +77 -14
- package/plugins/acryl-router-http/src/index.ts +15 -1
- package/plugins/acryl-router-http/src/serve.ts +31 -0
- package/plugins/acryl-web-server/plugin.json +14 -0
- package/plugins/acryl-web-server/src/index.ts +164 -0
- package/plugins/agent/agent.ts +38 -7
- package/plugins/agent/index.ts +1 -1
- package/plugins/agent/model.ts +10 -2
- package/plugins/agent/tools.ts +9 -3
- package/plugins/agent-panel/client/describe.ts +57 -0
- package/plugins/agent-panel/client/driver-pick.ts +22 -0
- package/plugins/agent-panel/client/frame.ts +51 -0
- package/plugins/agent-panel/client/main.ts +238 -26
- package/plugins/agent-panel/drivers.ts +7 -2
- package/plugins/agent-panel/headless.ts +5 -2
- package/plugins/agent-panel/index.ts +146 -57
- package/plugins/agent-panel/mount.ts +27 -0
- package/plugins/agent-panel/visitors.ts +231 -0
- package/plugins/builder/index.ts +13 -35
- package/plugins/builder/package.ts +186 -0
- package/plugins/builder/plugin.json +3 -1
- package/plugins/desktop/frame.ts +231 -0
- package/plugins/desktop/launch.ts +44 -9
- package/plugins/desktop/main.ts +100 -32
- package/plugins/desktop/window.ts +21 -4
- package/plugins/driver-pty/client/terminal.ts +4 -1
- package/plugins/driver-pty/index.ts +38 -11
- package/plugins/driver-pty/spawn.ts +38 -0
- package/plugins/driver-pty/terminal.ts +2 -0
- package/plugins/extensions/index.ts +47 -18
- package/plugins/extensions/plugin-store.ts +27 -0
- package/plugins/extensions/plugin.json +1 -1
- package/plugins/extensions/resolve.ts +13 -0
- package/plugins/extensions/stage.ts +15 -5
- package/plugins/feedback/client.d.ts +22 -0
- package/plugins/feedback/client.js +471 -0
- package/plugins/feedback/index.ts +143 -0
- package/plugins/feedback/plugin.json +11 -0
- package/plugins/feedback/store.ts +307 -0
- package/plugins/home/index.ts +122 -0
- package/plugins/home/plugin.json +11 -0
- package/plugins/instance/index.ts +6 -0
- package/plugins/manifest/app-plugins.ts +86 -0
- package/plugins/manifest/index.ts +12 -2
- package/plugins/manifest/plugin.json +1 -1
- package/plugins/manifest/snapshot.ts +275 -0
- package/plugins/openai/index.ts +64 -5
- package/plugins/openai/prompt.ts +23 -0
- package/plugins/plugin-share/README.md +51 -0
- package/plugins/plugin-share/index.ts +467 -0
- package/plugins/plugin-share/plugin.json +15 -0
- package/plugins/plugin-share/publish.ts +65 -0
- package/plugins/plugin-share/pull-check.ts +129 -0
- package/plugins/plugin-share/pulled-records.ts +70 -0
- package/shared/AGENTS.core.md +36 -16
- package/shared/skills/acryl-add-ui/SKILL.md +19 -0
- package/shared/skills/acryl-app/SKILL.md +13 -0
- package/shared/skills/acryl-build-plugin/SKILL.md +22 -0
- package/shared/skills/acryl-change-plugin/SKILL.md +14 -0
- package/shared/skills/acryl-diagnose-plugin/SKILL.md +13 -0
- package/shared/skills/acryl-fix-plugin/SKILL.md +13 -0
- package/shared/skills/acryl-remove-plugin/SKILL.md +12 -0
- package/shared/skills/acryl-share-plugin/SKILL.md +16 -0
- package/shared/skills/cordis-plugin/SKILL.md +26 -0
- package/templates/agent/.gitattributes +2 -0
- package/templates/agent/AGENTS.md +1 -1
- package/templates/agent/blend.yaml +6 -0
- package/templates/agent/gitignore +1 -0
- package/templates/agent/package.json +1 -0
- package/templates/agent/src/app.ts +154 -33
- package/templates/agent/src/frame.ts +17 -0
- package/templates/agent/src/main.ts +5 -3
- package/templates/agent/src/routes.ts +38 -19
- package/templates/agent/template.json +3 -0
- package/templates/agent/test/app.test.ts +45 -3
- package/templates/blank/.gitattributes +2 -0
- package/templates/blank/AGENTS.md +3 -2
- package/templates/blank/blend.yaml +11 -0
- package/templates/blank/gitignore +1 -0
- package/templates/blank/package.json +1 -0
- package/templates/blank/src/app.ts +100 -29
- package/templates/blank/src/door.ts +7 -0
- package/templates/blank/src/first-page.ts +5 -0
- package/templates/blank/src/frame.ts +17 -0
- package/templates/blank/src/routes.ts +65 -8
- package/templates/blank/template.json +14 -1
- package/templates/blank/test/app.test.ts +103 -16
- package/templates/chat/.gitattributes +2 -0
- package/templates/chat/blend.yaml +2 -1
- package/templates/chat/gitignore +1 -0
- package/templates/chat/package.json +1 -0
- package/templates/chat/src/app.ts +72 -22
- package/templates/chat/src/chat-app.ts +11 -2
- package/templates/chat/src/client/chat.tsx +4 -2
- package/templates/chat/src/frame.ts +17 -0
- package/templates/chat/src/page.tsx +6 -4
- package/templates/chat/src/routes.ts +14 -4
- package/templates/chat/src/runtime.ts +9 -3
- package/templates/chat/template.json +3 -1
- package/templates/chat/test/chat.test.ts +46 -1
- package/templates/chat/test/server.test.ts +17 -3
- package/templates/nodejs/.gitattributes +2 -0
- package/templates/nodejs/blend.yaml +2 -1
- package/templates/nodejs/gitignore +1 -0
- package/templates/nodejs/package.json +1 -0
- package/templates/nodejs/src/app.ts +76 -21
- package/templates/nodejs/src/frame.ts +17 -0
- package/templates/nodejs/src/page.tsx +3 -3
- package/templates/nodejs/src/routes.ts +32 -13
- package/templates/nodejs/template.json +3 -1
- package/templates/nodejs/test/app.test.ts +38 -3
- package/templates/nodejs/test/server.test.ts +17 -3
- package/templates/notes/.gitattributes +2 -0
- package/templates/notes/blend.yaml +2 -1
- package/templates/notes/gitignore +1 -0
- package/templates/notes/package.json +1 -0
- package/templates/notes/src/app.ts +72 -22
- package/templates/notes/src/frame.ts +17 -0
- package/templates/notes/src/notes-app.ts +24 -17
- package/templates/notes/src/routes.ts +11 -4
- package/templates/notes/src/runtime.ts +9 -3
- package/templates/notes/src/views.tsx +26 -17
- package/templates/notes/template.json +3 -1
- package/templates/notes/test/notes.test.ts +39 -1
- package/templates/notes/test/server.test.ts +17 -3
- package/templates/todo/.gitattributes +2 -0
- package/templates/todo/blend.yaml +3 -0
- package/templates/todo/gitignore +1 -0
- package/templates/todo/package.json +1 -0
- package/templates/todo/src/app.ts +81 -27
- package/templates/todo/src/door.ts +7 -0
- package/templates/todo/src/frame.ts +17 -0
- package/templates/todo/src/page.ts +13 -6
- package/templates/todo/src/todo.ts +18 -6
- package/templates/todo/template.json +11 -1
- package/templates/todo/test/todo.test.ts +67 -9
- package/lib/src-LX0E4ec4.js +0 -117
- package/shared/SKILL.md +0 -14
|
@@ -73,12 +73,17 @@ function alive(pid: number): boolean {
|
|
|
73
73
|
* Write this run's connection file, replacing a stale one.
|
|
74
74
|
* @throws when another live process already serves this folder's door: two apps from one folder would race for one file.
|
|
75
75
|
*/
|
|
76
|
-
|
|
77
|
-
|
|
76
|
+
/** @throws when another live process already serves this folder's door: two apps from one folder would race for one file. */
|
|
77
|
+
export function assertDoorIsFree(appRoot: string): void {
|
|
78
78
|
const existing = readDoorFile(appRoot)
|
|
79
79
|
if (existing !== undefined && existing.pid !== process.pid && alive(existing.pid)) {
|
|
80
80
|
throw new Error(`acryl-door: another process (pid ${String(existing.pid)}) serves the door of ${appRoot}; stop it first`)
|
|
81
81
|
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export function writeDoorFile(appRoot: string, url: string, secret: string): string {
|
|
85
|
+
const dir = ensureStateDir(appRoot)
|
|
86
|
+
assertDoorIsFree(appRoot)
|
|
82
87
|
const file = doorFilePath(appRoot)
|
|
83
88
|
const temporary = join(dir, `${DOOR_FILE}.${String(process.pid)}.tmp`)
|
|
84
89
|
const content: DoorFile = { version: 1, url, secret, pid: process.pid }
|
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
|
|
7
7
|
import { existsSync } from 'node:fs'
|
|
8
8
|
import { isAbsolute, relative, resolve } from 'node:path'
|
|
9
|
+
import { underAppBase } from '../page/base.ts'
|
|
9
10
|
import { parseUiRequest, UiControlError, type AuditEntry, type UiActionResult, type UiRequest, type UiResult, type UiSnapshot } from '../page/protocol.ts'
|
|
10
11
|
import { renderSnapshot } from '../page/snapshot-text.ts'
|
|
11
12
|
import type { AuditLog } from './audit.ts'
|
|
@@ -14,7 +15,7 @@ import type { UiChannel } from './channel.ts'
|
|
|
14
15
|
import { NO_DESKTOP, type ShellChannel } from './shell.ts'
|
|
15
16
|
import { VERB_SPECS, VERBS } from './verbs.ts'
|
|
16
17
|
|
|
17
|
-
export type DoorErrorCode = 'invalid' | 'unknown-verb' | 'unavailable' | 'not-exposed' | 'outside-app' | 'failed' | ShellErrorCode
|
|
18
|
+
export type DoorErrorCode = 'invalid' | 'unknown-verb' | 'unavailable' | 'not-exposed' | 'outside-app' | 'failed' | 'refused' | ShellErrorCode
|
|
18
19
|
|
|
19
20
|
export class DoorError extends Error {
|
|
20
21
|
readonly code: DoorErrorCode
|
|
@@ -34,10 +35,45 @@ export interface ExtensionsPort {
|
|
|
34
35
|
list(): readonly unknown[]
|
|
35
36
|
}
|
|
36
37
|
|
|
38
|
+
/** The slice of acrylblends' `pluginShare` service the pull verb uses. */
|
|
39
|
+
export interface PluginSharePort {
|
|
40
|
+
pull(source: string): Promise<{ readonly ok: boolean; readonly action?: string; readonly reason?: string; readonly next?: string }>
|
|
41
|
+
planPublish(name: string, options?: PublishOptionsView): Promise<PublishPlanView | { readonly ok: false; readonly reason: string; readonly next: string }>
|
|
42
|
+
publish(name: string, confirmed: PublishPlanView, options?: PublishOptionsView): Promise<{ readonly ok: boolean; readonly reason?: string }>
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** Who a plan publishes as, and under which license; the service's defaults when absent. */
|
|
46
|
+
export interface PublishOptionsView {
|
|
47
|
+
readonly owner?: string
|
|
48
|
+
readonly license?: string
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** A plan as the door shows it. The service owns it; the door passes it back unchanged. */
|
|
52
|
+
export interface PublishPlanView {
|
|
53
|
+
readonly owner: string
|
|
54
|
+
readonly repo: string
|
|
55
|
+
readonly version: string
|
|
56
|
+
readonly visibility: string
|
|
57
|
+
readonly license: string
|
|
58
|
+
readonly createsRepository: boolean
|
|
59
|
+
readonly files: readonly string[]
|
|
60
|
+
readonly digest: string
|
|
61
|
+
readonly pullLine: string
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** What a plan says, for a person deciding. */
|
|
65
|
+
export function renderPublishPlan(plan: PublishPlanView): string {
|
|
66
|
+
return [
|
|
67
|
+
`Publish to ${plan.owner}/${plan.repo}${plan.createsRepository ? ' (a new repository)' : ''}, ${plan.visibility}, version ${plan.version}, license ${plan.license}.`,
|
|
68
|
+
`Files: ${plan.files.join(', ')}.`,
|
|
69
|
+
`A friend gets exactly these bytes with: ${plan.pullLine}`,
|
|
70
|
+
].join('\n')
|
|
71
|
+
}
|
|
72
|
+
|
|
37
73
|
/** The slice of acrylblends' `tools` registry (the `agent` plugin's) the tool verbs use, and where the built-in chat finds the verbs. */
|
|
38
74
|
export interface ToolsPort {
|
|
39
75
|
/** @returns the function that removes the tool. @throws when the name is taken. */
|
|
40
|
-
register(name: string, tool: { readonly description: string; readonly parameters?: Readonly<Record<string, unknown>>; run(input: unknown): unknown }): () => void
|
|
76
|
+
register(name: string, tool: { readonly description: string; readonly parameters?: Readonly<Record<string, unknown>>; run(input: unknown, options?: { signal?: AbortSignal; page?: string }): unknown }): () => void
|
|
41
77
|
list(): ReadonlyArray<{ readonly name: string; readonly description: string }>
|
|
42
78
|
call(name: string, input: unknown): Promise<unknown>
|
|
43
79
|
}
|
|
@@ -49,8 +85,12 @@ export interface DoorDeps {
|
|
|
49
85
|
readonly audit: AuditLog
|
|
50
86
|
/** The app's name, folder and, when it listens, its address (what `brief` prints so an agent can curl the running app). */
|
|
51
87
|
readonly app: { readonly name: string; readonly root: string; readonly url?: string | undefined }
|
|
88
|
+
/** The prefix the app's pages answer under (the frame's canvas, `/app`); absent on the app's own server, at the root. */
|
|
89
|
+
readonly appBase?: string | undefined
|
|
52
90
|
/** Read at each call: the services may come and go while the door runs. */
|
|
53
91
|
readonly extensions: () => ExtensionsPort | undefined
|
|
92
|
+
/** Read at each call, as `extensions` is; absent where the app has no plugin-share row. */
|
|
93
|
+
readonly pluginShare?: () => PluginSharePort | undefined
|
|
54
94
|
readonly tools: () => ToolsPort | undefined
|
|
55
95
|
/** The app tools the door offers; anything else is refused before it reaches the registry. */
|
|
56
96
|
readonly expose: readonly string[]
|
|
@@ -60,6 +100,11 @@ export interface DoorDeps {
|
|
|
60
100
|
readonly pageBackMs?: number
|
|
61
101
|
}
|
|
62
102
|
|
|
103
|
+
/** Who a verb runs for: `page`, the door's id of the page a run was asked from; its look and act go to that page alone (followed into its tab), never to another one. */
|
|
104
|
+
export interface VerbScope {
|
|
105
|
+
readonly page?: string
|
|
106
|
+
}
|
|
107
|
+
|
|
63
108
|
/** What one verb answered: the value for `--json` and programs, the text for an agent reading a terminal. */
|
|
64
109
|
export interface VerbAnswer {
|
|
65
110
|
readonly value: unknown
|
|
@@ -96,20 +141,22 @@ export class Door {
|
|
|
96
141
|
}
|
|
97
142
|
|
|
98
143
|
/** Run one verb. @throws DoorError or UiControlError. */
|
|
99
|
-
async run(verb: string, args: unknown, signal: AbortSignal): Promise<VerbAnswer> {
|
|
144
|
+
async run(verb: string, args: unknown, signal: AbortSignal, scope: VerbScope = {}): Promise<VerbAnswer> {
|
|
100
145
|
const input = (typeof args === 'object' && args !== null && !Array.isArray(args) ? args : {}) as Record<string, unknown>
|
|
101
146
|
switch (verb) {
|
|
102
147
|
case 'brief': return this.brief()
|
|
103
148
|
case 'look': {
|
|
104
|
-
const snapshot = await this.look(input, signal)
|
|
149
|
+
const snapshot = await this.look(input, signal, scope)
|
|
105
150
|
return { value: snapshot, text: renderSnapshot(snapshot, cursor => `run look --cursor ${String(cursor)}`) }
|
|
106
151
|
}
|
|
107
152
|
case 'act': {
|
|
108
|
-
const result = await this.act(input, signal)
|
|
153
|
+
const result = await this.act(input, signal, scope)
|
|
109
154
|
return { value: result, text: `done${result.target === undefined ? '' : `: ${result.target.role} "${result.target.name}"`}${result.detail === undefined ? '' : ` (${result.detail})`}; look again before the next act` }
|
|
110
155
|
}
|
|
111
156
|
case 'plugin-list': return this.plugin(extensions => extensions.list(), false)
|
|
112
157
|
case 'plugin-add': return this.plugin(extensions => extensions.install(this.insideApp(input.folder)))
|
|
158
|
+
case 'plugin-pull': return this.pull(input.source)
|
|
159
|
+
case 'plugin-publish': return this.planPublish(input)
|
|
113
160
|
case 'plugin-remove': {
|
|
114
161
|
const name = input.name
|
|
115
162
|
if (typeof name !== 'string' || name === '') throw new DoorError('invalid', 'plugin remove needs a plugin name')
|
|
@@ -151,30 +198,47 @@ export class Door {
|
|
|
151
198
|
return { value, text }
|
|
152
199
|
}
|
|
153
200
|
|
|
154
|
-
look(input: Record<string, unknown>, signal: AbortSignal): Promise<UiSnapshot> {
|
|
155
|
-
const { window, rest } = this.windowOf(input)
|
|
201
|
+
look(input: Record<string, unknown>, signal: AbortSignal, scope: VerbScope = {}): Promise<UiSnapshot> {
|
|
202
|
+
const { window, rest } = this.windowOf(input, scope)
|
|
156
203
|
const request = this.parse({ ...rest, op: 'snapshot' })
|
|
157
|
-
return this.callPage('look', request, signal, window) as Promise<UiSnapshot>
|
|
204
|
+
return this.callPage('look', request, signal, window, scope.page) as Promise<UiSnapshot>
|
|
158
205
|
}
|
|
159
206
|
|
|
160
|
-
async act(input: Record<string, unknown>, signal: AbortSignal): Promise<UiActionResult> {
|
|
207
|
+
async act(input: Record<string, unknown>, signal: AbortSignal, scope: VerbScope = {}): Promise<UiActionResult> {
|
|
161
208
|
if (input.op === 'snapshot') throw new DoorError('invalid', 'use look to see the page')
|
|
162
|
-
const { window, rest } = this.windowOf(input)
|
|
209
|
+
const { window, rest } = this.windowOf(input, scope)
|
|
163
210
|
const request = this.parse(rest)
|
|
164
|
-
|
|
165
|
-
//
|
|
166
|
-
const
|
|
211
|
+
const page = scope.page
|
|
212
|
+
// A reload or a navigate replaces the page; a click, a key or typing may (a link, a form that submits), and the page says so when it does.
|
|
213
|
+
const moves = request.op === 'reload' || request.op === 'navigate'
|
|
214
|
+
if (!moves && request.op !== 'click' && request.op !== 'press' && request.op !== 'type')
|
|
215
|
+
return await this.callPage(`act ${String(input.op)}`, request, signal, window, page) as UiActionResult
|
|
216
|
+
// Wait for the new page to connect, so the next look sees it; the wait starts before the act, or a fast page is missed. The page may drop before it has answered;
|
|
217
|
+
// that is the move working. A run's own page comes back in its own tab; no other page that happens to connect meanwhile is taken for it.
|
|
218
|
+
const backMs = this.deps.pageBackMs ?? PAGE_BACK_MS
|
|
219
|
+
const stay = new AbortController()
|
|
220
|
+
const back = page === undefined ? this.deps.channel.expectNewPage(backMs, stay.signal) : this.deps.channel.expectPageBack(page, backMs, stay.signal)
|
|
167
221
|
let answer: UiActionResult
|
|
222
|
+
let left = moves
|
|
168
223
|
try {
|
|
169
|
-
answer = await this.callPage(`act ${request.op}`, request, signal, window) as UiActionResult
|
|
224
|
+
answer = await this.callPage(`act ${request.op}`, request, signal, window, page) as UiActionResult
|
|
225
|
+
left ||= answer.leaving === true
|
|
170
226
|
} catch (cause) {
|
|
171
|
-
if (!(cause instanceof UiControlError && cause.code === 'disconnected')) {
|
|
172
|
-
answer = { ok: true, detail: request.op === 'reload' ? 'reloading' : `going to ${request.path}` }
|
|
227
|
+
if (!(cause instanceof UiControlError && cause.code === 'disconnected')) { stay.abort(); throw cause }
|
|
228
|
+
answer = { ok: true, detail: request.op === 'reload' ? 'reloading' : request.op === 'navigate' ? `going to ${request.path}` : 'the page is going to another address' }
|
|
229
|
+
left = true
|
|
230
|
+
}
|
|
231
|
+
if (!left) {
|
|
232
|
+
stay.abort()
|
|
233
|
+
return answer
|
|
173
234
|
}
|
|
174
235
|
const came = await back
|
|
175
|
-
// The page that came back is the one this agent was driving: it stays the target until the person focuses another page.
|
|
176
|
-
if (came !== undefined) this.deps.channel.prefer(came)
|
|
177
|
-
if (came === undefined)
|
|
236
|
+
// The page that came back is the one this agent was driving: it stays the target until the person focuses another page. A run that names its page follows that page's tab instead.
|
|
237
|
+
if (came !== undefined && page === undefined) this.deps.channel.prefer(came)
|
|
238
|
+
if (came === undefined)
|
|
239
|
+
throw new UiControlError('timeout', request.op === 'reload' ? 'the page did not come back after the reload'
|
|
240
|
+
: request.op === 'navigate' ? `the page did not come back after going to ${request.path}: is there a page at that path, and does it load the door's script?`
|
|
241
|
+
: `the ${request.op} took the page to another address, and no page came back: does the page there load the door's script?`)
|
|
178
242
|
const landed = request.op === 'navigate' ? await this.statusAt(request.path) : undefined
|
|
179
243
|
return { ...answer, detail: `${answer.detail ?? 'done'}${landed === undefined ? '' : `; ${landed}`}; the page is back: look again` }
|
|
180
244
|
}
|
|
@@ -187,7 +251,10 @@ export class Door {
|
|
|
187
251
|
const base = this.deps.app.url
|
|
188
252
|
if (base === undefined) return undefined
|
|
189
253
|
try {
|
|
190
|
-
|
|
254
|
+
// The app's own path, under the host prefix when the pages answer there (the frame's canvas): /board is /app/board in the canvas and /board at the
|
|
255
|
+
// root, and the app's root is the prefix itself (`/app`, where its rows mount `/`), as the page's navigation join does it.
|
|
256
|
+
const target = underAppBase(this.deps.appBase ?? '', path)
|
|
257
|
+
const response = await fetch(new URL(target, base), { headers: { accept: 'text/html' }, signal: AbortSignal.timeout(3000) })
|
|
191
258
|
await response.body?.cancel()
|
|
192
259
|
return response.ok ? `${response.status} at ${path}` : `${response.status} at ${path}: the app serves no page there, look at what is shown and navigate to a path it does`
|
|
193
260
|
} catch {
|
|
@@ -237,9 +304,11 @@ export class Door {
|
|
|
237
304
|
}
|
|
238
305
|
|
|
239
306
|
/** `window` on look and act: the desktop window (the number `window list` prints) whose page the call is for; without it, the page used last. */
|
|
240
|
-
private windowOf(input: Record<string, unknown
|
|
307
|
+
private windowOf(input: Record<string, unknown>, scope: VerbScope = {}): { window: number | undefined; rest: Record<string, unknown> } {
|
|
241
308
|
const { window, ...rest } = input
|
|
242
309
|
if (window === undefined) return { window: undefined, rest }
|
|
310
|
+
// A run that is for one page cannot be pointed at a desktop window instead.
|
|
311
|
+
if (scope.page !== undefined) throw new DoorError('invalid', 'this run drives only the page it was asked from; window does not apply')
|
|
243
312
|
if (typeof window !== 'number' || !Number.isSafeInteger(window) || window < 1) throw new DoorError('invalid', 'window must be the number `window list` prints for the window')
|
|
244
313
|
return { window, rest }
|
|
245
314
|
}
|
|
@@ -253,10 +322,10 @@ export class Door {
|
|
|
253
322
|
}
|
|
254
323
|
|
|
255
324
|
/** Run one call on the page and record it. The audit holds which control was touched and how it ended, never what was typed. */
|
|
256
|
-
private async callPage(verb: string, request: UiRequest, signal: AbortSignal, window?: number): Promise<UiResult> {
|
|
325
|
+
private async callPage(verb: string, request: UiRequest, signal: AbortSignal, window?: number, page?: string): Promise<UiResult> {
|
|
257
326
|
const approval: AuditEntry['approval'] = 'token'
|
|
258
327
|
try {
|
|
259
|
-
const value = await this.deps.channel.call(request, { signal, ...(window === undefined ? {} : { window }), ...(request.op === 'wait' ? { timeoutMs: (request.timeoutMs ?? 3000) + 5000 } : {}) })
|
|
328
|
+
const value = await this.deps.channel.call(request, { signal, ...(window === undefined ? {} : { window }), ...(page === undefined ? {} : { page }), ...(request.op === 'wait' ? { timeoutMs: (request.timeoutMs ?? 3000) + 5000 } : {}) })
|
|
260
329
|
const target = 'target' in value ? value.target : undefined
|
|
261
330
|
this.deps.audit.record({ at: new Date().toISOString(), tool: verb, ...(target === undefined ? {} : { target }), ...(window === undefined ? {} : { window }), outcome: 'ok', approval })
|
|
262
331
|
return value
|
|
@@ -270,7 +339,7 @@ export class Door {
|
|
|
270
339
|
|
|
271
340
|
private async plugin(run: (extensions: ExtensionsPort) => unknown, mutates = true): Promise<VerbAnswer> {
|
|
272
341
|
const extensions = this.deps.extensions()
|
|
273
|
-
if (extensions === undefined) throw new DoorError('unavailable', 'this
|
|
342
|
+
if (extensions === undefined) throw new DoorError('unavailable', 'this door has no extensions plugin to drive. In ACRYL\'s frame none is needed: a saved change in src/ or plugins/ reloads the app by itself, so do not restart it. Elsewhere, the app has no way to install plugins while it runs')
|
|
274
343
|
const value = await run(extensions)
|
|
275
344
|
// The app changed under the open pages: reload them (not when the plugin was already as it should be), and answer once they are back.
|
|
276
345
|
const unchanged = typeof value === 'object' && value !== null && (value as { action?: unknown }).action === 'unchanged'
|
|
@@ -278,6 +347,41 @@ export class Door {
|
|
|
278
347
|
return { value, text: JSON.stringify(value, null, 2) }
|
|
279
348
|
}
|
|
280
349
|
|
|
350
|
+
private share(): PluginSharePort {
|
|
351
|
+
const share = this.deps.pluginShare?.()
|
|
352
|
+
if (share === undefined) throw new DoorError('unavailable', 'plugin-share is not running in this app. If the app just changed it is restarting, so try again in a moment. An app made by an older version does not have it, and upgrade does not add it: run `npx acrylblends add plugin-share` and follow its README')
|
|
353
|
+
return share
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
/** Only a plan: no argument of any verb can publish, so an agent never says yes for a person. */
|
|
357
|
+
private async planPublish(input: Record<string, unknown>): Promise<VerbAnswer> {
|
|
358
|
+
const { name, owner, license } = input
|
|
359
|
+
if (typeof name !== 'string' || name === '') throw new DoorError('invalid', 'plugin publish needs a plugin name, e.g. plugin publish pomodoro')
|
|
360
|
+
const value = await this.share().planPublish(name, {
|
|
361
|
+
...(typeof owner === 'string' && owner !== '' ? { owner } : {}),
|
|
362
|
+
...(typeof license === 'string' && license !== '' ? { license } : {}),
|
|
363
|
+
})
|
|
364
|
+
if ('ok' in value) throw new DoorError('refused', `${value.reason}. ${value.next}.`)
|
|
365
|
+
return { value, text: `${renderPublishPlan(value)}\nNothing was sent. Only a person can say yes: they run \`npm run acryl -- plugin publish ${name}\` in their own terminal, or press Share in the frame. \`--license <id>\` changes the license before the yes.` }
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
/** The person's yes, from their own terminal. Not a verb: MCP and the chat do not have it. */
|
|
369
|
+
async publishConfirmed(name: string, confirmed: PublishPlanView, options: PublishOptionsView): Promise<unknown> {
|
|
370
|
+
const value = await this.share().publish(name, confirmed, options)
|
|
371
|
+
this.deps.audit.record({ at: new Date().toISOString(), tool: 'plugin-publish', target: { role: 'plugin', name: `${confirmed.owner}/${confirmed.repo}@${confirmed.version}` }, outcome: value.ok ? 'ok' : 'refused', approval: 'token' })
|
|
372
|
+
return value
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
private async pull(source: unknown): Promise<VerbAnswer> {
|
|
376
|
+
if (typeof source !== 'string' || source === '') throw new DoorError('invalid', 'plugin pull needs a source, e.g. plugin pull github:someone/pomodoro@0.1.0')
|
|
377
|
+
const value = await this.share().pull(source)
|
|
378
|
+
// A refusal is a failed command, so a script does not read it as done; it changed nothing, and the open pages stay.
|
|
379
|
+
if (!value.ok) throw new DoorError('refused', `${value.reason ?? 'the pull was refused'}. ${value.next ?? 'nothing changed'}.`)
|
|
380
|
+
// Neither did pulling what is already there change anything.
|
|
381
|
+
if (value.action !== 'unchanged' && this.deps.reloadOnChange !== false) await this.deps.channel.reloadAll()
|
|
382
|
+
return { value, text: JSON.stringify(value, null, 2) }
|
|
383
|
+
}
|
|
384
|
+
|
|
281
385
|
/** A folder named by an agent, resolved from the app folder; it must be inside the app. */
|
|
282
386
|
private insideApp(folder: unknown): string {
|
|
283
387
|
if (typeof folder !== 'string' || folder === '') throw new DoorError('invalid', 'plugin add needs a folder, e.g. plugins/pomodoro')
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
|
|
8
8
|
import { Acryl } from 'acryl-router'
|
|
9
9
|
import { UiControlError } from '../page/protocol.ts'
|
|
10
|
-
import { Door, DoorError } from './door.ts'
|
|
10
|
+
import { Door, DoorError, type PublishPlanView } from './door.ts'
|
|
11
11
|
import { error, INVALID_JSON, jsonBody, operatorOnly, readJson, type ReportError } from './http.ts'
|
|
12
12
|
import { VERBS } from './verbs.ts'
|
|
13
13
|
|
|
@@ -35,4 +35,24 @@ export function operatorRoutes(prefix: string, secret: string, door: Door, repor
|
|
|
35
35
|
return c.json(error('the verb could not be run'), 500)
|
|
36
36
|
}
|
|
37
37
|
})
|
|
38
|
+
// A person's yes to a plan, from the prompt in their own terminal. Not a verb, so MCP and the chat do not offer it. An agent with this machine's
|
|
39
|
+
// secret could still reach it: the door keeps honest agents from publishing by accident, the person's account is the real boundary.
|
|
40
|
+
.post(`${prefix}/publish-confirmed`, operatorOnly(secret), jsonBody, async (c) => {
|
|
41
|
+
const body = await readJson(c)
|
|
42
|
+
const { name, confirm, owner, license } = (body === INVALID_JSON || typeof body !== 'object' || body === null ? {} : body) as { name?: unknown; confirm?: unknown; owner?: unknown; license?: unknown }
|
|
43
|
+
if (typeof name !== 'string' || typeof confirm !== 'object' || confirm === null) {
|
|
44
|
+
return c.json({ ok: false, code: 'invalid', message: 'the body is { name, confirm }' } satisfies CallResponse, 400)
|
|
45
|
+
}
|
|
46
|
+
try {
|
|
47
|
+
const value = await door.publishConfirmed(name, confirm as PublishPlanView, {
|
|
48
|
+
...(typeof owner === 'string' ? { owner } : {}),
|
|
49
|
+
...(typeof license === 'string' ? { license } : {}),
|
|
50
|
+
})
|
|
51
|
+
return c.json({ ok: true, value, text: JSON.stringify(value, null, 2) } satisfies CallResponse)
|
|
52
|
+
} catch (cause) {
|
|
53
|
+
if (cause instanceof DoorError) return c.json({ ok: false, code: cause.code, message: cause.message } satisfies CallResponse, 409)
|
|
54
|
+
reportError('publish a plugin', cause)
|
|
55
|
+
return c.json(error('the plugin could not be published'), 500)
|
|
56
|
+
}
|
|
57
|
+
})
|
|
38
58
|
}
|
|
@@ -34,6 +34,8 @@ export function pageChannelRoutes(channel: UiChannel, options: PageRoutesOptions
|
|
|
34
34
|
const focused = c.req.query('focused') === '1'
|
|
35
35
|
const shellQuery = Number(c.req.query('shell'))
|
|
36
36
|
const shellWindow = Number.isSafeInteger(shellQuery) && shellQuery > 0 ? shellQuery : undefined
|
|
37
|
+
const tabQuery = c.req.query('tab')
|
|
38
|
+
const tab = tabQuery !== undefined && WINDOW_ID.test(tabQuery) ? tabQuery : undefined
|
|
37
39
|
const signal = c.req.raw.signal
|
|
38
40
|
return streamSSE(c, async (stream) => {
|
|
39
41
|
let end = (): void => {}
|
|
@@ -43,7 +45,7 @@ export function pageChannelRoutes(channel: UiChannel, options: PageRoutesOptions
|
|
|
43
45
|
const write = (event: string, data: string): void => {
|
|
44
46
|
stream.writeSSE({ event, data }).catch(end)
|
|
45
47
|
}
|
|
46
|
-
const detach = channel.attach(windowId, focused, { send: (data) => { write('call', data) }, notify: write, close: end }, shellWindow)
|
|
48
|
+
const detach = channel.attach(windowId, focused, { send: (data) => { write('call', data) }, notify: write, close: end }, shellWindow, tab)
|
|
47
49
|
const beat = setInterval(() => { write('ping', '') }, heartbeatMs)
|
|
48
50
|
try {
|
|
49
51
|
write('ready', '')
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
11
|
import { Acryl, type MiddlewareHandler } from 'acryl-router'
|
|
12
|
-
import type { UpgradeWebSocket } from 'acryl-router/helpers'
|
|
12
|
+
import type { UpgradeWebSocket, WSEvents } from 'acryl-router/helpers'
|
|
13
13
|
import { loopbackAuthority } from 'acryl-router/middleware'
|
|
14
14
|
import { parseChannelToHost, WINDOW_ID } from '../page/protocol.ts'
|
|
15
15
|
import type { UiChannel } from './channel.ts'
|
|
@@ -20,7 +20,8 @@ export const SOCKET_HEARTBEAT_MS = 15_000
|
|
|
20
20
|
|
|
21
21
|
export interface PageSocketOptions {
|
|
22
22
|
readonly prefix: string
|
|
23
|
-
|
|
23
|
+
/** The live upgrade helper: the route is mounted once, so the helper is read per request (a restarting host has a new one). */
|
|
24
|
+
readonly upgrade: () => UpgradeWebSocket | undefined
|
|
24
25
|
readonly heartbeatMs?: number
|
|
25
26
|
}
|
|
26
27
|
|
|
@@ -39,14 +40,13 @@ export const ownOrigin: MiddlewareHandler = async (c, next) => {
|
|
|
39
40
|
|
|
40
41
|
export function pageSocketRoutes(channel: UiChannel, options: PageSocketOptions): Acryl {
|
|
41
42
|
const heartbeatMs = options.heartbeatMs ?? SOCKET_HEARTBEAT_MS
|
|
42
|
-
|
|
43
|
-
`${options.prefix}/channel/ws`,
|
|
44
|
-
ownOrigin,
|
|
45
|
-
options.upgrade((c) => {
|
|
43
|
+
const events = (c: Parameters<MiddlewareHandler>[0]): WSEvents => {
|
|
46
44
|
const windowId = c.req.query('window') as string
|
|
47
45
|
const focused = c.req.query('focused') === '1'
|
|
48
46
|
const shellQuery = Number(c.req.query('shell'))
|
|
49
47
|
const shellWindow = Number.isSafeInteger(shellQuery) && shellQuery > 0 ? shellQuery : undefined
|
|
48
|
+
const tabQuery = c.req.query('tab')
|
|
49
|
+
const tab = tabQuery !== undefined && WINDOW_ID.test(tabQuery) ? tabQuery : undefined
|
|
50
50
|
let detach: (() => void) | undefined
|
|
51
51
|
let beat: NodeJS.Timeout | undefined
|
|
52
52
|
return {
|
|
@@ -55,7 +55,7 @@ export function pageSocketRoutes(channel: UiChannel, options: PageSocketOptions)
|
|
|
55
55
|
ws.send(JSON.stringify(frame))
|
|
56
56
|
}
|
|
57
57
|
// A call that cannot be written fails at once (`disconnected`), and a socket that is gone is dropped by its close: the door never holds a call for it.
|
|
58
|
-
detach = channel.attach(windowId, focused, { send: data => { write({ e: 'call', d: data }) }, notify: (event, data) => { write({ e: event, d: data }) }, close: () => { ws.close(1000, 'replaced') } }, shellWindow)
|
|
58
|
+
detach = channel.attach(windowId, focused, { send: data => { write({ e: 'call', d: data }) }, notify: (event, data) => { write({ e: event, d: data }) }, close: () => { ws.close(1000, 'replaced') } }, shellWindow, tab)
|
|
59
59
|
write({ e: 'ready' })
|
|
60
60
|
beat = setInterval(() => {
|
|
61
61
|
try { write({ e: 'ping' }) } catch { /* the close event follows */ }
|
|
@@ -73,6 +73,16 @@ export function pageSocketRoutes(channel: UiChannel, options: PageSocketOptions)
|
|
|
73
73
|
detach?.()
|
|
74
74
|
},
|
|
75
75
|
}
|
|
76
|
-
|
|
76
|
+
}
|
|
77
|
+
// Mounted once, ahead of the private routes: the route must keep its precedence across a host restart, so the middleware asks for the live helper
|
|
78
|
+
// at request time and refuses cleanly while none is ready.
|
|
79
|
+
return new Acryl().get(
|
|
80
|
+
`${options.prefix}/channel/ws`,
|
|
81
|
+
ownOrigin,
|
|
82
|
+
async (c, next) => {
|
|
83
|
+
const allow = options.upgrade()
|
|
84
|
+
if (allow === undefined) return c.json(error('the page channel is not ready yet'), 503)
|
|
85
|
+
return allow(events, { textOnly: true })(c, next)
|
|
86
|
+
},
|
|
77
87
|
)
|
|
78
88
|
}
|
|
@@ -6,7 +6,10 @@
|
|
|
6
6
|
import { MAX_SNAPSHOT_NODES, MAX_WAIT_MS } from '../page/protocol.ts'
|
|
7
7
|
|
|
8
8
|
/** Every verb, in the order `brief` lists them. */
|
|
9
|
-
|
|
9
|
+
/** The verbs that only use the page: what a page-only door (a production assistant) offers. */
|
|
10
|
+
export const PAGE_VERBS = ['look', 'act'] as const satisfies readonly Verb[]
|
|
11
|
+
|
|
12
|
+
export const VERBS = ['brief', 'look', 'act', 'plugin-list', 'plugin-add', 'plugin-pull', 'plugin-publish', 'plugin-remove', 'plugin-reload', 'tool-list', 'tool-call', 'window-list', 'window-open', 'window-focus', 'window-close', 'window-resize', 'menu-list', 'menu-click'] as const
|
|
10
13
|
export type Verb = (typeof VERBS)[number]
|
|
11
14
|
|
|
12
15
|
/** The JSON Schema of a verb's arguments, as MCP lists it. */
|
|
@@ -83,6 +86,16 @@ export const VERB_SPECS: Readonly<Record<Verb, VerbSpec>> = {
|
|
|
83
86
|
description: 'Check, install and start a plugin folder inside the app, without a restart.',
|
|
84
87
|
arguments: { type: 'object', properties: { folder: { type: 'string', description: 'a folder inside the app, e.g. plugins/pomodoro' } }, required: ['folder'], additionalProperties: false },
|
|
85
88
|
},
|
|
89
|
+
'plugin-pull': {
|
|
90
|
+
usage: 'plugin pull <source>: fetch a plugin someone released (github:<owner>/<repo>[@<version>][@sha256:<digest>], or a .tgz), check it and start it, live',
|
|
91
|
+
description: 'Fetch a plugin someone released, without running anything; check its digest, that it works on every OS and that this app has the services it needs; then install and start it without a restart. A refusal says why and changes nothing.',
|
|
92
|
+
arguments: { type: 'object', properties: { source: { type: 'string', description: 'github:<owner>/<repo>[@<version>][@sha256:<digest>], or a .tgz path or https address' } }, required: ['source'], additionalProperties: false },
|
|
93
|
+
},
|
|
94
|
+
'plugin-publish': {
|
|
95
|
+
usage: 'plugin publish <name> [--owner <account>] [--license <id>]: show what publishing this plugin would send; in a person\'s own terminal, ask them',
|
|
96
|
+
description: 'Show what publishing a plugin would send: the account, repository, version, license, files and the line a friend pulls it with. Sends nothing. Only a person can say yes, in their own terminal or with Share in the frame; tell them the plan and how.',
|
|
97
|
+
arguments: { type: 'object', properties: { name: { type: 'string', description: 'a plugin of plugins/' }, owner: { type: 'string', description: 'the GitHub account or organization; the signed-in one when absent' }, license: { type: 'string', description: 'an SPDX license id; MIT when absent' } }, required: ['name'], additionalProperties: false },
|
|
98
|
+
},
|
|
86
99
|
'plugin-remove': {
|
|
87
100
|
usage: 'plugin remove <name>: stop and remove an installed plugin',
|
|
88
101
|
description: 'Stop and remove an installed plugin.',
|