acrylblends 1.0.5 → 1.0.6

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 (124) hide show
  1. package/docs/framework/agents/index.md +1 -1
  2. package/lib/{audit-D5z9uwOP.d.ts → audit-GcLtvNLV.d.ts} +4 -2
  3. package/lib/cordis/door.d.ts +90 -4
  4. package/lib/cordis/door.js +524 -13
  5. package/lib/door/host.d.ts +2 -2
  6. package/lib/door/host.js +1 -1
  7. package/lib/door/page.d.ts +3 -1
  8. package/lib/door/page.js +1 -1
  9. package/lib/door/protocol.d.ts +1 -1
  10. package/lib/{protocol-C37gPePD.d.ts → protocol-CR31M7K_.d.ts} +2 -0
  11. package/lib/{snapshot-text-Bbx43mN0.js → snapshot-text-CtQm3n8n.js} +14 -5
  12. package/package.json +1 -1
  13. package/plugins/acryl-door/src/cli/acryl.ts +36 -5
  14. package/plugins/acryl-door/src/host/channel.ts +14 -6
  15. package/plugins/acryl-door/src/host/door.ts +78 -10
  16. package/plugins/acryl-door/src/host/page-routes.ts +3 -1
  17. package/plugins/acryl-door/src/host/shell.ts +147 -0
  18. package/plugins/acryl-door/src/host/shim.ts +39 -3
  19. package/plugins/acryl-door/src/host/verbs.ts +39 -2
  20. package/plugins/acryl-door/src/index.ts +7 -1
  21. package/plugins/acryl-door/src/page/channel.ts +3 -1
  22. package/plugins/acryl-door/src/page/index.ts +2 -1
  23. package/plugins/acryl-door/src/page/protocol.ts +2 -0
  24. package/plugins/acryl-door/src/shell/protocol.ts +130 -0
  25. package/plugins/agent-panel/headless.ts +29 -8
  26. package/plugins/agent-panel/launch.ts +67 -0
  27. package/plugins/desktop/door-shell.ts +379 -0
  28. package/plugins/desktop/main.ts +66 -12
  29. package/templates/agent/AGENTS.md +2 -0
  30. package/plugins/acryl-router/src/connection.d.ts +0 -25
  31. package/plugins/acryl-router/src/core/app-base.d.ts +0 -215
  32. package/plugins/acryl-router/src/core/app.d.ts +0 -19
  33. package/plugins/acryl-router/src/core/client/client.d.ts +0 -4
  34. package/plugins/acryl-router/src/core/client/fetch-result-please.d.ts +0 -35
  35. package/plugins/acryl-router/src/core/client/index.d.ts +0 -7
  36. package/plugins/acryl-router/src/core/client/types.d.ts +0 -229
  37. package/plugins/acryl-router/src/core/client/utils.d.ts +0 -18
  38. package/plugins/acryl-router/src/core/compose.d.ts +0 -14
  39. package/plugins/acryl-router/src/core/context.d.ts +0 -443
  40. package/plugins/acryl-router/src/core/helper/accepts/accepts.d.ts +0 -30
  41. package/plugins/acryl-router/src/core/helper/accepts/index.d.ts +0 -5
  42. package/plugins/acryl-router/src/core/helper/conninfo/index.d.ts +0 -5
  43. package/plugins/acryl-router/src/core/helper/conninfo/types.d.ts +0 -36
  44. package/plugins/acryl-router/src/core/helper/cookie/index.d.ts +0 -25
  45. package/plugins/acryl-router/src/core/helper/css/common.d.ts +0 -59
  46. package/plugins/acryl-router/src/core/helper/css/index.d.ts +0 -80
  47. package/plugins/acryl-router/src/core/helper/dev/index.d.ts +0 -20
  48. package/plugins/acryl-router/src/core/helper/factory/index.d.ts +0 -82
  49. package/plugins/acryl-router/src/core/helper/html/index.d.ts +0 -8
  50. package/plugins/acryl-router/src/core/helper/proxy/index.d.ts +0 -73
  51. package/plugins/acryl-router/src/core/helper/route/index.d.ts +0 -75
  52. package/plugins/acryl-router/src/core/helper/ssg/index.d.ts +0 -7
  53. package/plugins/acryl-router/src/core/helper/ssg/middleware.d.ts +0 -44
  54. package/plugins/acryl-router/src/core/helper/ssg/plugins.d.ts +0 -27
  55. package/plugins/acryl-router/src/core/helper/ssg/ssg.d.ts +0 -91
  56. package/plugins/acryl-router/src/core/helper/ssg/utils.d.ts +0 -16
  57. package/plugins/acryl-router/src/core/helper/streaming/index.d.ts +0 -8
  58. package/plugins/acryl-router/src/core/helper/streaming/sse.d.ts +0 -13
  59. package/plugins/acryl-router/src/core/helper/streaming/stream.d.ts +0 -3
  60. package/plugins/acryl-router/src/core/helper/streaming/text.d.ts +0 -3
  61. package/plugins/acryl-router/src/core/helper/streaming/utils.d.ts +0 -1
  62. package/plugins/acryl-router/src/core/helper/testing/index.d.ts +0 -13
  63. package/plugins/acryl-router/src/core/helper/websocket/index.d.ts +0 -70
  64. package/plugins/acryl-router/src/core/http-exception.d.ts +0 -58
  65. package/plugins/acryl-router/src/core/index.d.ts +0 -37
  66. package/plugins/acryl-router/src/core/jsx/base.d.ts +0 -61
  67. package/plugins/acryl-router/src/core/jsx/children.d.ts +0 -9
  68. package/plugins/acryl-router/src/core/jsx/components.d.ts +0 -15
  69. package/plugins/acryl-router/src/core/jsx/constants.d.ts +0 -6
  70. package/plugins/acryl-router/src/core/jsx/context.d.ts +0 -51
  71. package/plugins/acryl-router/src/core/jsx/dom/components.d.ts +0 -10
  72. package/plugins/acryl-router/src/core/jsx/dom/context.d.ts +0 -3
  73. package/plugins/acryl-router/src/core/jsx/dom/css.d.ts +0 -83
  74. package/plugins/acryl-router/src/core/jsx/dom/hooks/index.d.ts +0 -39
  75. package/plugins/acryl-router/src/core/jsx/dom/intrinsic-element/components.d.ts +0 -16
  76. package/plugins/acryl-router/src/core/jsx/dom/jsx-dev-runtime.d.ts +0 -8
  77. package/plugins/acryl-router/src/core/jsx/dom/jsx-runtime.d.ts +0 -7
  78. package/plugins/acryl-router/src/core/jsx/dom/render.d.ts +0 -73
  79. package/plugins/acryl-router/src/core/jsx/dom/utils.d.ts +0 -1
  80. package/plugins/acryl-router/src/core/jsx/hooks/index.d.ts +0 -47
  81. package/plugins/acryl-router/src/core/jsx/index.d.ts +0 -76
  82. package/plugins/acryl-router/src/core/jsx/intrinsic-element/common.d.ts +0 -6
  83. package/plugins/acryl-router/src/core/jsx/intrinsic-element/components.d.ts +0 -13
  84. package/plugins/acryl-router/src/core/jsx/intrinsic-elements.d.ts +0 -735
  85. package/plugins/acryl-router/src/core/jsx/streaming.d.ts +0 -36
  86. package/plugins/acryl-router/src/core/jsx/types.d.ts +0 -36
  87. package/plugins/acryl-router/src/core/jsx/utils.d.ts +0 -4
  88. package/plugins/acryl-router/src/core/request/constants.d.ts +0 -1
  89. package/plugins/acryl-router/src/core/request.d.ts +0 -306
  90. package/plugins/acryl-router/src/core/router/reg-exp-router/index.d.ts +0 -6
  91. package/plugins/acryl-router/src/core/router/reg-exp-router/matcher.d.ts +0 -7
  92. package/plugins/acryl-router/src/core/router/reg-exp-router/node.d.ts +0 -13
  93. package/plugins/acryl-router/src/core/router/reg-exp-router/prepared-router.d.ts +0 -17
  94. package/plugins/acryl-router/src/core/router/reg-exp-router/router.d.ts +0 -11
  95. package/plugins/acryl-router/src/core/router/reg-exp-router/trie.d.ts +0 -8
  96. package/plugins/acryl-router/src/core/router/smart-router/index.d.ts +0 -5
  97. package/plugins/acryl-router/src/core/router/smart-router/router.d.ts +0 -11
  98. package/plugins/acryl-router/src/core/router/trie-router/index.d.ts +0 -5
  99. package/plugins/acryl-router/src/core/router/trie-router/node.d.ts +0 -6
  100. package/plugins/acryl-router/src/core/router/trie-router/router.d.ts +0 -7
  101. package/plugins/acryl-router/src/core/router/utils.d.ts +0 -1
  102. package/plugins/acryl-router/src/core/router.d.ts +0 -97
  103. package/plugins/acryl-router/src/core/types.d.ts +0 -573
  104. package/plugins/acryl-router/src/core/utils/accept.d.ts +0 -6
  105. package/plugins/acryl-router/src/core/utils/body.d.ts +0 -79
  106. package/plugins/acryl-router/src/core/utils/buffer.d.ts +0 -17
  107. package/plugins/acryl-router/src/core/utils/color.d.ts +0 -21
  108. package/plugins/acryl-router/src/core/utils/concurrent.d.ts +0 -11
  109. package/plugins/acryl-router/src/core/utils/constants.d.ts +0 -4
  110. package/plugins/acryl-router/src/core/utils/cookie.d.ts +0 -41
  111. package/plugins/acryl-router/src/core/utils/crypto.d.ts +0 -15
  112. package/plugins/acryl-router/src/core/utils/handler.d.ts +0 -6
  113. package/plugins/acryl-router/src/core/utils/headers.d.ts +0 -8
  114. package/plugins/acryl-router/src/core/utils/html.d.ts +0 -44
  115. package/plugins/acryl-router/src/core/utils/http-status.d.ts +0 -32
  116. package/plugins/acryl-router/src/core/utils/mime.d.ts +0 -70
  117. package/plugins/acryl-router/src/core/utils/stream.d.ts +0 -31
  118. package/plugins/acryl-router/src/core/utils/types.d.ts +0 -128
  119. package/plugins/acryl-router/src/core/utils/url.d.ts +0 -44
  120. package/plugins/acryl-router/src/core/validator/index.d.ts +0 -7
  121. package/plugins/acryl-router/src/core/validator/utils.d.ts +0 -29
  122. package/plugins/acryl-router/src/core/validator/validator.d.ts +0 -16
  123. package/plugins/acryl-router/src/helpers/index.d.ts +0 -17
  124. package/plugins/acryl-router/src/helpers/schema-validator.d.ts +0 -54
