acrylblends 1.0.3 → 1.0.5
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/docs/framework/agents/index.md +6 -4
- package/docs/framework/workshop/README.md +3 -3
- package/docs/framework/workshop/facilitator.md +1 -0
- package/lib/accept-CNNHu4Cf.js +193 -0
- package/lib/{accepts-XBuZPVRI.js → accepts-Btq83xPd.js} +1 -1
- package/lib/accepts.js +1 -1
- package/lib/acryl-base.js +1 -331
- package/lib/adapter-CaZ6FZUU.js +37 -0
- package/lib/adapter.js +1 -36
- package/lib/{app-BM0rTCp7.js → app-CQcGWjrk.js} +4 -4
- package/lib/app-base-CpRMZL4v.js +332 -0
- package/lib/{audit-BXnYsLge.d.ts → audit-D5z9uwOP.d.ts} +14 -1
- package/lib/{base-NSN_SNv-.js → base-D_6kfOr-.js} +1 -1
- package/lib/basic-auth-C0HTRpXh.js +20 -0
- package/lib/basic-auth-DFLflXNe.js +95 -0
- package/lib/basic-auth.js +1 -94
- package/lib/bearer-auth-Cwxt-vgS.js +94 -0
- package/lib/bearer-auth.js +1 -93
- package/lib/body-D7f4e7w_.js +94 -0
- package/lib/body-limit-WmIjWRLt.js +66 -0
- package/lib/body-limit.js +1 -65
- package/lib/buffer-CkEOJCcY.js +51 -0
- package/lib/bun.js +3 -3
- package/lib/cache-B0bHH7s5.js +169 -0
- package/lib/cache.js +1 -168
- package/lib/{client-DDLXwhBo.js → client-BrMFnMEd.js} +1 -1
- package/lib/{client-modules-CLvWOjbd.js → client-modules-drj4mjhj.js} +4 -4
- package/lib/client.js +1 -1
- package/lib/color-COa3DplO.js +37 -0
- package/lib/combine-TWuGdcbC.js +137 -0
- package/lib/combine.js +1 -136
- package/lib/{components-cEW3ovK0.js → components-B2ka9Ax1.js} +4 -4
- package/lib/{components-JXVm45kK.js → components-PVXwaOaC.js} +2 -2
- package/lib/compress-BzWqsHF1.js +79 -0
- package/lib/compress-C-9vK4B2.d.ts +11 -0
- package/lib/compress-DCyrrQ5D.js +11 -0
- package/lib/compress.d.ts +1 -1
- package/lib/compress.js +2 -78
- package/lib/concurrent-DrF3d2c-.js +30 -0
- package/lib/conninfo-BCROmdnf.d.ts +11 -0
- package/lib/conninfo-CWJwieh4.js +19 -0
- package/lib/constants-CBSEudrb.js +7 -0
- package/lib/{context-DDO0cfuP.js → context-CkOh7Ygv.js} +2 -2
- package/lib/context-storage-iuPoI_CR.js +52 -0
- package/lib/context-storage.js +1 -51
- package/lib/cookie-B1LlkzOe.js +125 -0
- package/lib/cookie-BHIduy2T.js +82 -0
- package/lib/cookie.js +1 -81
- package/lib/cordis/db-sqlite.d.ts +1 -1
- package/lib/cordis/door.d.ts +17 -3
- package/lib/cordis/door.js +123 -16
- package/lib/cordis/inspect.js +4 -4
- package/lib/cordis/kv-db.d.ts +1 -1
- package/lib/cordis/kv-db.js +1 -1
- package/lib/cordis/kv-json.d.ts +1 -1
- package/lib/cordis/kv-json.js +1 -1
- package/lib/cordis/node-server.d.ts +1 -1
- package/lib/cordis/node-server.js +3 -116
- package/lib/cordis/router-http.d.ts +1 -1
- package/lib/cordis/router-http.js +2 -2
- package/lib/cordis/router.d.ts +1 -1
- package/lib/cordis/router.js +4 -4
- package/lib/cordis/state.js +7 -7
- package/lib/{core-CaY8XDZ6.js → core-BOu1jYua.js} +1 -1
- package/lib/cors-CbAQ-8qU.js +106 -0
- package/lib/cors.js +1 -105
- package/lib/crypto-iVP-N-tS.js +34 -0
- package/lib/csrf-DqqOZtgy.js +101 -0
- package/lib/csrf.js +1 -100
- package/lib/css-C5CPVMvM.js +134 -0
- package/lib/css.js +2 -133
- package/lib/db/sqlite.d.ts +1 -25
- package/lib/dev-CLgu-JRe.js +50 -0
- package/lib/dev.js +1 -49
- package/lib/dom-YjND9Jt3.js +74 -0
- package/lib/door/host.d.ts +2 -2
- package/lib/door/host.js +1 -1
- package/lib/door/page.d.ts +12 -1
- package/lib/door/page.js +22 -1
- package/lib/door/protocol.d.ts +2 -2
- package/lib/door/protocol.js +2 -197
- package/lib/encode-BhbNLrjp.js +33 -0
- package/lib/factory-CLdSf-xS.js +30 -0
- package/lib/factory.js +1 -29
- package/lib/handler-C0wZ0KwO.js +12 -0
- package/lib/helpers-BEjcdbgw.js +5 -0
- package/lib/{hooks-w72iLeFR.js → hooks-CfC07ri0.js} +1 -1
- package/lib/html-CgNa89xw.js +31 -0
- package/lib/html-DGExBFNg.js +103 -0
- package/lib/html.d.ts +2 -5
- package/lib/html.js +2 -30
- package/lib/http-exception-MjWkT89V.js +54 -0
- package/lib/http-exception.js +1 -53
- package/lib/{index-D4nVR3wu.d.ts → index-DMOtA-NJ.d.ts} +7 -1
- package/lib/index-dAzQsJG9.d.ts +6 -0
- package/lib/index.js +3 -3
- package/lib/ip-restriction-jKtKCRM_.js +166 -0
- package/lib/ip-restriction.js +1 -165
- package/lib/ipaddr-DCoIo528.js +238 -0
- package/lib/jsx/dom/client.js +1 -1
- package/lib/jsx/dom/jsx-dev-runtime.js +1 -1
- package/lib/jsx/dom/jsx-runtime.js +2 -1
- package/lib/jsx/dom/server.js +3 -3
- package/lib/jsx/dom.js +5 -73
- package/lib/jsx/jsx-dev-runtime.d.ts +2 -5
- package/lib/jsx/jsx-dev-runtime.js +2 -13
- package/lib/jsx/jsx-runtime.d.ts +2 -2
- package/lib/jsx/jsx-runtime.js +4 -4
- package/lib/jsx/streaming.js +1 -1
- package/lib/jsx-CUQgttxU.js +47 -0
- package/lib/jsx-dev-runtime-B4WTyq8r.js +14 -0
- package/lib/{jsx-dev-runtime-CWLRA2F3.js → jsx-dev-runtime-BLfA-EhI.js} +2 -2
- package/lib/jsx-dev-runtime-CGt4kNYr.d.ts +6 -0
- package/lib/jsx-renderer-CfifM4hT.js +99 -0
- package/lib/jsx-renderer.js +1 -98
- package/lib/jsx-runtime-COCRWBJ9.js +2 -0
- package/lib/jsx.js +5 -46
- package/lib/{jwk-LkzeoaU8.js → jwk-CDPpYQpD.js} +3 -3
- package/lib/jwk.js +1 -1
- package/lib/{jwt-BGJBw5AF.js → jwt-BXoIA2fQ.js} +2 -2
- package/lib/{jwt-CskdhEW9.js → jwt-lyS4Q3mT.js} +3 -3
- package/lib/jwt.js +2 -2
- package/lib/kv/db.d.ts +1 -10
- package/lib/kv/db.js +1 -56
- package/lib/kv/json.d.ts +1 -9
- package/lib/kv/json.js +1 -223
- package/lib/{language-D9a4zqvn.js → language-DeGRtLJt.js} +2 -2
- package/lib/language.js +1 -1
- package/lib/{linear-router-C3FPib6h.js → linear-router-CmZn_JLV.js} +2 -2
- package/lib/logger-Ckxu8diz.js +54 -0
- package/lib/logger.js +1 -53
- package/lib/loopback-only-BbasoF7R.js +90 -0
- package/lib/loopback-only.js +1 -89
- package/lib/method-not-allowed-TO2_hnfl.js +82 -0
- package/lib/method-not-allowed.js +1 -81
- package/lib/method-override-DnZKfJbV.js +103 -0
- package/lib/method-override.js +1 -102
- package/lib/middleware-BfG_fp8P.js +12 -0
- package/lib/mime-BnScXkQn.js +77 -0
- package/lib/mount-QkK_3NtJ.js +67 -0
- package/lib/mount.js +1 -66
- package/lib/node-server/conninfo.d.ts +1 -10
- package/lib/node-server/conninfo.js +1 -18
- package/lib/node-server/register.js +25 -2
- package/lib/node-server/serve-static.d.ts +1 -17
- package/lib/node-server/serve-static.js +1 -1
- package/lib/node-server.d.ts +2 -2
- package/lib/node-server.js +4 -4
- package/lib/open-BqK4D2Wn.d.ts +11 -0
- package/lib/open-But9E2rL.js +57 -0
- package/lib/open-CULVJgOc.d.ts +12 -0
- package/lib/open-Dp4sxspc.js +224 -0
- package/lib/open-T7kgbO2O.d.ts +27 -0
- package/lib/{pattern-router-HZxjDGEQ.js → pattern-router-CJBecJBd.js} +1 -1
- package/lib/powered-by-CXQNjNIX.js +24 -0
- package/lib/powered-by.js +1 -23
- package/lib/pretty-json-CeC1yvYt.js +39 -0
- package/lib/pretty-json.js +1 -38
- package/lib/{protocol-BizVt4J6.d.ts → protocol-C37gPePD.d.ts} +9 -2
- package/lib/protocol-Dt0AZBOz.js +213 -0
- package/lib/proxy-C2w_mH7k.js +121 -0
- package/lib/proxy.js +1 -120
- package/lib/quick.js +4 -4
- package/lib/{reg-exp-router-DC1R3qoY.js → reg-exp-router-C_WV84eV.js} +2 -2
- package/lib/request-DvMgGtlW.js +323 -0
- package/lib/request.js +1 -322
- package/lib/route-D8m-a2I1.js +126 -0
- package/lib/route.js +1 -125
- package/lib/router/linear-router.js +1 -1
- package/lib/router/pattern-router.js +1 -1
- package/lib/router/reg-exp-router.js +1 -1
- package/lib/router/smart-router.js +1 -1
- package/lib/router/trie-router.js +1 -1
- package/lib/router-Be_o6nFu.js +35 -0
- package/lib/router.js +1 -34
- package/lib/schema-validator-EK3cUUCt.js +22 -0
- package/lib/schema-validator.js +1 -21
- package/lib/{secure-headers-mP3j6Hm6.js → secure-headers-C5ZOcXqk.js} +1 -1
- package/lib/secure-headers.js +1 -1
- package/lib/serve-static-B7sqnftW.d.ts +18 -0
- package/lib/{serve-static-3uo_9rjF.js → serve-static-C1Nkj_xB.js} +2 -2
- package/lib/{serve-static-Cp5adzGo.js → serve-static-EiJf1zkO.js} +4 -4
- package/lib/serve-static.js +1 -1
- package/lib/{smart-router-C5CZ5XSM.js → smart-router-BjxkDQCE.js} +1 -1
- package/lib/{snapshot-text-DmE0YfeZ.js → snapshot-text-Bbx43mN0.js} +57 -9
- package/lib/{src-2Z4mJg0x.js → src-BTBm4OPD.js} +22 -6
- package/lib/src-LX0E4ec4.js +117 -0
- package/lib/{ssg-BwjcA-Ge.js → ssg-BEl7MHqg.js} +5 -5
- package/lib/ssg.js +1 -1
- package/lib/stream-CzM_SHkq.js +88 -0
- package/lib/{streaming-DHST3Scg.js → streaming-BlDaph8U.js} +3 -3
- package/lib/streaming.js +1 -1
- package/lib/{test-websocket-D8WWqE5G.js → test-websocket-BLeEvybj.js} +2 -2
- package/lib/testing-BQx5T_UX.js +17 -0
- package/lib/testing.js +1 -16
- package/lib/timeout-FeqZ9A-0.js +42 -0
- package/lib/timeout.js +1 -41
- package/lib/tiny.js +2 -2
- package/lib/trailing-slash-DwTx-1sk.js +85 -0
- package/lib/trailing-slash.js +1 -84
- package/lib/{trie-router-CfFbapzL.js → trie-router-JydLKUa2.js} +2 -2
- package/lib/url-BPh4d6j-.js +197 -0
- package/lib/utils/accept.js +1 -192
- package/lib/utils/basic-auth.js +1 -19
- package/lib/utils/body.js +1 -93
- package/lib/utils/buffer.js +1 -50
- package/lib/utils/color.js +1 -36
- package/lib/utils/compress.d.ts +1 -10
- package/lib/utils/compress.js +1 -10
- package/lib/utils/concurrent.js +1 -29
- package/lib/utils/constants.js +1 -6
- package/lib/utils/cookie.js +1 -124
- package/lib/utils/crypto.js +1 -33
- package/lib/utils/encode.js +1 -32
- package/lib/utils/handler.js +1 -11
- package/lib/utils/html.js +1 -102
- package/lib/utils/ipaddr.js +1 -237
- package/lib/utils/jwt.js +1 -1
- package/lib/utils/mime.js +1 -76
- package/lib/utils/stream.js +1 -87
- package/lib/utils/url.js +1 -196
- package/lib/{validator-D84mq-dO.js → validator-Btiyt5VJ.js} +3 -3
- package/lib/validator.js +1 -1
- package/lib/websocket-DsyBIKVA.js +56 -0
- package/lib/ws.js +1 -55
- package/package.json +1 -1
- package/plugins/acryl-door/src/cli/acryl.ts +5 -3
- package/plugins/acryl-door/src/host/channel.ts +36 -0
- package/plugins/acryl-door/src/host/chat-tools.ts +2 -0
- package/plugins/acryl-door/src/host/door.ts +44 -5
- package/plugins/acryl-door/src/host/shim.ts +23 -0
- package/plugins/acryl-door/src/host/verbs.ts +4 -3
- package/plugins/acryl-door/src/index.ts +39 -3
- package/plugins/acryl-door/src/page/driver/driver.ts +22 -1
- package/plugins/acryl-door/src/page/protocol.ts +13 -2
- package/plugins/acryl-node-server/src/register.ts +29 -2
- package/plugins/acryl-router/src/service.ts +19 -1
- package/plugins/agent/model.ts +6 -1
- package/plugins/agent/tools.ts +12 -2
- package/plugins/agent-panel/client/describe.ts +44 -0
- package/plugins/agent-panel/client/main.ts +67 -35
- package/plugins/agent-panel/index.ts +137 -35
- package/plugins/desktop/attach.ts +72 -0
- package/plugins/desktop/main.ts +17 -4
- package/plugins/extensions/index.ts +3 -4
- package/plugins/extensions/stage.ts +33 -5
- package/plugins/openai/index.ts +1 -1
- package/shared/DEFAULTS.md +4 -0
- package/templates/agent/AGENTS.md +2 -0
- package/templates/agent/blend.yaml +4 -1
- package/templates/agent/src/app.ts +2 -0
- package/templates/agent/src/routes.ts +1 -1
- package/templates/agent/template.json +1 -0
- package/templates/agent/test/builder.test.ts +26 -21
- package/lib/helpers-CvTauQT5.js +0 -5
- package/lib/middleware-CPQ8himN.js +0 -12
package/lib/utils/stream.js
CHANGED
|
@@ -1,88 +1,2 @@
|
|
|
1
|
-
|
|
2
|
-
/**
|
|
3
|
-
* @module
|
|
4
|
-
* Stream utility.
|
|
5
|
-
*/
|
|
6
|
-
var StreamingApi = class {
|
|
7
|
-
writer;
|
|
8
|
-
encoder;
|
|
9
|
-
writable;
|
|
10
|
-
abortSubscribers = [];
|
|
11
|
-
responseReadable;
|
|
12
|
-
/**
|
|
13
|
-
* Whether the stream has been aborted.
|
|
14
|
-
*/
|
|
15
|
-
aborted = false;
|
|
16
|
-
/**
|
|
17
|
-
* Whether the stream has been closed normally.
|
|
18
|
-
*/
|
|
19
|
-
closed = false;
|
|
20
|
-
constructor(writable, _readable) {
|
|
21
|
-
this.writable = writable;
|
|
22
|
-
this.writer = writable.getWriter();
|
|
23
|
-
this.encoder = new TextEncoder();
|
|
24
|
-
const reader = _readable.getReader();
|
|
25
|
-
this.abortSubscribers.push(async () => {
|
|
26
|
-
await reader.cancel();
|
|
27
|
-
});
|
|
28
|
-
this.responseReadable = new ReadableStream({
|
|
29
|
-
async pull(controller) {
|
|
30
|
-
const { done, value } = await reader.read();
|
|
31
|
-
done ? controller.close() : controller.enqueue(value);
|
|
32
|
-
},
|
|
33
|
-
cancel: () => {
|
|
34
|
-
if (!this.closed) this.abort();
|
|
35
|
-
}
|
|
36
|
-
});
|
|
37
|
-
}
|
|
38
|
-
async write(input) {
|
|
39
|
-
try {
|
|
40
|
-
if (typeof input === "string") input = this.encoder.encode(input);
|
|
41
|
-
await this.writer.write(input);
|
|
42
|
-
} catch {}
|
|
43
|
-
return this;
|
|
44
|
-
}
|
|
45
|
-
async writeln(input) {
|
|
46
|
-
await this.write(input + "\n");
|
|
47
|
-
return this;
|
|
48
|
-
}
|
|
49
|
-
sleep(ms) {
|
|
50
|
-
return new Promise((res) => setTimeout(res, ms));
|
|
51
|
-
}
|
|
52
|
-
async close() {
|
|
53
|
-
this.closed = true;
|
|
54
|
-
try {
|
|
55
|
-
await this.writer.close();
|
|
56
|
-
} catch {}
|
|
57
|
-
}
|
|
58
|
-
async pipe(body) {
|
|
59
|
-
this.writer.releaseLock();
|
|
60
|
-
try {
|
|
61
|
-
await body.pipeTo(this.writable, {
|
|
62
|
-
preventClose: true,
|
|
63
|
-
preventAbort: true
|
|
64
|
-
});
|
|
65
|
-
} finally {
|
|
66
|
-
this.writer = this.writable.getWriter();
|
|
67
|
-
}
|
|
68
|
-
}
|
|
69
|
-
onAbort(listener) {
|
|
70
|
-
this.abortSubscribers.push(listener);
|
|
71
|
-
}
|
|
72
|
-
/**
|
|
73
|
-
* Abort the stream.
|
|
74
|
-
* You can call this method when stream is aborted by external event.
|
|
75
|
-
*/
|
|
76
|
-
abort() {
|
|
77
|
-
if (!this.aborted) {
|
|
78
|
-
this.aborted = true;
|
|
79
|
-
this.abortSubscribers.forEach((subscriber) => {
|
|
80
|
-
try {
|
|
81
|
-
Promise.resolve(subscriber()).catch(() => {});
|
|
82
|
-
} catch {}
|
|
83
|
-
});
|
|
84
|
-
}
|
|
85
|
-
}
|
|
86
|
-
};
|
|
87
|
-
//#endregion
|
|
1
|
+
import { t as StreamingApi } from "../stream-CzM_SHkq.js";
|
|
88
2
|
export { StreamingApi };
|
package/lib/utils/url.js
CHANGED
|
@@ -1,197 +1,2 @@
|
|
|
1
|
-
|
|
2
|
-
const splitPath = (path) => {
|
|
3
|
-
const paths = path.split("/");
|
|
4
|
-
if (paths[0] === "") paths.shift();
|
|
5
|
-
return paths;
|
|
6
|
-
};
|
|
7
|
-
const splitRoutingPath = (routePath) => {
|
|
8
|
-
const { groups, path } = extractGroupsFromPath(routePath);
|
|
9
|
-
const paths = splitPath(path);
|
|
10
|
-
return replaceGroupMarks(paths, groups);
|
|
11
|
-
};
|
|
12
|
-
const extractGroupsFromPath = (path) => {
|
|
13
|
-
const groups = [];
|
|
14
|
-
path = path.replace(/\{[^}]+\}/g, (match, index) => {
|
|
15
|
-
const mark = `@${index}`;
|
|
16
|
-
groups.push([mark, match]);
|
|
17
|
-
return mark;
|
|
18
|
-
});
|
|
19
|
-
return {
|
|
20
|
-
groups,
|
|
21
|
-
path
|
|
22
|
-
};
|
|
23
|
-
};
|
|
24
|
-
const replaceGroupMarks = (paths, groups) => {
|
|
25
|
-
for (let i = groups.length - 1; i >= 0; i--) {
|
|
26
|
-
const [mark] = groups[i];
|
|
27
|
-
for (let j = paths.length - 1; j >= 0; j--) if (paths[j].includes(mark)) {
|
|
28
|
-
paths[j] = paths[j].replace(mark, groups[i][1]);
|
|
29
|
-
break;
|
|
30
|
-
}
|
|
31
|
-
}
|
|
32
|
-
return paths;
|
|
33
|
-
};
|
|
34
|
-
const patternCache = {};
|
|
35
|
-
const getPattern = (label, next) => {
|
|
36
|
-
if (label === "*") return "*";
|
|
37
|
-
const match = label.match(/^\:([^\{\}]+)(?:\{(.+)\})?$/);
|
|
38
|
-
if (match) {
|
|
39
|
-
const cacheKey = `${label}#${next}`;
|
|
40
|
-
if (!patternCache[cacheKey]) if (match[2]) patternCache[cacheKey] = next && next[0] !== ":" && next[0] !== "*" ? [
|
|
41
|
-
cacheKey,
|
|
42
|
-
match[1],
|
|
43
|
-
new RegExp(`^${match[2]}(?=/${next})`)
|
|
44
|
-
] : [
|
|
45
|
-
label,
|
|
46
|
-
match[1],
|
|
47
|
-
new RegExp(`^${match[2]}$`)
|
|
48
|
-
];
|
|
49
|
-
else patternCache[cacheKey] = [
|
|
50
|
-
label,
|
|
51
|
-
match[1],
|
|
52
|
-
true
|
|
53
|
-
];
|
|
54
|
-
return patternCache[cacheKey];
|
|
55
|
-
}
|
|
56
|
-
return null;
|
|
57
|
-
};
|
|
58
|
-
const tryDecode = (str, decoder) => {
|
|
59
|
-
try {
|
|
60
|
-
return decoder(str);
|
|
61
|
-
} catch {
|
|
62
|
-
return str.replace(/(?:%[0-9A-Fa-f]{2})+/g, (match) => {
|
|
63
|
-
try {
|
|
64
|
-
return decoder(match);
|
|
65
|
-
} catch {
|
|
66
|
-
return match;
|
|
67
|
-
}
|
|
68
|
-
});
|
|
69
|
-
}
|
|
70
|
-
};
|
|
71
|
-
/**
|
|
72
|
-
* Try to apply decodeURI() to given string.
|
|
73
|
-
* If it fails, skip invalid percent encoding or invalid UTF-8 sequences, and apply decodeURI() to the rest as much as possible.
|
|
74
|
-
* @param str The string to decode.
|
|
75
|
-
* @returns The decoded string that sometimes contains undecodable percent encoding.
|
|
76
|
-
* @example
|
|
77
|
-
* tryDecodeURI('Hello%20World') // 'Hello World'
|
|
78
|
-
* tryDecodeURI('Hello%20World/%A4%A2') // 'Hello World/%A4%A2'
|
|
79
|
-
*/
|
|
80
|
-
const tryDecodeURI = (str) => tryDecode(str, decodeURI);
|
|
81
|
-
const getPath = (request) => {
|
|
82
|
-
const url = request.url;
|
|
83
|
-
const start = url.indexOf("/", url.indexOf(":") + 4);
|
|
84
|
-
let i = start;
|
|
85
|
-
for (; i < url.length; i++) {
|
|
86
|
-
const charCode = url.charCodeAt(i);
|
|
87
|
-
if (charCode === 37) {
|
|
88
|
-
const queryIndex = url.indexOf("?", i);
|
|
89
|
-
const hashIndex = url.indexOf("#", i);
|
|
90
|
-
const end = queryIndex === -1 ? hashIndex === -1 ? void 0 : hashIndex : hashIndex === -1 ? queryIndex : Math.min(queryIndex, hashIndex);
|
|
91
|
-
const path = url.slice(start, end);
|
|
92
|
-
return tryDecodeURI(path.includes("%25") ? path.replace(/%25/g, "%2525") : path);
|
|
93
|
-
} else if (charCode === 63 || charCode === 35) break;
|
|
94
|
-
}
|
|
95
|
-
return url.slice(start, i);
|
|
96
|
-
};
|
|
97
|
-
/**
|
|
98
|
-
* @deprecated
|
|
99
|
-
* Use the `URL` API instead.
|
|
100
|
-
*/
|
|
101
|
-
const getQueryStrings = (url) => {
|
|
102
|
-
const queryIndex = url.indexOf("?", 8);
|
|
103
|
-
if (queryIndex === -1) return "";
|
|
104
|
-
const hashIndex = url.indexOf("#", 8);
|
|
105
|
-
return hashIndex === -1 ? url.slice(queryIndex) : queryIndex < hashIndex ? url.slice(queryIndex, hashIndex) : "";
|
|
106
|
-
};
|
|
107
|
-
const getPathNoStrict = (request) => {
|
|
108
|
-
const result = getPath(request);
|
|
109
|
-
return result.length > 1 && result.at(-1) === "/" ? result.slice(0, -1) : result;
|
|
110
|
-
};
|
|
111
|
-
/**
|
|
112
|
-
* Merge paths.
|
|
113
|
-
* @param {string[]} ...paths - The paths to merge.
|
|
114
|
-
* @returns {string} The merged path.
|
|
115
|
-
* @example
|
|
116
|
-
* mergePath('/api', '/users') // '/api/users'
|
|
117
|
-
* mergePath('/api/', '/users') // '/api/users'
|
|
118
|
-
* mergePath('/api', '/') // '/api'
|
|
119
|
-
* mergePath('/api/', '/') // '/api/'
|
|
120
|
-
*/
|
|
121
|
-
const mergePath = (base, sub, ...rest) => {
|
|
122
|
-
if (rest.length) sub = mergePath(sub, ...rest);
|
|
123
|
-
return `${base?.[0] === "/" ? "" : "/"}${base}${sub === "/" ? "" : `${base?.at(-1) === "/" ? "" : "/"}${sub?.[0] === "/" ? sub.slice(1) : sub}`}`;
|
|
124
|
-
};
|
|
125
|
-
const checkOptionalParameter = (path) => {
|
|
126
|
-
if (path.charCodeAt(path.length - 1) !== 63 || !path.includes(":")) return null;
|
|
127
|
-
const segments = path.split("/");
|
|
128
|
-
const results = [];
|
|
129
|
-
let basePath = "";
|
|
130
|
-
segments.forEach((segment) => {
|
|
131
|
-
if (segment !== "" && !/\:/.test(segment)) basePath += "/" + segment;
|
|
132
|
-
else if (/\:/.test(segment)) if (segment.charCodeAt(segment.length - 1) === 63) {
|
|
133
|
-
if (results.length === 0 && basePath === "") results.push("/");
|
|
134
|
-
else results.push(basePath);
|
|
135
|
-
const optionalSegment = segment.slice(0, -1);
|
|
136
|
-
basePath += "/" + optionalSegment;
|
|
137
|
-
results.push(basePath);
|
|
138
|
-
} else basePath += "/" + segment;
|
|
139
|
-
});
|
|
140
|
-
return results.filter((v, i, a) => a.indexOf(v) === i);
|
|
141
|
-
};
|
|
142
|
-
const tryDecodeURIComponent = (str) => str.indexOf("%") !== -1 ? tryDecode(str, decodeURIComponent_) : str;
|
|
143
|
-
const _decodeURI = (value) => {
|
|
144
|
-
if (value.indexOf("+") !== -1) value = value.replace(/\+/g, " ");
|
|
145
|
-
return tryDecodeURIComponent(value);
|
|
146
|
-
};
|
|
147
|
-
const _getQueryParam = (url, key, multiple) => {
|
|
148
|
-
const hashIndex = url.indexOf("#", 8);
|
|
149
|
-
if (hashIndex !== -1) url = url.slice(0, hashIndex);
|
|
150
|
-
let encoded;
|
|
151
|
-
if (!multiple && key && key.indexOf("%") === -1 && key.indexOf("+") === -1) {
|
|
152
|
-
let keyIndex = url.indexOf("?", 8);
|
|
153
|
-
if (keyIndex === -1) return;
|
|
154
|
-
if (!url.startsWith(key, keyIndex + 1)) keyIndex = url.indexOf(`&${key}`, keyIndex + 1);
|
|
155
|
-
while (keyIndex !== -1) {
|
|
156
|
-
const trailingKeyCode = url.charCodeAt(keyIndex + key.length + 1);
|
|
157
|
-
if (trailingKeyCode === 61) {
|
|
158
|
-
const valueIndex = keyIndex + key.length + 2;
|
|
159
|
-
const endIndex = url.indexOf("&", valueIndex);
|
|
160
|
-
return _decodeURI(url.slice(valueIndex, endIndex === -1 ? void 0 : endIndex));
|
|
161
|
-
} else if (trailingKeyCode == 38 || isNaN(trailingKeyCode)) return "";
|
|
162
|
-
keyIndex = url.indexOf(`&${key}`, keyIndex + 1);
|
|
163
|
-
}
|
|
164
|
-
encoded = /[%+]/.test(url);
|
|
165
|
-
if (!encoded) return;
|
|
166
|
-
}
|
|
167
|
-
const results = Object.create(null);
|
|
168
|
-
encoded ??= /[%+]/.test(url);
|
|
169
|
-
let keyIndex = url.indexOf("?", 8);
|
|
170
|
-
while (keyIndex !== -1) {
|
|
171
|
-
const nextKeyIndex = url.indexOf("&", keyIndex + 1);
|
|
172
|
-
let valueIndex = url.indexOf("=", keyIndex);
|
|
173
|
-
if (valueIndex > nextKeyIndex && nextKeyIndex !== -1) valueIndex = -1;
|
|
174
|
-
let name = url.slice(keyIndex + 1, valueIndex === -1 ? nextKeyIndex === -1 ? void 0 : nextKeyIndex : valueIndex);
|
|
175
|
-
if (encoded) name = _decodeURI(name);
|
|
176
|
-
keyIndex = nextKeyIndex;
|
|
177
|
-
if (name === "") continue;
|
|
178
|
-
let value;
|
|
179
|
-
if (valueIndex === -1) value = "";
|
|
180
|
-
else {
|
|
181
|
-
value = url.slice(valueIndex + 1, nextKeyIndex === -1 ? void 0 : nextKeyIndex);
|
|
182
|
-
if (encoded) value = _decodeURI(value);
|
|
183
|
-
}
|
|
184
|
-
if (multiple) {
|
|
185
|
-
if (!(results[name] && Array.isArray(results[name]))) results[name] = [];
|
|
186
|
-
results[name].push(value);
|
|
187
|
-
} else results[name] ??= value;
|
|
188
|
-
}
|
|
189
|
-
return key ? results[key] : results;
|
|
190
|
-
};
|
|
191
|
-
const getQueryParam = _getQueryParam;
|
|
192
|
-
const getQueryParams = (url, key) => {
|
|
193
|
-
return _getQueryParam(url, key, true);
|
|
194
|
-
};
|
|
195
|
-
const decodeURIComponent_ = decodeURIComponent;
|
|
196
|
-
//#endregion
|
|
1
|
+
import { a as getPattern, c as getQueryStrings, d as splitRoutingPath, f as tryDecode, i as getPathNoStrict, l as mergePath, m as tryDecodeURIComponent, n as decodeURIComponent_, o as getQueryParam, p as tryDecodeURI, r as getPath, s as getQueryParams, t as checkOptionalParameter, u as splitPath } from "../url-BPh4d6j-.js";
|
|
197
2
|
export { checkOptionalParameter, decodeURIComponent_, getPath, getPathNoStrict, getPattern, getQueryParam, getQueryParams, getQueryStrings, mergePath, splitPath, splitRoutingPath, tryDecode, tryDecodeURI, tryDecodeURIComponent };
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { HTTPException } from "./http-exception.js";
|
|
2
|
-
import { bufferToFormData } from "./
|
|
3
|
-
import { getCookie } from "./cookie.js";
|
|
1
|
+
import { t as HTTPException } from "./http-exception-MjWkT89V.js";
|
|
2
|
+
import { t as bufferToFormData } from "./buffer-CkEOJCcY.js";
|
|
3
|
+
import { i as getCookie } from "./cookie-BHIduy2T.js";
|
|
4
4
|
//#region ../../plugins/acryl-router/src/core/validator/validator.ts
|
|
5
5
|
const jsonRegex = /^application\/([a-z-\.]+\+)?json(;\s*[a-zA-Z0-9\-]+\=([^;]+))*$/i;
|
|
6
6
|
const multipartRegex = /^multipart\/form-data(;\s?boundary=[a-zA-Z0-9'"()+_,\-./:=?]+)?$/i;
|
package/lib/validator.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { t as validator } from "./validator-
|
|
1
|
+
import { t as validator } from "./validator-Btiyt5VJ.js";
|
|
2
2
|
export { validator };
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
//#region ../../plugins/acryl-router/src/core/helper/websocket/index.ts
|
|
2
|
+
/**
|
|
3
|
+
* A context for controlling WebSockets
|
|
4
|
+
*/
|
|
5
|
+
var WSContext = class {
|
|
6
|
+
#init;
|
|
7
|
+
constructor(init) {
|
|
8
|
+
this.#init = init;
|
|
9
|
+
this.raw = init.raw;
|
|
10
|
+
this.url = init.url ? new URL(init.url) : null;
|
|
11
|
+
this.protocol = init.protocol ?? null;
|
|
12
|
+
}
|
|
13
|
+
send(source, options) {
|
|
14
|
+
this.#init.send(source, options ?? {});
|
|
15
|
+
}
|
|
16
|
+
raw;
|
|
17
|
+
binaryType = "arraybuffer";
|
|
18
|
+
get readyState() {
|
|
19
|
+
return this.#init.readyState;
|
|
20
|
+
}
|
|
21
|
+
get bufferedAmount() {
|
|
22
|
+
return this.#init.bufferedAmount ?? 0;
|
|
23
|
+
}
|
|
24
|
+
url;
|
|
25
|
+
protocol;
|
|
26
|
+
close(code, reason) {
|
|
27
|
+
this.#init.close(code, reason);
|
|
28
|
+
}
|
|
29
|
+
};
|
|
30
|
+
const createWSMessageEvent = (source) => {
|
|
31
|
+
return new MessageEvent("message", { data: source });
|
|
32
|
+
};
|
|
33
|
+
/**
|
|
34
|
+
* Create a WebSocket adapter/helper
|
|
35
|
+
*/
|
|
36
|
+
const defineWebSocketHelper = (handler) => {
|
|
37
|
+
return ((...args) => {
|
|
38
|
+
if (typeof args[0] === "function") {
|
|
39
|
+
const [createEvents, options] = args;
|
|
40
|
+
return async function upgradeWebSocket(c, next) {
|
|
41
|
+
const result = await handler(c, await createEvents(c), options);
|
|
42
|
+
if (result) return result;
|
|
43
|
+
await next();
|
|
44
|
+
};
|
|
45
|
+
} else {
|
|
46
|
+
const [c, events, options] = args;
|
|
47
|
+
return (async () => {
|
|
48
|
+
const upgraded = await handler(c, events, options);
|
|
49
|
+
if (!upgraded) throw new Error("Failed to upgrade WebSocket");
|
|
50
|
+
return upgraded;
|
|
51
|
+
})();
|
|
52
|
+
}
|
|
53
|
+
});
|
|
54
|
+
};
|
|
55
|
+
//#endregion
|
|
56
|
+
export { createWSMessageEvent as n, defineWebSocketHelper as r, WSContext as t };
|
package/lib/ws.js
CHANGED
|
@@ -1,56 +1,2 @@
|
|
|
1
|
-
|
|
2
|
-
/**
|
|
3
|
-
* A context for controlling WebSockets
|
|
4
|
-
*/
|
|
5
|
-
var WSContext = class {
|
|
6
|
-
#init;
|
|
7
|
-
constructor(init) {
|
|
8
|
-
this.#init = init;
|
|
9
|
-
this.raw = init.raw;
|
|
10
|
-
this.url = init.url ? new URL(init.url) : null;
|
|
11
|
-
this.protocol = init.protocol ?? null;
|
|
12
|
-
}
|
|
13
|
-
send(source, options) {
|
|
14
|
-
this.#init.send(source, options ?? {});
|
|
15
|
-
}
|
|
16
|
-
raw;
|
|
17
|
-
binaryType = "arraybuffer";
|
|
18
|
-
get readyState() {
|
|
19
|
-
return this.#init.readyState;
|
|
20
|
-
}
|
|
21
|
-
get bufferedAmount() {
|
|
22
|
-
return this.#init.bufferedAmount ?? 0;
|
|
23
|
-
}
|
|
24
|
-
url;
|
|
25
|
-
protocol;
|
|
26
|
-
close(code, reason) {
|
|
27
|
-
this.#init.close(code, reason);
|
|
28
|
-
}
|
|
29
|
-
};
|
|
30
|
-
const createWSMessageEvent = (source) => {
|
|
31
|
-
return new MessageEvent("message", { data: source });
|
|
32
|
-
};
|
|
33
|
-
/**
|
|
34
|
-
* Create a WebSocket adapter/helper
|
|
35
|
-
*/
|
|
36
|
-
const defineWebSocketHelper = (handler) => {
|
|
37
|
-
return ((...args) => {
|
|
38
|
-
if (typeof args[0] === "function") {
|
|
39
|
-
const [createEvents, options] = args;
|
|
40
|
-
return async function upgradeWebSocket(c, next) {
|
|
41
|
-
const result = await handler(c, await createEvents(c), options);
|
|
42
|
-
if (result) return result;
|
|
43
|
-
await next();
|
|
44
|
-
};
|
|
45
|
-
} else {
|
|
46
|
-
const [c, events, options] = args;
|
|
47
|
-
return (async () => {
|
|
48
|
-
const upgraded = await handler(c, events, options);
|
|
49
|
-
if (!upgraded) throw new Error("Failed to upgrade WebSocket");
|
|
50
|
-
return upgraded;
|
|
51
|
-
})();
|
|
52
|
-
}
|
|
53
|
-
});
|
|
54
|
-
};
|
|
55
|
-
//#endregion
|
|
1
|
+
import { n as createWSMessageEvent, r as defineWebSocketHelper, t as WSContext } from "./websocket-DsyBIKVA.js";
|
|
56
2
|
export { WSContext, createWSMessageEvent, defineWebSocketHelper };
|
package/package.json
CHANGED
|
@@ -15,8 +15,8 @@ import { dirname, join, resolve } from 'node:path'
|
|
|
15
15
|
import { fileURLToPath } from 'node:url'
|
|
16
16
|
import { readDoorFile, STATE_DIR, DOOR_FILE } from '../host/door-file.ts'
|
|
17
17
|
|
|
18
|
-
const ACT_OPS = new Set(['click', 'type', 'select', 'press', 'scroll', 'wait'])
|
|
19
|
-
const VALUED = new Set(['--dir', '--ref', '--text', '--option', '--key', '--direction', '--amount', '--cursor', '--max-nodes', '--role', '--name', '--timeout-ms', '--tool', '--args'])
|
|
18
|
+
const ACT_OPS = new Set(['click', 'type', 'select', 'press', 'scroll', 'wait', 'reload', 'navigate'])
|
|
19
|
+
const VALUED = new Set(['--dir', '--ref', '--text', '--option', '--key', '--direction', '--amount', '--cursor', '--max-nodes', '--role', '--name', '--timeout-ms', '--tool', '--args', '--path'])
|
|
20
20
|
const FLAGS = new Set(['--json', '--submit', '--no-clear', '--gone', '--help'])
|
|
21
21
|
|
|
22
22
|
export class UsageError extends Error {}
|
|
@@ -36,6 +36,8 @@ const USAGE = `usage: acryl <verb> [options] [--json] [--dir <app folder>]
|
|
|
36
36
|
act select --ref <r> --option <o>
|
|
37
37
|
act press --key <key> [--ref <r>]
|
|
38
38
|
act scroll --direction up|down|left|right [--ref <r>] [--amount <px>]
|
|
39
|
+
act reload
|
|
40
|
+
act navigate --path <path of the app, e.g. /demo>
|
|
39
41
|
act wait (--text <t> | --role <role> [--name <n>]) [--gone] [--timeout-ms <ms>]
|
|
40
42
|
plugin list | plugin add <folder> | plugin remove <name> | plugin reload
|
|
41
43
|
tool list | tool call --tool <name> [--args '<json>']
|
|
@@ -90,7 +92,7 @@ export function parseInvocation(argv: readonly string[]): Invocation {
|
|
|
90
92
|
return make('act', {
|
|
91
93
|
op: second,
|
|
92
94
|
...pick('--ref', 'ref'), ...pick('--text', 'text'), ...pick('--option', 'option'), ...pick('--key', 'key'),
|
|
93
|
-
...pick('--direction', 'direction'), ...pick('--role', 'role'), ...pick('--name', 'name'),
|
|
95
|
+
...pick('--direction', 'direction'), ...pick('--role', 'role'), ...pick('--name', 'name'), ...pick('--path', 'path'),
|
|
94
96
|
...(amount === undefined ? {} : { amount }),
|
|
95
97
|
...(timeoutMs === undefined ? {} : { timeoutMs }),
|
|
96
98
|
...(set.has('--submit') ? { submit: true } : {}),
|
|
@@ -45,6 +45,8 @@ export class UiChannel {
|
|
|
45
45
|
private nextId = 1
|
|
46
46
|
private clock = 0
|
|
47
47
|
private closed = false
|
|
48
|
+
/** Callers waiting for a page to (re)connect: each is told once, by the next stream that attaches. */
|
|
49
|
+
private readonly attachWaiters = new Set<() => void>()
|
|
48
50
|
|
|
49
51
|
/**
|
|
50
52
|
* Register a page's stream. A stream already open for the same window is closed first, and its calls settle as `disconnected`.
|
|
@@ -55,6 +57,7 @@ export class UiChannel {
|
|
|
55
57
|
if (previous !== undefined) this.drop(previous, true)
|
|
56
58
|
const connection: Connection = { stream, windowId, focused, lastActive: ++this.clock }
|
|
57
59
|
this.connections.set(windowId, connection)
|
|
60
|
+
for (const waiter of [...this.attachWaiters]) waiter()
|
|
58
61
|
return () => { if (this.connections.get(windowId) === connection) this.drop(connection, false) }
|
|
59
62
|
}
|
|
60
63
|
|
|
@@ -109,6 +112,39 @@ export class UiChannel {
|
|
|
109
112
|
})
|
|
110
113
|
}
|
|
111
114
|
|
|
115
|
+
/**
|
|
116
|
+
* A promise for the next time a page connects (a page that reloaded or moved opens a new stream). Ask for it BEFORE the call that makes the page leave, or a fast page is missed.
|
|
117
|
+
* @returns whether a page connected within `timeoutMs`.
|
|
118
|
+
*/
|
|
119
|
+
expectAttach(timeoutMs: number, count = 1): Promise<boolean> {
|
|
120
|
+
return new Promise((resolve) => {
|
|
121
|
+
let left = count
|
|
122
|
+
const done = (value: boolean): void => { clearTimeout(timer); this.attachWaiters.delete(waiter); resolve(value) }
|
|
123
|
+
const waiter = (): void => { left -= 1; if (left <= 0) done(true) }
|
|
124
|
+
const timer = setTimeout(() => { done(false) }, timeoutMs)
|
|
125
|
+
this.attachWaiters.add(waiter)
|
|
126
|
+
})
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Ask every connected page to reload (the app just changed under them) and wait, up to `timeoutMs`, for them to come back. Best effort: a page that does not come back
|
|
131
|
+
* (it was closed, or it moved to a page without the door) is not an error here.
|
|
132
|
+
* @returns how many pages were asked.
|
|
133
|
+
*/
|
|
134
|
+
async reloadAll(timeoutMs = 8000): Promise<number> {
|
|
135
|
+
const targets = this.byRecency()
|
|
136
|
+
if (this.closed || targets.length === 0) return 0
|
|
137
|
+
const back = this.expectAttach(timeoutMs, targets.length)
|
|
138
|
+
for (const target of targets) {
|
|
139
|
+
const id = this.nextId
|
|
140
|
+
this.nextId += 1
|
|
141
|
+
// No call is waiting for the answer: the page answers by POST and the Host ignores an id it does not know.
|
|
142
|
+
try { target.stream.send(JSON.stringify({ t: 'call', id, request: { op: 'reload' } } satisfies ChannelToPage)) } catch { /* that page is gone */ }
|
|
143
|
+
}
|
|
144
|
+
await back
|
|
145
|
+
return targets.length
|
|
146
|
+
}
|
|
147
|
+
|
|
112
148
|
/** Close every page's stream and settle every pending call as `unloaded`. */
|
|
113
149
|
close(): void {
|
|
114
150
|
this.closed = true
|
|
@@ -20,6 +20,8 @@ export function registerChatTools(tools: ToolsPort, door: Door): () => void {
|
|
|
20
20
|
for (const verb of VERBS) {
|
|
21
21
|
removers.push(tools.register(chatToolName(verb), {
|
|
22
22
|
description: VERB_SPECS[verb].description,
|
|
23
|
+
// The model is told which fields the verb takes (an `act` needs its `op`), the same schema MCP lists.
|
|
24
|
+
parameters: VERB_SPECS[verb].arguments as unknown as Readonly<Record<string, unknown>>,
|
|
23
25
|
run: async (input: unknown) => {
|
|
24
26
|
try {
|
|
25
27
|
return (await door.run(verb, input ?? {}, running.signal)).text
|
|
@@ -35,7 +35,7 @@ export interface ExtensionsPort {
|
|
|
35
35
|
/** The slice of acrylblends' `tools` registry (the `agent` plugin's) the tool verbs use, and where the built-in chat finds the verbs. */
|
|
36
36
|
export interface ToolsPort {
|
|
37
37
|
/** @returns the function that removes the tool. @throws when the name is taken. */
|
|
38
|
-
register(name: string, tool: { readonly description: string; run(input: unknown): unknown }): () => void
|
|
38
|
+
register(name: string, tool: { readonly description: string; readonly parameters?: Readonly<Record<string, unknown>>; run(input: unknown): unknown }): () => void
|
|
39
39
|
list(): ReadonlyArray<{ readonly name: string; readonly description: string }>
|
|
40
40
|
call(name: string, input: unknown): Promise<unknown>
|
|
41
41
|
}
|
|
@@ -50,6 +50,10 @@ export interface DoorDeps {
|
|
|
50
50
|
readonly tools: () => ToolsPort | undefined
|
|
51
51
|
/** The app tools the door offers; anything else is refused before it reaches the registry. */
|
|
52
52
|
readonly expose: readonly string[]
|
|
53
|
+
/** Reload the open pages after a plugin was added, removed or reloaded, so the page an agent looks at next is the new one. Default: true. */
|
|
54
|
+
readonly reloadOnChange?: boolean
|
|
55
|
+
/** How long a reload or a move may take before the new page has connected again (tests shorten it). Default 15 s. */
|
|
56
|
+
readonly pageBackMs?: number
|
|
53
57
|
}
|
|
54
58
|
|
|
55
59
|
/** What one verb answered: the value for `--json` and programs, the text for an agent reading a terminal. */
|
|
@@ -61,6 +65,9 @@ export interface VerbAnswer {
|
|
|
61
65
|
/** The files an agent should read first, when the app has them. */
|
|
62
66
|
const SELF_KNOWLEDGE = ['AGENTS.md', 'CLAUDE.md', '.agents/skills', '.claude/skills', 'docs', 'plugins']
|
|
63
67
|
|
|
68
|
+
/** How long a reload or a move may take before the new page has connected to the door again. */
|
|
69
|
+
const PAGE_BACK_MS = 15_000
|
|
70
|
+
|
|
64
71
|
export class Door {
|
|
65
72
|
private readonly deps: DoorDeps
|
|
66
73
|
|
|
@@ -81,7 +88,7 @@ export class Door {
|
|
|
81
88
|
const result = await this.act(input, signal)
|
|
82
89
|
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` }
|
|
83
90
|
}
|
|
84
|
-
case 'plugin-list': return this.plugin(extensions => extensions.list())
|
|
91
|
+
case 'plugin-list': return this.plugin(extensions => extensions.list(), false)
|
|
85
92
|
case 'plugin-add': return this.plugin(extensions => extensions.install(this.insideApp(input.folder)))
|
|
86
93
|
case 'plugin-remove': {
|
|
87
94
|
const name = input.name
|
|
@@ -127,9 +134,38 @@ export class Door {
|
|
|
127
134
|
return this.callPage('look', request, signal) as Promise<UiSnapshot>
|
|
128
135
|
}
|
|
129
136
|
|
|
130
|
-
act(input: Record<string, unknown>, signal: AbortSignal): Promise<UiActionResult> {
|
|
137
|
+
async act(input: Record<string, unknown>, signal: AbortSignal): Promise<UiActionResult> {
|
|
131
138
|
if (input.op === 'snapshot') throw new DoorError('invalid', 'use look to see the page')
|
|
132
|
-
|
|
139
|
+
const request = this.parse(input)
|
|
140
|
+
if (request.op !== 'reload' && request.op !== 'navigate') return await this.callPage(`act ${String(input.op)}`, request, signal) as UiActionResult
|
|
141
|
+
// The page is replaced: wait for the new one to connect, so the next look sees it. The page may drop before it has answered; that is the reload working.
|
|
142
|
+
const back = this.deps.channel.expectAttach(this.deps.pageBackMs ?? PAGE_BACK_MS)
|
|
143
|
+
let answer: UiActionResult
|
|
144
|
+
try {
|
|
145
|
+
answer = await this.callPage(`act ${request.op}`, request, signal) as UiActionResult
|
|
146
|
+
} catch (cause) {
|
|
147
|
+
if (!(cause instanceof UiControlError && cause.code === 'disconnected')) { void back; throw cause }
|
|
148
|
+
answer = { ok: true, detail: request.op === 'reload' ? 'reloading' : `going to ${request.path}` }
|
|
149
|
+
}
|
|
150
|
+
if (!await back) throw new UiControlError('timeout', request.op === 'reload' ? 'the page did not come back after the reload' : `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?`)
|
|
151
|
+
const landed = request.op === 'navigate' ? await this.statusAt(request.path) : undefined
|
|
152
|
+
return { ...answer, detail: `${answer.detail ?? 'done'}${landed === undefined ? '' : `; ${landed}`}; the page is back: look again` }
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* What the app answers at a path the page was sent to, so an agent that mistyped one is told ("404 at /nope") and not left to find out from a page it did not expect.
|
|
157
|
+
* A second GET of the same path (a page is read-only); nothing when the app has no address or does not answer in time.
|
|
158
|
+
*/
|
|
159
|
+
private async statusAt(path: string): Promise<string | undefined> {
|
|
160
|
+
const base = this.deps.app.url
|
|
161
|
+
if (base === undefined) return undefined
|
|
162
|
+
try {
|
|
163
|
+
const response = await fetch(new URL(path, base), { headers: { accept: 'text/html' }, signal: AbortSignal.timeout(3000) })
|
|
164
|
+
await response.body?.cancel()
|
|
165
|
+
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`
|
|
166
|
+
} catch {
|
|
167
|
+
return undefined
|
|
168
|
+
}
|
|
133
169
|
}
|
|
134
170
|
|
|
135
171
|
private parse(input: Record<string, unknown>): UiRequest {
|
|
@@ -156,10 +192,13 @@ export class Door {
|
|
|
156
192
|
}
|
|
157
193
|
}
|
|
158
194
|
|
|
159
|
-
private async plugin(run: (extensions: ExtensionsPort) => unknown): Promise<VerbAnswer> {
|
|
195
|
+
private async plugin(run: (extensions: ExtensionsPort) => unknown, mutates = true): Promise<VerbAnswer> {
|
|
160
196
|
const extensions = this.deps.extensions()
|
|
161
197
|
if (extensions === undefined) throw new DoorError('unavailable', 'this app has no extensions plugin, so plugins cannot be installed while it runs')
|
|
162
198
|
const value = await run(extensions)
|
|
199
|
+
// 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.
|
|
200
|
+
const unchanged = typeof value === 'object' && value !== null && (value as { action?: unknown }).action === 'unchanged'
|
|
201
|
+
if (mutates && !unchanged && this.deps.reloadOnChange !== false) await this.deps.channel.reloadAll()
|
|
163
202
|
return { value, text: JSON.stringify(value, null, 2) }
|
|
164
203
|
}
|
|
165
204
|
|
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
* app is running in the folder (exit 3). It carries no secret; the client reads `door.json` at each call.
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
|
+
import { spawnSync } from 'node:child_process'
|
|
10
11
|
import { chmodSync, mkdirSync, renameSync, writeFileSync } from 'node:fs'
|
|
11
12
|
import { join } from 'node:path'
|
|
12
13
|
import { ensureStateDir } from './door-file.ts'
|
|
@@ -25,6 +26,28 @@ export interface ShimTarget {
|
|
|
25
26
|
readonly electron: boolean
|
|
26
27
|
}
|
|
27
28
|
|
|
29
|
+
/** The oldest Node the client runs on (the framework's own floor). */
|
|
30
|
+
const MIN_NODE = [22, 19]
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* A real Node for the command, when the app runs inside Electron: Electron can run the client as Node, but on macOS every such call prints a code-signing complaint on stderr,
|
|
34
|
+
* which lands in an agent's transcript. A `node` on the PATH that is new enough is used instead; without one, Electron runs the client. `find` is replaceable for tests.
|
|
35
|
+
*/
|
|
36
|
+
export function realNode(find: () => { path: string; version: string } | undefined = findNodeOnPath): string | undefined {
|
|
37
|
+
const found = find()
|
|
38
|
+
if (found === undefined) return undefined
|
|
39
|
+
const [major = 0, minor = 0] = found.version.replace(/^v/u, '').split('.').map(Number)
|
|
40
|
+
return major > MIN_NODE[0]! || (major === MIN_NODE[0] && minor >= MIN_NODE[1]!) ? found.path : undefined
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function findNodeOnPath(): { path: string; version: string } | undefined {
|
|
44
|
+
const located = spawnSync(process.platform === 'win32' ? 'where' : 'which', ['node'], { encoding: 'utf8', env: { ...process.env, ELECTRON_RUN_AS_NODE: undefined } })
|
|
45
|
+
const path = located.status === 0 ? located.stdout.split(/\r?\n/u).find((line) => line.trim() !== '')?.trim() : undefined
|
|
46
|
+
if (path === undefined) return undefined
|
|
47
|
+
const asked = spawnSync(path, ['--version'], { encoding: 'utf8' })
|
|
48
|
+
return asked.status === 0 ? { path, version: asked.stdout.trim() } : undefined
|
|
49
|
+
}
|
|
50
|
+
|
|
28
51
|
/** A word for `sh`: single-quoted, with each `'` closed, escaped and reopened. */
|
|
29
52
|
function shWord(value: string): string {
|
|
30
53
|
return `'${value.replaceAll('\'', '\'\\\'\'')}'`
|
|
@@ -47,12 +47,13 @@ export const VERB_SPECS: Readonly<Record<Verb, VerbSpec>> = {
|
|
|
47
47
|
},
|
|
48
48
|
},
|
|
49
49
|
'act': {
|
|
50
|
-
usage: 'act click|type|select|press|scroll|wait ...: do one thing on the page (refs from the latest look)',
|
|
51
|
-
description: 'Do one thing on the page: click, type, select, press a key, scroll,
|
|
50
|
+
usage: 'act click|type|select|press|scroll|wait|reload|navigate ...: do one thing on the page (refs from the latest look)',
|
|
51
|
+
description: 'Do one thing on the page: click, type, select, press a key, scroll, wait for text or a control, reload the page, or navigate to another path of the app. Look again before the next act.',
|
|
52
52
|
arguments: {
|
|
53
53
|
type: 'object',
|
|
54
54
|
properties: {
|
|
55
|
-
op: { type: 'string', enum: ['click', 'type', 'select', 'press', 'scroll', 'wait'] },
|
|
55
|
+
op: { type: 'string', enum: ['click', 'type', 'select', 'press', 'scroll', 'wait', 'reload', 'navigate'] },
|
|
56
|
+
path: { type: 'string', description: 'navigate: a path of this app, e.g. /demo (the page must load the door script to be controlled afterwards)' },
|
|
56
57
|
ref,
|
|
57
58
|
text: { type: 'string', description: 'type: what to type; wait: the text to wait for' },
|
|
58
59
|
submit: { type: 'boolean', description: 'type: press Enter after' },
|