@@ -1,13 +1,280 @@
1
1
  import { t as Acryl } from "../app-CQcGWjrk.js";
2
+ import { r as streamSSE } from "../streaming-BlDaph8U.js";
3
+ import "../helpers-BEjcdbgw.js";
2
4
  import { n as clientModules } from "../client-modules-drj4mjhj.js";
3
5
  import { a as MAX_WAIT_MS, l as UiControlError, p as parseUiRequest, r as MAX_SNAPSHOT_NODES } from "../protocol-Dt0AZBOz.js";
4
- import { a as INVALID_JSON, c as operatorOnly, d as AuditLog, f as auditPath, i as DOOR_PREFIX, l as privateApi, o as error, p as UiChannel, r as pageChannelRoutes, s as jsonBody, t as renderSnapshot, u as readJson } from "../snapshot-text-Bbx43mN0.js";
6
+ import { a as INVALID_JSON, c as operatorOnly, d as AuditLog, f as auditPath, i as DOOR_PREFIX, l as privateApi, o as error, p as UiChannel, r as pageChannelRoutes, s as jsonBody, t as renderSnapshot, u as readJson } from "../snapshot-text-CtQm3n8n.js";
5
7
  import "../src-BTBm4OPD.js";
6
8
  import { isAbsolute, join, relative, resolve } from "node:path";
7
9
  import { chmodSync, existsSync, mkdirSync, readFileSync, renameSync, rmSync, writeFileSync } from "node:fs";
8
10
  import z from "@deepseek-ai/schemastery";
9
11
  import { randomBytes } from "node:crypto";
10
12
  import { spawnSync } from "node:child_process";
13
+ //#region ../../plugins/acryl-door/src/shell/protocol.ts
14
+ var ShellError = class extends Error {
15
+ code;
16
+ constructor(code, message) {
17
+ super(message);
18
+ this.code = code;
19
+ this.name = "ShellError";
20
+ }
21
+ };
22
+ /** A path of the app, as `act navigate` takes one: one leading `/`, and no other site. */
23
+ const APP_PATH = /^\/(?![/\\])[^\s\\]*$/u;
24
+ const isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
25
+ function only(value, allowed) {
26
+ const extra = Object.keys(value).filter((key) => key !== "op" && !allowed.includes(key));
27
+ if (extra.length > 0) throw new ShellError("invalid", `unknown field ${extra.join(", ")}`);
28
+ }
29
+ function windowId(value) {
30
+ if (typeof value.id !== "number" || !Number.isSafeInteger(value.id) || value.id < 1) throw new ShellError("invalid", "id must be the number `window list` prints for the window");
31
+ return value.id;
32
+ }
33
+ function size(value, key) {
34
+ const number = value[key];
35
+ if (typeof number !== "number" || !Number.isInteger(number) || number < 200 || number > 1e4) throw new ShellError("invalid", `${key} must be a whole number of pixels from 200 to 10000`);
36
+ return number;
37
+ }
38
+ /** @throws ShellError `invalid`. */
39
+ function parseShellRequest(op, input) {
40
+ const value = isRecord(input) ? input : {};
41
+ switch (op) {
42
+ case "window-list":
43
+ only(value, []);
44
+ return { op };
45
+ case "window-open":
46
+ only(value, ["path"]);
47
+ if (value.path === void 0) return { op };
48
+ if (typeof value.path !== "string" || !APP_PATH.test(value.path)) throw new ShellError("invalid", "path must be a path of this app, such as /settings (it starts with one /, and names no other site)");
49
+ return {
50
+ op,
51
+ path: value.path
52
+ };
53
+ case "window-focus":
54
+ only(value, ["id"]);
55
+ return {
56
+ op,
57
+ id: windowId(value)
58
+ };
59
+ case "window-close":
60
+ only(value, ["id", "force"]);
61
+ if (value.force !== void 0 && typeof value.force !== "boolean") throw new ShellError("invalid", "force must be true or false");
62
+ return {
63
+ op,
64
+ id: windowId(value),
65
+ ...value.force === true ? { force: true } : {}
66
+ };
67
+ case "window-resize":
68
+ only(value, [
69
+ "id",
70
+ "width",
71
+ "height"
72
+ ]);
73
+ return {
74
+ op,
75
+ id: windowId(value),
76
+ width: size(value, "width"),
77
+ height: size(value, "height")
78
+ };
79
+ case "menu-list":
80
+ only(value, []);
81
+ return { op };
82
+ case "menu-click":
83
+ only(value, ["path", "force"]);
84
+ if (typeof value.path !== "string" || value.path.trim() === "" || value.path.length > 300) throw new ShellError("invalid", "path is the labels from the top of the menu joined with >, e.g. \"File>New Note\" (`menu list` prints them)");
85
+ if (value.force !== void 0 && typeof value.force !== "boolean") throw new ShellError("invalid", "force must be true or false");
86
+ return {
87
+ op,
88
+ path: value.path,
89
+ ...value.force === true ? { force: true } : {}
90
+ };
91
+ }
92
+ }
93
+ /** @param value - one parsed POST body from the shell; null when it is not a well-formed answer. */
94
+ function parseShellAnswer(value) {
95
+ if (!isRecord(value) || typeof value.id !== "number" || !Number.isSafeInteger(value.id)) return null;
96
+ if (value.t === "result" && isRecord(value.value)) return {
97
+ t: "result",
98
+ id: value.id,
99
+ value: value.value
100
+ };
101
+ if (value.t === "error" && typeof value.code === "string" && typeof value.message === "string") return {
102
+ t: "error",
103
+ id: value.id,
104
+ code: value.code.slice(0, 40),
105
+ message: value.message.slice(0, 500)
106
+ };
107
+ return null;
108
+ }
109
+ /** Whatever code the shell used, as one the door knows (an unknown code is `failed`). */
110
+ function shellErrorCode(code) {
111
+ return [
112
+ "invalid",
113
+ "no-such-window",
114
+ "last-window",
115
+ "no-such-menu-item",
116
+ "disabled",
117
+ "quits-app"
118
+ ].find((known) => known === code) ?? "failed";
119
+ }
120
+ //#endregion
121
+ //#region ../../plugins/acryl-door/src/host/shell.ts
122
+ /**
123
+ * The desktop shell's channel: the Electron main process of `npm run desktop`, which reaches the door as a page does (an event stream down, a POST up) and answers calls about
124
+ * the app's windows and menu. One shell at a time: a new one replaces the old (the dev server restarted and the shell came back). It needs this run's secret, so no page can open it.
125
+ */
126
+ const CALL_TIMEOUT_MS = 1e4;
127
+ /** What a verb says when no desktop shell is attached: one line a model can act on. */
128
+ const NO_DESKTOP = "no desktop window is attached; run npm run desktop";
129
+ var ShellChannel = class {
130
+ current;
131
+ pending = /* @__PURE__ */ new Map();
132
+ nextId = 1;
133
+ closed = false;
134
+ attachWaiters = /* @__PURE__ */ new Set();
135
+ get connected() {
136
+ return this.current !== void 0;
137
+ }
138
+ /** @returns the function to call when this stream ends. */
139
+ attach(stream) {
140
+ const previous = this.current;
141
+ const connection = { stream };
142
+ this.current = connection;
143
+ if (previous !== void 0) {
144
+ this.failAll(new ShellError("disconnected", "the desktop shell reconnected before it answered"));
145
+ previous.stream.close();
146
+ }
147
+ for (const waiter of [...this.attachWaiters]) waiter();
148
+ return () => {
149
+ if (this.current !== connection) return;
150
+ this.current = void 0;
151
+ this.failAll(new ShellError("disconnected", "the desktop shell went away before it answered"));
152
+ };
153
+ }
154
+ /** A promise for the shell connecting (it reconnects after the dev server restarted). */
155
+ waitForShell(timeoutMs) {
156
+ if (this.current !== void 0) return Promise.resolve(true);
157
+ return new Promise((resolve) => {
158
+ const done = (value) => {
159
+ clearTimeout(timer);
160
+ this.attachWaiters.delete(waiter);
161
+ resolve(value);
162
+ };
163
+ const waiter = () => {
164
+ done(true);
165
+ };
166
+ const timer = setTimeout(() => {
167
+ done(false);
168
+ }, timeoutMs);
169
+ this.attachWaiters.add(waiter);
170
+ });
171
+ }
172
+ receive(body) {
173
+ const answer = parseShellAnswer(body);
174
+ if (answer === null) return;
175
+ if (answer.t === "result") this.settle(answer.id, answer.value);
176
+ else this.settle(answer.id, void 0, new ShellError(shellErrorCode(answer.code), answer.message));
177
+ }
178
+ /** @throws ShellError `no-desktop` when no shell is attached, else what the shell refused with. */
179
+ call(request, options = {}) {
180
+ if (this.closed) return Promise.reject(new ShellError("unloaded", "the door was unloaded"));
181
+ const target = this.current;
182
+ if (target === void 0) return Promise.reject(new ShellError("no-desktop", NO_DESKTOP));
183
+ const signal = options.signal;
184
+ if (signal?.aborted === true) return Promise.reject(new ShellError("aborted", "the call was cancelled"));
185
+ const id = this.nextId;
186
+ this.nextId += 1;
187
+ return new Promise((resolve, reject) => {
188
+ const timer = setTimeout(() => {
189
+ this.settle(id, void 0, new ShellError("timeout", "the desktop shell did not answer in time"));
190
+ }, options.timeoutMs ?? CALL_TIMEOUT_MS);
191
+ const onAbort = () => {
192
+ this.settle(id, void 0, new ShellError("aborted", "the call was cancelled"));
193
+ };
194
+ signal?.addEventListener("abort", onAbort, { once: true });
195
+ this.pending.set(id, {
196
+ resolve,
197
+ reject,
198
+ timer,
199
+ cleanup: () => {
200
+ signal?.removeEventListener("abort", onAbort);
201
+ }
202
+ });
203
+ try {
204
+ target.stream.send(JSON.stringify({
205
+ t: "call",
206
+ id,
207
+ request
208
+ }));
209
+ } catch {
210
+ this.settle(id, void 0, new ShellError("disconnected", "the desktop shell could not be reached"));
211
+ }
212
+ });
213
+ }
214
+ close() {
215
+ this.closed = true;
216
+ this.failAll(new ShellError("unloaded", "the door was unloaded"));
217
+ this.current?.stream.close();
218
+ this.current = void 0;
219
+ }
220
+ failAll(failure) {
221
+ for (const id of [...this.pending.keys()]) this.settle(id, void 0, failure);
222
+ }
223
+ settle(id, value, failure) {
224
+ const entry = this.pending.get(id);
225
+ if (entry === void 0) return;
226
+ this.pending.delete(id);
227
+ clearTimeout(entry.timer);
228
+ entry.cleanup();
229
+ if (failure !== void 0) entry.reject(failure);
230
+ else entry.resolve(value);
231
+ }
232
+ };
233
+ /** How often a `ping` is written, so the shell knows a silent stream died and reconnects. */
234
+ const SHELL_HEARTBEAT_MS = 15e3;
235
+ /** `GET <prefix>/shell/events` is the shell's stream and `POST <prefix>/shell/answer` carries its answers: both need this run's secret. */
236
+ function shellRoutes(prefix, secret, channel, heartbeatMs = SHELL_HEARTBEAT_MS) {
237
+ return new Acryl().get(`${prefix}/shell/events`, operatorOnly(secret), (c) => {
238
+ const signal = c.req.raw.signal;
239
+ return streamSSE(c, async (stream) => {
240
+ let end = () => {};
241
+ const ended = new Promise((resolve) => {
242
+ end = resolve;
243
+ });
244
+ stream.onAbort(end);
245
+ signal.addEventListener("abort", end, { once: true });
246
+ const write = (event, data) => {
247
+ stream.writeSSE({
248
+ event,
249
+ data
250
+ }).catch(end);
251
+ };
252
+ const detach = channel.attach({
253
+ send: (data) => {
254
+ write("call", data);
255
+ },
256
+ close: end
257
+ });
258
+ const beat = setInterval(() => {
259
+ write("ping", "");
260
+ }, heartbeatMs);
261
+ try {
262
+ write("ready", "");
263
+ await ended;
264
+ } finally {
265
+ clearInterval(beat);
266
+ signal.removeEventListener("abort", end);
267
+ detach();
268
+ }
269
+ });
270
+ }).post(`${prefix}/shell/answer`, operatorOnly(secret), jsonBody, async (c) => {
271
+ const body = await readJson(c);
272
+ if (body === INVALID_JSON || typeof body !== "object" || body === null) return c.json(error("invalid request body"), 400);
273
+ channel.receive(body);
274
+ return c.json({ ok: true });
275
+ });
276
+ }
277
+ //#endregion
11
278
  //#region ../../plugins/acryl-door/src/host/verbs.ts
12
279
  /**
13
280
  * The one verb list (spec 049 slice 13). Every face of the door reads it: the operator client (`acryl <verb>`, its `verbs` and `brief`),
@@ -23,7 +290,14 @@ const VERBS = [
23
290
  "plugin-remove",
24
291
  "plugin-reload",
25
292
  "tool-list",
26
- "tool-call"
293
+ "tool-call",
294
+ "window-list",
295
+ "window-open",
296
+ "window-focus",
297
+ "window-close",
298
+ "window-resize",
299
+ "menu-list",
300
+ "menu-click"
27
301
  ];
28
302
  const none = {
29
303
  type: "object",
@@ -37,7 +311,7 @@ const VERB_SPECS = {
37
311
  arguments: none
38
312
  },
39
313
  "look": {
40
- usage: "look [--cursor <n>] [--max-nodes <n>]: the page as text, with refs",
314
+ usage: "look [--window <n>] [--cursor <n>] [--max-nodes <n>]: the page as text, with refs",
41
315
  description: "The open page as text: its roles, names, values and visible text, each with a ref to act on. A long page comes in parts (cursor).",
42
316
  arguments: {
43
317
  type: "object",
@@ -51,6 +325,11 @@ const VERB_SPECS = {
51
325
  type: "integer",
52
326
  minimum: 1,
53
327
  maximum: MAX_SNAPSHOT_NODES
328
+ },
329
+ window: {
330
+ type: "integer",
331
+ minimum: 1,
332
+ description: "a desktop window (the id window-list prints); default: the page used last"
54
333
  }
55
334
  },
56
335
  additionalProperties: false
@@ -134,6 +413,11 @@ const VERB_SPECS = {
134
413
  type: "integer",
135
414
  minimum: 0,
136
415
  maximum: MAX_WAIT_MS
416
+ },
417
+ window: {
418
+ type: "integer",
419
+ minimum: 1,
420
+ description: "a desktop window (the id window-list prints); default: the page used last"
137
421
  }
138
422
  },
139
423
  required: ["op"],
@@ -190,6 +474,109 @@ const VERB_SPECS = {
190
474
  required: ["tool"],
191
475
  additionalProperties: false
192
476
  }
477
+ },
478
+ "window-list": {
479
+ usage: "window list: the windows of the desktop app",
480
+ description: "The windows of the desktop app: id, title, path, size, and which one is focused. Needs the desktop app (npm run desktop).",
481
+ arguments: none
482
+ },
483
+ "window-open": {
484
+ usage: "window open [--path <path of the app>]: open a new window",
485
+ description: "Open a new window of the desktop app on a path of the app (default: its start page). Answers with the new window's id; look and act take it as window.",
486
+ arguments: {
487
+ type: "object",
488
+ properties: { path: {
489
+ type: "string",
490
+ description: "a path of this app, e.g. /settings"
491
+ } },
492
+ additionalProperties: false
493
+ }
494
+ },
495
+ "window-focus": {
496
+ usage: "window focus --id <n>: bring a window to the front",
497
+ description: "Bring a window of the desktop app to the front. Look and act without a window then drive it.",
498
+ arguments: {
499
+ type: "object",
500
+ properties: { id: {
501
+ type: "integer",
502
+ minimum: 1,
503
+ description: "the id window-list prints"
504
+ } },
505
+ required: ["id"],
506
+ additionalProperties: false
507
+ }
508
+ },
509
+ "window-close": {
510
+ usage: "window close --id <n> [--force]: close a window",
511
+ description: "Close a window of the desktop app. The last window cannot be closed without force, because that ends the desktop app.",
512
+ arguments: {
513
+ type: "object",
514
+ properties: {
515
+ id: {
516
+ type: "integer",
517
+ minimum: 1
518
+ },
519
+ force: {
520
+ type: "boolean",
521
+ description: "allow closing the last window, which ends the desktop app"
522
+ }
523
+ },
524
+ required: ["id"],
525
+ additionalProperties: false
526
+ }
527
+ },
528
+ "window-resize": {
529
+ usage: "window resize --id <n> --width <px> --height <px>",
530
+ description: "Resize a window of the desktop app.",
531
+ arguments: {
532
+ type: "object",
533
+ properties: {
534
+ id: {
535
+ type: "integer",
536
+ minimum: 1
537
+ },
538
+ width: {
539
+ type: "integer",
540
+ minimum: 200,
541
+ maximum: 1e4
542
+ },
543
+ height: {
544
+ type: "integer",
545
+ minimum: 200,
546
+ maximum: 1e4
547
+ }
548
+ },
549
+ required: [
550
+ "id",
551
+ "width",
552
+ "height"
553
+ ],
554
+ additionalProperties: false
555
+ }
556
+ },
557
+ "menu-list": {
558
+ usage: "menu list: the application menu",
559
+ description: "The desktop app's application menu as paths such as File>New Note, with the disabled items marked.",
560
+ arguments: none
561
+ },
562
+ "menu-click": {
563
+ usage: "menu click --path \"File>New Note\" [--force]: press a menu item",
564
+ description: "Press an item of the desktop app's application menu by its path (labels joined with >, as menu-list prints them). A disabled item is refused, and so is one that ends the app (Quit, Close Window on the last window) unless force is set.",
565
+ arguments: {
566
+ type: "object",
567
+ properties: {
568
+ path: {
569
+ type: "string",
570
+ description: "e.g. File>New Note"
571
+ },
572
+ force: {
573
+ type: "boolean",
574
+ description: "allow an item that ends the desktop app"
575
+ }
576
+ },
577
+ required: ["path"],
578
+ additionalProperties: false
579
+ }
193
580
  }
194
581
  };
195
582
  /** A verb's name in the `tools` registry the built-in chat reads: prefixed, so it never meets an app tool's name. */
@@ -209,6 +596,12 @@ var DoorError = class extends Error {
209
596
  this.name = "DoorError";
210
597
  }
211
598
  };
599
+ /** A shell answer as text an agent reads: windows and menu items one to a line. */
600
+ function renderShell(result) {
601
+ if ("windows" in result) return result.windows.length === 0 ? "no windows are open" : result.windows.map((window) => `${String(window.id)} ${JSON.stringify(window.title)} ${window.path} ${String(window.width)}x${String(window.height)}${window.focused ? " focused" : ""}`).join("\n");
602
+ if ("items" in result) return result.items.length === 0 ? "the app has no menu" : result.items.map((item) => `${item.path}${item.enabled ? "" : " (disabled)"}${item.ends === true ? " (ends the app; needs force)" : ""}`).join("\n");
603
+ return result.detail;
604
+ }
212
605
  /** The files an agent should read first, when the app has them. */
213
606
  const SELF_KNOWLEDGE = [
214
607
  "AGENTS.md",
@@ -260,6 +653,13 @@ var Door = class {
260
653
  };
261
654
  }
262
655
  case "tool-call": return this.callTool(input);
656
+ case "window-list":
657
+ case "window-open":
658
+ case "window-focus":
659
+ case "window-close":
660
+ case "window-resize":
661
+ case "menu-list":
662
+ case "menu-click": return this.desktop(verb, input, signal);
263
663
  default: throw new DoorError("unknown-verb", `unknown verb ${JSON.stringify(verb)}; the verbs are ${VERBS.join(", ")}`);
264
664
  }
265
665
  }
@@ -289,20 +689,22 @@ var Door = class {
289
689
  };
290
690
  }
291
691
  look(input, signal) {
692
+ const { window, rest } = this.windowOf(input);
292
693
  const request = this.parse({
293
- ...input,
694
+ ...rest,
294
695
  op: "snapshot"
295
696
  });
296
- return this.callPage("look", request, signal);
697
+ return this.callPage("look", request, signal, window);
297
698
  }
298
699
  async act(input, signal) {
299
700
  if (input.op === "snapshot") throw new DoorError("invalid", "use look to see the page");
300
- const request = this.parse(input);
301
- if (request.op !== "reload" && request.op !== "navigate") return await this.callPage(`act ${String(input.op)}`, request, signal);
701
+ const { window, rest } = this.windowOf(input);
702
+ const request = this.parse(rest);
703
+ if (request.op !== "reload" && request.op !== "navigate") return await this.callPage(`act ${String(input.op)}`, request, signal, window);
302
704
  const back = this.deps.channel.expectAttach(this.deps.pageBackMs ?? PAGE_BACK_MS);
303
705
  let answer;
304
706
  try {
305
- answer = await this.callPage(`act ${request.op}`, request, signal);
707
+ answer = await this.callPage(`act ${request.op}`, request, signal, window);
306
708
  } catch (cause) {
307
709
  if (!(cause instanceof UiControlError && cause.code === "disconnected")) throw cause;
308
710
  answer = {
@@ -335,6 +737,73 @@ var Door = class {
335
737
  return;
336
738
  }
337
739
  }
740
+ /**
741
+ * The windows and the menu of the desktop app, through the shell. Every call is audited with the window it was about; a call with no shell attached answers one line that says what to run;
742
+ * closing the last window is refused unless the call says `force` (it ends the desktop app, and the session an agent works in with it).
743
+ */
744
+ async desktop(verb, input, signal) {
745
+ const shell = this.deps.shell;
746
+ let request;
747
+ try {
748
+ request = parseShellRequest(verb, input);
749
+ } catch (cause) {
750
+ throw new DoorError("invalid", cause instanceof Error ? cause.message : "invalid request");
751
+ }
752
+ const windowId = "id" in request ? request.id : void 0;
753
+ const record = (outcome, detail, windowOf) => {
754
+ const id = windowOf ?? windowId;
755
+ this.deps.audit.record({
756
+ at: (/* @__PURE__ */ new Date()).toISOString(),
757
+ tool: verb,
758
+ ...request.op === "menu-click" ? { target: {
759
+ role: "menuitem",
760
+ name: request.path
761
+ } } : {},
762
+ ...id === void 0 ? {} : { window: id },
763
+ outcome,
764
+ ...detail === void 0 ? {} : { detail },
765
+ approval: "token"
766
+ });
767
+ };
768
+ try {
769
+ if (shell === void 0 || !shell.connected) throw new ShellError("no-desktop", NO_DESKTOP);
770
+ if (request.op === "window-close" && request.force !== true) {
771
+ const { windows } = await shell.call({ op: "window-list" }, { signal });
772
+ if (windows.length === 1 && windows[0]?.id === request.id) throw new ShellError("last-window", `window ${String(request.id)} is the app's last window: closing it ends the desktop app. Open another window first, or pass force to end it on purpose`);
773
+ }
774
+ const result = await shell.call(request, { signal });
775
+ record("ok", void 0, "id" in result ? result.id : void 0);
776
+ return {
777
+ value: result,
778
+ text: renderShell(result)
779
+ };
780
+ } catch (cause) {
781
+ if (!(cause instanceof ShellError)) throw cause;
782
+ record([
783
+ "invalid",
784
+ "no-such-window",
785
+ "last-window",
786
+ "no-such-menu-item",
787
+ "disabled",
788
+ "quits-app",
789
+ "no-desktop"
790
+ ].includes(cause.code) ? "refused" : "failed", cause.code);
791
+ throw new DoorError(cause.code, cause.message);
792
+ }
793
+ }
794
+ /** `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. */
795
+ windowOf(input) {
796
+ const { window, ...rest } = input;
797
+ if (window === void 0) return {
798
+ window: void 0,
799
+ rest
800
+ };
801
+ if (typeof window !== "number" || !Number.isSafeInteger(window) || window < 1) throw new DoorError("invalid", "window must be the number `window list` prints for the window");
802
+ return {
803
+ window,
804
+ rest
805
+ };
806
+ }
338
807
  parse(input) {
339
808
  try {
340
809
  return parseUiRequest(input);
@@ -343,11 +812,12 @@ var Door = class {
343
812
  }
344
813
  }
345
814
  /** Run one call on the page and record it. The audit holds which control was touched and how it ended, never what was typed. */
346
- async callPage(verb, request, signal) {
815
+ async callPage(verb, request, signal, window) {
347
816
  const approval = "token";
348
817
  try {
349
818
  const value = await this.deps.channel.call(request, {
350
819
  signal,
820
+ ...window === void 0 ? {} : { window },
351
821
  ...request.op === "wait" ? { timeoutMs: (request.timeoutMs ?? 3e3) + 5e3 } : {}
352
822
  });
353
823
  const target = "target" in value ? value.target : void 0;
@@ -355,6 +825,7 @@ var Door = class {
355
825
  at: (/* @__PURE__ */ new Date()).toISOString(),
356
826
  tool: verb,
357
827
  ...target === void 0 ? {} : { target },
828
+ ...window === void 0 ? {} : { window },
358
829
  outcome: "ok",
359
830
  approval
360
831
  });
@@ -373,6 +844,7 @@ var Door = class {
373
844
  this.deps.audit.record({
374
845
  at: (/* @__PURE__ */ new Date()).toISOString(),
375
846
  tool: verb,
847
+ ...window === void 0 ? {} : { window },
376
848
  outcome: refused ? "refused" : "failed",
377
849
  detail: code,
378
850
  approval
@@ -735,11 +1207,41 @@ function windowsShim(root, target) {
735
1207
  ].map(cmdWord).join(" ");
736
1208
  return `@echo off\r\nsetlocal\r\nrem This app's door (acryl-door): reaches only the app running from this folder. Rewritten each time the app starts.\r\n${target.electron ? "set ELECTRON_RUN_AS_NODE=1\r\n" : ""}${words} %*\r\n`;
737
1209
  }
738
- function writeAtomically(file, content, mode) {
1210
+ /** The error codes with which Windows refuses to replace a file that a running process (a `cmd.exe` executing the batch file) has open. */
1211
+ const LOCKED = /* @__PURE__ */ new Set([
1212
+ "EPERM",
1213
+ "EBUSY",
1214
+ "EACCES"
1215
+ ]);
1216
+ const realFs = {
1217
+ rename: renameSync,
1218
+ pause: (ms) => {
1219
+ Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
1220
+ }
1221
+ };
1222
+ /**
1223
+ * Write `content` to `file` so a reader never sees half of it. A shim that already holds this content is left alone: a restart of the app (the dev server restarts on a change)
1224
+ * rewrites the same text, and on Windows replacing a batch file that an agent's `acryl` call is running fails with EPERM, which stopped the app from starting. A file that
1225
+ * is held while its content differs is retried for about a second, and then reported: the old shim stays (it still works), and the app starts.
1226
+ */
1227
+ function writeAtomically(file, content, mode, fs = realFs) {
1228
+ try {
1229
+ if (readFileSync(file, "utf8") === content) return;
1230
+ } catch {}
739
1231
  const temporary = `${file}.${String(process.pid)}.tmp`;
740
1232
  writeFileSync(temporary, content, { mode });
741
1233
  if (process.platform !== "win32") chmodSync(temporary, mode);
742
- renameSync(temporary, file);
1234
+ for (let attempt = 0;; attempt += 1) try {
1235
+ fs.rename(temporary, file);
1236
+ return;
1237
+ } catch (cause) {
1238
+ const code = cause.code;
1239
+ if (code === void 0 || !LOCKED.has(code) || attempt >= 10) {
1240
+ rmSync(temporary, { force: true });
1241
+ throw cause;
1242
+ }
1243
+ fs.pause(25 * (attempt + 1));
1244
+ }
743
1245
  }
744
1246
  /** Write `.acryl/bin/acryl` and `.acryl/bin/acryl.cmd` for the app at `root`. @returns the folder they are in. */
745
1247
  function writeShims(root, target) {
@@ -815,8 +1317,10 @@ function apply(ctx, rawConfig = {}) {
815
1317
  };
816
1318
  const secret = newSecret();
817
1319
  const channel = new UiChannel();
1320
+ const shell = new ShellChannel();
818
1321
  const door = new Door({
819
1322
  channel,
1323
+ shell,
820
1324
  audit: new AuditLog(auditPath(stateDir(app.root))),
821
1325
  app: {
822
1326
  name: app.name,
@@ -831,6 +1335,9 @@ function apply(ctx, rawConfig = {}) {
831
1335
  ctx.effect(() => () => {
832
1336
  channel.close();
833
1337
  }, "acryl-door: page channel");
1338
+ ctx.effect(() => () => {
1339
+ shell.close();
1340
+ }, "acryl-door: desktop shell channel");
834
1341
  const pageModule = new Acryl().get(`${DOOR_PREFIX}/page.js`, (c) => c.body(`import '${DOOR_PREFIX}/page/main.ts'\n`, 200, { "content-type": "text/javascript; charset=utf-8" })).route(`${DOOR_PREFIX}/page`, clientModules({ root: PAGE_DIR }));
835
1342
  const tag = `<script type="module" src="${DOOR_PREFIX}/page.js"><\/script>`;
836
1343
  const addScript = async (c, next) => {
@@ -854,14 +1361,18 @@ function apply(ctx, rawConfig = {}) {
854
1361
  const router = ctx.router;
855
1362
  if (typeof router.wrap === "function") router.wrap(addScript);
856
1363
  else ctx.router.route("/", new Acryl().use("*", addScript));
857
- ctx.router.route("/", privateApi(DOOR_PREFIX, pageModule, pageChannelRoutes(channel, { prefix: DOOR_PREFIX }), operatorRoutes(DOOR_PREFIX, secret, door, reportError), mcpRoutes(DOOR_PREFIX, secret, door, reportError)));
1364
+ ctx.router.route("/", privateApi(DOOR_PREFIX, pageModule, pageChannelRoutes(channel, { prefix: DOOR_PREFIX }), shellRoutes(DOOR_PREFIX, secret, shell), operatorRoutes(DOOR_PREFIX, secret, door, reportError), mcpRoutes(DOOR_PREFIX, secret, door, reportError)));
858
1365
  ctx.inject(["tools"], (inner) => {
859
1366
  inner.effect(() => registerChatTools(inner.get("tools"), door), "acryl-door: the verbs in the tools registry");
860
1367
  });
861
1368
  ctx.effect(function* () {
862
1369
  yield ctx.reflect.provide("door", door);
863
1370
  writeDoorFile(app.root, loopbackUrl(server.url), secret);
864
- writeShims(app.root, shimTarget());
1371
+ try {
1372
+ writeShims(app.root, shimTarget());
1373
+ } catch (cause) {
1374
+ reportError("write .acryl/bin", cause);
1375
+ }
865
1376
  yield () => {
866
1377
  removeDoorFile(app.root, secret);
867
1378
  };
@@ -1,6 +1,6 @@
1
1
  import { t as Acryl } from "../app-D8riatvv.js";
2
- import { n as AuditEntry, y as UiSnapshot } from "../protocol-C37gPePD.js";
3
- import { i as UiChannel, n as auditPath, r as PageStream, t as AuditLog } from "../audit-D5z9uwOP.js";
2
+ import { n as AuditEntry, y as UiSnapshot } from "../protocol-CR31M7K_.js";
3
+ import { i as UiChannel, n as auditPath, r as PageStream, t as AuditLog } from "../audit-GcLtvNLV.js";
4
4
 
5
5
  //#region ../../plugins/acryl-door/src/host/page-routes.d.ts
6
6
  /** How often the Host writes a `ping`; the page gives up on a stream that is silent for three of these. */
package/lib/door/host.js CHANGED
@@ -1,2 +1,2 @@
1
- import { d as AuditLog, f as auditPath, n as HEARTBEAT_MS, p as UiChannel, r as pageChannelRoutes, t as renderSnapshot } from "../snapshot-text-Bbx43mN0.js";
1
+ import { d as AuditLog, f as auditPath, n as HEARTBEAT_MS, p as UiChannel, r as pageChannelRoutes, t as renderSnapshot } from "../snapshot-text-CtQm3n8n.js";
2
2
  export { AuditLog, HEARTBEAT_MS, UiChannel, auditPath, pageChannelRoutes, renderSnapshot };