acrylblends 1.0.4 → 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.
- package/docs/framework/agents/index.md +7 -5
- 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-D5z9uwOP.d.ts → audit-GcLtvNLV.d.ts} +4 -2
- 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 +99 -5
- package/lib/cordis/door.js +562 -20
- 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.js +2 -2
- 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 +3 -1
- package/lib/door/page.js +2 -2
- package/lib/door/protocol.d.ts +1 -1
- package/lib/door/protocol.js +1 -212
- 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-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-C37gPePD.d.ts → protocol-CR31M7K_.d.ts} +2 -0
- 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-BLz8nlSc.js → snapshot-text-CtQm3n8n.js} +23 -14
- package/lib/{src-dy3Fk9tA.js → src-BTBm4OPD.js} +6 -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 +36 -5
- package/plugins/acryl-door/src/host/channel.ts +14 -6
- package/plugins/acryl-door/src/host/chat-tools.ts +2 -0
- package/plugins/acryl-door/src/host/door.ts +97 -12
- package/plugins/acryl-door/src/host/page-routes.ts +3 -1
- package/plugins/acryl-door/src/host/shell.ts +147 -0
- package/plugins/acryl-door/src/host/shim.ts +39 -3
- package/plugins/acryl-door/src/host/verbs.ts +39 -2
- package/plugins/acryl-door/src/index.ts +22 -3
- package/plugins/acryl-door/src/page/channel.ts +3 -1
- package/plugins/acryl-door/src/page/index.ts +2 -1
- package/plugins/acryl-door/src/page/protocol.ts +2 -0
- package/plugins/acryl-door/src/shell/protocol.ts +130 -0
- package/plugins/acryl-node-server/src/register.ts +29 -2
- 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 +4 -1
- package/plugins/agent-panel/headless.ts +29 -8
- package/plugins/agent-panel/index.ts +22 -10
- package/plugins/agent-panel/launch.ts +67 -0
- package/plugins/desktop/attach.ts +72 -0
- package/plugins/desktop/door-shell.ts +379 -0
- package/plugins/desktop/main.ts +82 -15
- package/plugins/extensions/index.ts +3 -4
- package/plugins/extensions/stage.ts +33 -5
- package/plugins/openai/index.ts +1 -1
- package/templates/agent/AGENTS.md +4 -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/plugins/acryl-router/src/connection.d.ts +0 -25
- package/plugins/acryl-router/src/core/app-base.d.ts +0 -215
- package/plugins/acryl-router/src/core/app.d.ts +0 -19
- package/plugins/acryl-router/src/core/client/client.d.ts +0 -4
- package/plugins/acryl-router/src/core/client/fetch-result-please.d.ts +0 -35
- package/plugins/acryl-router/src/core/client/index.d.ts +0 -7
- package/plugins/acryl-router/src/core/client/types.d.ts +0 -229
- package/plugins/acryl-router/src/core/client/utils.d.ts +0 -18
- package/plugins/acryl-router/src/core/compose.d.ts +0 -14
- package/plugins/acryl-router/src/core/context.d.ts +0 -443
- package/plugins/acryl-router/src/core/helper/accepts/accepts.d.ts +0 -30
- package/plugins/acryl-router/src/core/helper/accepts/index.d.ts +0 -5
- package/plugins/acryl-router/src/core/helper/conninfo/index.d.ts +0 -5
- package/plugins/acryl-router/src/core/helper/conninfo/types.d.ts +0 -36
- package/plugins/acryl-router/src/core/helper/cookie/index.d.ts +0 -25
- package/plugins/acryl-router/src/core/helper/css/common.d.ts +0 -59
- package/plugins/acryl-router/src/core/helper/css/index.d.ts +0 -80
- package/plugins/acryl-router/src/core/helper/dev/index.d.ts +0 -20
- package/plugins/acryl-router/src/core/helper/factory/index.d.ts +0 -82
- package/plugins/acryl-router/src/core/helper/html/index.d.ts +0 -8
- package/plugins/acryl-router/src/core/helper/proxy/index.d.ts +0 -73
- package/plugins/acryl-router/src/core/helper/route/index.d.ts +0 -75
- package/plugins/acryl-router/src/core/helper/ssg/index.d.ts +0 -7
- package/plugins/acryl-router/src/core/helper/ssg/middleware.d.ts +0 -44
- package/plugins/acryl-router/src/core/helper/ssg/plugins.d.ts +0 -27
- package/plugins/acryl-router/src/core/helper/ssg/ssg.d.ts +0 -91
- package/plugins/acryl-router/src/core/helper/ssg/utils.d.ts +0 -16
- package/plugins/acryl-router/src/core/helper/streaming/index.d.ts +0 -8
- package/plugins/acryl-router/src/core/helper/streaming/sse.d.ts +0 -13
- package/plugins/acryl-router/src/core/helper/streaming/stream.d.ts +0 -3
- package/plugins/acryl-router/src/core/helper/streaming/text.d.ts +0 -3
- package/plugins/acryl-router/src/core/helper/streaming/utils.d.ts +0 -1
- package/plugins/acryl-router/src/core/helper/testing/index.d.ts +0 -13
- package/plugins/acryl-router/src/core/helper/websocket/index.d.ts +0 -70
- package/plugins/acryl-router/src/core/http-exception.d.ts +0 -58
- package/plugins/acryl-router/src/core/index.d.ts +0 -37
- package/plugins/acryl-router/src/core/jsx/base.d.ts +0 -61
- package/plugins/acryl-router/src/core/jsx/children.d.ts +0 -9
- package/plugins/acryl-router/src/core/jsx/components.d.ts +0 -15
- package/plugins/acryl-router/src/core/jsx/constants.d.ts +0 -6
- package/plugins/acryl-router/src/core/jsx/context.d.ts +0 -51
- package/plugins/acryl-router/src/core/jsx/dom/components.d.ts +0 -10
- package/plugins/acryl-router/src/core/jsx/dom/context.d.ts +0 -3
- package/plugins/acryl-router/src/core/jsx/dom/css.d.ts +0 -83
- package/plugins/acryl-router/src/core/jsx/dom/hooks/index.d.ts +0 -39
- package/plugins/acryl-router/src/core/jsx/dom/intrinsic-element/components.d.ts +0 -16
- package/plugins/acryl-router/src/core/jsx/dom/jsx-dev-runtime.d.ts +0 -8
- package/plugins/acryl-router/src/core/jsx/dom/jsx-runtime.d.ts +0 -7
- package/plugins/acryl-router/src/core/jsx/dom/render.d.ts +0 -73
- package/plugins/acryl-router/src/core/jsx/dom/utils.d.ts +0 -1
- package/plugins/acryl-router/src/core/jsx/hooks/index.d.ts +0 -47
- package/plugins/acryl-router/src/core/jsx/index.d.ts +0 -76
- package/plugins/acryl-router/src/core/jsx/intrinsic-element/common.d.ts +0 -6
- package/plugins/acryl-router/src/core/jsx/intrinsic-element/components.d.ts +0 -13
- package/plugins/acryl-router/src/core/jsx/intrinsic-elements.d.ts +0 -735
- package/plugins/acryl-router/src/core/jsx/streaming.d.ts +0 -36
- package/plugins/acryl-router/src/core/jsx/types.d.ts +0 -36
- package/plugins/acryl-router/src/core/jsx/utils.d.ts +0 -4
- package/plugins/acryl-router/src/core/request/constants.d.ts +0 -1
- package/plugins/acryl-router/src/core/request.d.ts +0 -306
- package/plugins/acryl-router/src/core/router/reg-exp-router/index.d.ts +0 -6
- package/plugins/acryl-router/src/core/router/reg-exp-router/matcher.d.ts +0 -7
- package/plugins/acryl-router/src/core/router/reg-exp-router/node.d.ts +0 -13
- package/plugins/acryl-router/src/core/router/reg-exp-router/prepared-router.d.ts +0 -17
- package/plugins/acryl-router/src/core/router/reg-exp-router/router.d.ts +0 -11
- package/plugins/acryl-router/src/core/router/reg-exp-router/trie.d.ts +0 -8
- package/plugins/acryl-router/src/core/router/smart-router/index.d.ts +0 -5
- package/plugins/acryl-router/src/core/router/smart-router/router.d.ts +0 -11
- package/plugins/acryl-router/src/core/router/trie-router/index.d.ts +0 -5
- package/plugins/acryl-router/src/core/router/trie-router/node.d.ts +0 -6
- package/plugins/acryl-router/src/core/router/trie-router/router.d.ts +0 -7
- package/plugins/acryl-router/src/core/router/utils.d.ts +0 -1
- package/plugins/acryl-router/src/core/router.d.ts +0 -97
- package/plugins/acryl-router/src/core/types.d.ts +0 -573
- package/plugins/acryl-router/src/core/utils/accept.d.ts +0 -6
- package/plugins/acryl-router/src/core/utils/body.d.ts +0 -79
- package/plugins/acryl-router/src/core/utils/buffer.d.ts +0 -17
- package/plugins/acryl-router/src/core/utils/color.d.ts +0 -21
- package/plugins/acryl-router/src/core/utils/concurrent.d.ts +0 -11
- package/plugins/acryl-router/src/core/utils/constants.d.ts +0 -4
- package/plugins/acryl-router/src/core/utils/cookie.d.ts +0 -41
- package/plugins/acryl-router/src/core/utils/crypto.d.ts +0 -15
- package/plugins/acryl-router/src/core/utils/handler.d.ts +0 -6
- package/plugins/acryl-router/src/core/utils/headers.d.ts +0 -8
- package/plugins/acryl-router/src/core/utils/html.d.ts +0 -44
- package/plugins/acryl-router/src/core/utils/http-status.d.ts +0 -32
- package/plugins/acryl-router/src/core/utils/mime.d.ts +0 -70
- package/plugins/acryl-router/src/core/utils/stream.d.ts +0 -31
- package/plugins/acryl-router/src/core/utils/types.d.ts +0 -128
- package/plugins/acryl-router/src/core/utils/url.d.ts +0 -44
- package/plugins/acryl-router/src/core/validator/index.d.ts +0 -7
- package/plugins/acryl-router/src/core/validator/utils.d.ts +0 -29
- package/plugins/acryl-router/src/core/validator/validator.d.ts +0 -16
- package/plugins/acryl-router/src/helpers/index.d.ts +0 -17
- package/plugins/acryl-router/src/helpers/schema-validator.d.ts +0 -54
|
@@ -6,7 +6,7 @@
|
|
|
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
|
-
export const VERBS = ['brief', 'look', 'act', 'plugin-list', 'plugin-add', 'plugin-remove', 'plugin-reload', 'tool-list', 'tool-call'] as const
|
|
9
|
+
export const VERBS = ['brief', 'look', 'act', 'plugin-list', 'plugin-add', '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
10
|
export type Verb = (typeof VERBS)[number]
|
|
11
11
|
|
|
12
12
|
/** The JSON Schema of a verb's arguments, as MCP lists it. */
|
|
@@ -35,13 +35,14 @@ export const VERB_SPECS: Readonly<Record<Verb, VerbSpec>> = {
|
|
|
35
35
|
arguments: none,
|
|
36
36
|
},
|
|
37
37
|
'look': {
|
|
38
|
-
usage: 'look [--cursor <n>] [--max-nodes <n>]: the page as text, with refs',
|
|
38
|
+
usage: 'look [--window <n>] [--cursor <n>] [--max-nodes <n>]: the page as text, with refs',
|
|
39
39
|
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).',
|
|
40
40
|
arguments: {
|
|
41
41
|
type: 'object',
|
|
42
42
|
properties: {
|
|
43
43
|
cursor: { type: 'integer', minimum: 0, description: 'where the previous part ended' },
|
|
44
44
|
maxNodes: { type: 'integer', minimum: 1, maximum: MAX_SNAPSHOT_NODES },
|
|
45
|
+
window: { type: 'integer', minimum: 1, description: 'a desktop window (the id window-list prints); default: the page used last' },
|
|
45
46
|
},
|
|
46
47
|
additionalProperties: false,
|
|
47
48
|
},
|
|
@@ -66,6 +67,7 @@ export const VERB_SPECS: Readonly<Record<Verb, VerbSpec>> = {
|
|
|
66
67
|
name: { type: 'string', description: 'wait: its name' },
|
|
67
68
|
gone: { type: 'boolean', description: 'wait: until it is gone instead' },
|
|
68
69
|
timeoutMs: { type: 'integer', minimum: 0, maximum: MAX_WAIT_MS },
|
|
70
|
+
window: { type: 'integer', minimum: 1, description: 'a desktop window (the id window-list prints); default: the page used last' },
|
|
69
71
|
},
|
|
70
72
|
required: ['op'],
|
|
71
73
|
additionalProperties: false,
|
|
@@ -101,6 +103,41 @@ export const VERB_SPECS: Readonly<Record<Verb, VerbSpec>> = {
|
|
|
101
103
|
description: 'Run one of the app tools that tool-list names.',
|
|
102
104
|
arguments: { type: 'object', properties: { tool: { type: 'string' }, args: { description: 'the tool\'s input' } }, required: ['tool'], additionalProperties: false },
|
|
103
105
|
},
|
|
106
|
+
'window-list': {
|
|
107
|
+
usage: 'window list: the windows of the desktop app',
|
|
108
|
+
description: 'The windows of the desktop app: id, title, path, size, and which one is focused. Needs the desktop app (npm run desktop).',
|
|
109
|
+
arguments: none,
|
|
110
|
+
},
|
|
111
|
+
'window-open': {
|
|
112
|
+
usage: 'window open [--path <path of the app>]: open a new window',
|
|
113
|
+
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.',
|
|
114
|
+
arguments: { type: 'object', properties: { path: { type: 'string', description: 'a path of this app, e.g. /settings' } }, additionalProperties: false },
|
|
115
|
+
},
|
|
116
|
+
'window-focus': {
|
|
117
|
+
usage: 'window focus --id <n>: bring a window to the front',
|
|
118
|
+
description: 'Bring a window of the desktop app to the front. Look and act without a window then drive it.',
|
|
119
|
+
arguments: { type: 'object', properties: { id: { type: 'integer', minimum: 1, description: 'the id window-list prints' } }, required: ['id'], additionalProperties: false },
|
|
120
|
+
},
|
|
121
|
+
'window-close': {
|
|
122
|
+
usage: 'window close --id <n> [--force]: close a window',
|
|
123
|
+
description: 'Close a window of the desktop app. The last window cannot be closed without force, because that ends the desktop app.',
|
|
124
|
+
arguments: { type: 'object', properties: { id: { type: 'integer', minimum: 1 }, force: { type: 'boolean', description: 'allow closing the last window, which ends the desktop app' } }, required: ['id'], additionalProperties: false },
|
|
125
|
+
},
|
|
126
|
+
'window-resize': {
|
|
127
|
+
usage: 'window resize --id <n> --width <px> --height <px>',
|
|
128
|
+
description: 'Resize a window of the desktop app.',
|
|
129
|
+
arguments: { type: 'object', properties: { id: { type: 'integer', minimum: 1 }, width: { type: 'integer', minimum: 200, maximum: 10_000 }, height: { type: 'integer', minimum: 200, maximum: 10_000 } }, required: ['id', 'width', 'height'], additionalProperties: false },
|
|
130
|
+
},
|
|
131
|
+
'menu-list': {
|
|
132
|
+
usage: 'menu list: the application menu',
|
|
133
|
+
description: 'The desktop app\'s application menu as paths such as File>New Note, with the disabled items marked.',
|
|
134
|
+
arguments: none,
|
|
135
|
+
},
|
|
136
|
+
'menu-click': {
|
|
137
|
+
usage: 'menu click --path "File>New Note" [--force]: press a menu item',
|
|
138
|
+
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.',
|
|
139
|
+
arguments: { type: 'object', properties: { path: { type: 'string', description: 'e.g. File>New Note' }, force: { type: 'boolean', description: 'allow an item that ends the desktop app' } }, required: ['path'], additionalProperties: false },
|
|
140
|
+
},
|
|
104
141
|
}
|
|
105
142
|
|
|
106
143
|
/** A verb's name in the `tools` registry the built-in chat reads: prefixed, so it never meets an app tool's name. */
|
|
@@ -23,6 +23,7 @@ import { DOOR_PREFIX, privateApi } from './host/http.ts'
|
|
|
23
23
|
import { mcpRoutes } from './host/mcp-routes.ts'
|
|
24
24
|
import { operatorRoutes } from './host/operator-routes.ts'
|
|
25
25
|
import { pageChannelRoutes } from './host/page-routes.ts'
|
|
26
|
+
import { ShellChannel, shellRoutes } from './host/shell.ts'
|
|
26
27
|
import { realNode, writeShims, type ShimTarget } from './host/shim.ts'
|
|
27
28
|
|
|
28
29
|
/** Stable Cordis plugin name, equal to the package name and the Loader row id. */
|
|
@@ -94,6 +95,13 @@ function loopbackUrl(url: string): string {
|
|
|
94
95
|
return parsed.origin
|
|
95
96
|
}
|
|
96
97
|
|
|
98
|
+
const escapeHtml = (text: string): string => text.replace(/[&<>"']/gu, char => `&#${char.charCodeAt(0)};`)
|
|
99
|
+
|
|
100
|
+
/** The page of a path the app does not serve, development only: what is missing, a way back, and the door's script. */
|
|
101
|
+
export function notFoundPage(path: string, scriptTag: string): string {
|
|
102
|
+
return `<!doctype html><html lang="en"><head><meta charset="utf-8"><title>404 Not Found</title></head><body><main><h1>404 Not Found</h1><p>This app has no page at <code>${escapeHtml(path)}</code>.</p><p><a href="/">Back to the app</a></p></main>${scriptTag}</body></html>`
|
|
103
|
+
}
|
|
104
|
+
|
|
97
105
|
export function apply(ctx: Context, rawConfig: Config = {}): void {
|
|
98
106
|
const config = Config(rawConfig)
|
|
99
107
|
// Development only: nothing is started, mounted or written in production, or when switched off.
|
|
@@ -106,9 +114,11 @@ export function apply(ctx: Context, rawConfig: Config = {}): void {
|
|
|
106
114
|
}
|
|
107
115
|
const secret = newSecret()
|
|
108
116
|
const channel = new UiChannel()
|
|
117
|
+
const shell = new ShellChannel()
|
|
109
118
|
const audit = new AuditLog(auditPath(stateDir(app.root)))
|
|
110
119
|
const door = new Door({
|
|
111
120
|
channel,
|
|
121
|
+
shell,
|
|
112
122
|
audit,
|
|
113
123
|
app: { name: app.name, root: app.root, url: loopbackUrl(server.url) },
|
|
114
124
|
extensions: () => ctx.get('extensions') as unknown as ExtensionsPort | undefined,
|
|
@@ -117,6 +127,7 @@ export function apply(ctx: Context, rawConfig: Config = {}): void {
|
|
|
117
127
|
reloadOnChange: config.reloadOnChange !== false,
|
|
118
128
|
})
|
|
119
129
|
ctx.effect(() => () => { channel.close() }, 'acryl-door: page channel')
|
|
130
|
+
ctx.effect(() => () => { shell.close() }, 'acryl-door: desktop shell channel')
|
|
120
131
|
// One script tag for the page (`page.js`): it imports the module tree served below it.
|
|
121
132
|
const pageModule = new Acryl()
|
|
122
133
|
.get(`${DOOR_PREFIX}/page.js`, c => c.body(`import '${DOOR_PREFIX}/page/main.ts'\n`, 200, { 'content-type': 'text/javascript; charset=utf-8' }))
|
|
@@ -124,9 +135,15 @@ export function apply(ctx: Context, rawConfig: Config = {}): void {
|
|
|
124
135
|
// The page script on every HTML page the app answers, whenever its plugin was mounted (the pages of a plugin that loaded at boot, before this one, included): a page an agent writes
|
|
125
136
|
// can be looked at and driven without anyone adding the tag. A page that already carries it (the templates' own pages) is left as it is. Development only, like the rest of the door.
|
|
126
137
|
const tag = `<script type="module" src="${DOOR_PREFIX}/page.js"></script>`
|
|
127
|
-
const addScript = async (c: { res: Response }, next: () => Promise<void>): Promise<void> => {
|
|
138
|
+
const addScript = async (c: { res: Response; req: { path: string; header(name: string): string | undefined } }, next: () => Promise<void>): Promise<void> => {
|
|
128
139
|
await next()
|
|
129
|
-
if (!(c.res.headers.get('content-type') ?? '').includes('text/html'))
|
|
140
|
+
if (!(c.res.headers.get('content-type') ?? '').includes('text/html')) {
|
|
141
|
+
// A path the app does not serve, opened as a page: the browser would show a bare "404 Not Found" with no script, and the window would be left where nothing can look at it or
|
|
142
|
+
// act on it (an agent that mistypes a path would need a person to bring the window back). The page says what is missing, carries the script, and keeps the 404.
|
|
143
|
+
if (c.res.status === 404 && (c.req.header('accept') ?? '').includes('text/html') && !(c.res.headers.get('content-type') ?? '').includes('json'))
|
|
144
|
+
c.res = new Response(notFoundPage(c.req.path, tag), { status: 404, headers: { 'content-type': 'text/html; charset=utf-8' } })
|
|
145
|
+
return
|
|
146
|
+
}
|
|
130
147
|
const html = await c.res.text()
|
|
131
148
|
const headers = new Headers(c.res.headers)
|
|
132
149
|
headers.delete('content-length')
|
|
@@ -141,6 +158,7 @@ export function apply(ctx: Context, rawConfig: Config = {}): void {
|
|
|
141
158
|
DOOR_PREFIX,
|
|
142
159
|
pageModule,
|
|
143
160
|
pageChannelRoutes(channel, { prefix: DOOR_PREFIX }),
|
|
161
|
+
shellRoutes(DOOR_PREFIX, secret, shell),
|
|
144
162
|
operatorRoutes(DOOR_PREFIX, secret, door, reportError),
|
|
145
163
|
mcpRoutes(DOOR_PREFIX, secret, door, reportError),
|
|
146
164
|
))
|
|
@@ -151,7 +169,8 @@ export function apply(ctx: Context, rawConfig: Config = {}): void {
|
|
|
151
169
|
ctx.effect(function* () {
|
|
152
170
|
yield ctx.reflect.provide('door', door)
|
|
153
171
|
writeDoorFile(app.root, loopbackUrl(server.url), secret)
|
|
154
|
-
|
|
172
|
+
// A shim that cannot be replaced (Windows, held open by a running `acryl`) leaves the old one in place; it must not stop the app from starting.
|
|
173
|
+
try { writeShims(app.root, shimTarget()) } catch (cause) { reportError('write .acryl/bin', cause) }
|
|
155
174
|
yield () => { removeDoorFile(app.root, secret) }
|
|
156
175
|
}, 'acryl-door: the door service, .acryl/door.json and .acryl/bin/acryl')
|
|
157
176
|
}
|
|
@@ -26,6 +26,8 @@ const BACKOFF_MAX_MS = 10_000
|
|
|
26
26
|
|
|
27
27
|
export interface PageChannelOptions {
|
|
28
28
|
readonly windowId: string
|
|
29
|
+
/** The desktop window this page is in (`AcrylWindow/<n>` in the user agent, set by the Electron shell), so the Host can drive it by the number `window list` prints. */
|
|
30
|
+
readonly shellWindow?: number
|
|
29
31
|
readonly focused: () => boolean
|
|
30
32
|
/** Where the door's routes are, e.g. `/api/acryl-door`. */
|
|
31
33
|
readonly base: string
|
|
@@ -56,7 +58,7 @@ export class PageChannel {
|
|
|
56
58
|
connect(): void {
|
|
57
59
|
if (this.disposed || this.source !== undefined) return
|
|
58
60
|
const create = this.options.createSource ?? ((url: string): ChannelSource => new EventSource(url))
|
|
59
|
-
const query = `window=${encodeURIComponent(this.options.windowId)}&focused=${this.options.focused() ? '1' : '0'}`
|
|
61
|
+
const query = `window=${encodeURIComponent(this.options.windowId)}&focused=${this.options.focused() ? '1' : '0'}${this.options.shellWindow === undefined ? '' : `&shell=${String(this.options.shellWindow)}`}`
|
|
60
62
|
const source = create(`${this.options.base}/channel?${query}`)
|
|
61
63
|
this.source = source
|
|
62
64
|
this.lastHeard = this.now()
|
|
@@ -21,7 +21,8 @@ export function startDoor(document: Document, base: string = DOOR_BASE): () => v
|
|
|
21
21
|
const focused = (): boolean => document.visibilityState === 'visible' && document.hasFocus()
|
|
22
22
|
const detachInput = clock.attach(document)
|
|
23
23
|
const removeIndicator = mountIndicator(driver, document)
|
|
24
|
-
const
|
|
24
|
+
const shellWindow = Number(/\bAcrylWindow\/(\d{1,9})\b/u.exec(view.navigator?.userAgent ?? '')?.[1])
|
|
25
|
+
const channel = new PageChannel(driver, { windowId, focused, base, ...(Number.isSafeInteger(shellWindow) && shellWindow > 0 ? { shellWindow } : {}) })
|
|
25
26
|
const reportFocus = (): void => { channel.reportFocus(focused()) }
|
|
26
27
|
view.addEventListener('focus', reportFocus)
|
|
27
28
|
view.addEventListener('blur', reportFocus)
|
|
@@ -212,6 +212,8 @@ export interface AuditEntry {
|
|
|
212
212
|
readonly tool: string
|
|
213
213
|
/** The control touched, when the call had one. */
|
|
214
214
|
readonly target?: { readonly role: string; readonly name: string }
|
|
215
|
+
/** The desktop window the call was about (the number `window list` prints), when it had one. */
|
|
216
|
+
readonly window?: number
|
|
215
217
|
readonly outcome: 'ok' | 'refused' | 'failed'
|
|
216
218
|
/** The error code for a refusal or failure, or a short note for a success. */
|
|
217
219
|
readonly detail?: string
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What the door and the desktop shell say to each other. The shell is the Electron main process of `npm run desktop`; it reaches the door as a page does (an event
|
|
3
|
+
* stream down, a POST up) and answers calls about the app's windows and menu. This file is plain data and checks, so the shell side can follow it without importing the door.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/** The shell's operations, one per door verb (`window-list` is `acryl window list`). */
|
|
7
|
+
export const SHELL_OPS = ['window-list', 'window-open', 'window-focus', 'window-close', 'window-resize', 'menu-list', 'menu-click'] as const
|
|
8
|
+
export type ShellOp = (typeof SHELL_OPS)[number]
|
|
9
|
+
|
|
10
|
+
export type ShellRequest =
|
|
11
|
+
| { readonly op: 'window-list' }
|
|
12
|
+
| { readonly op: 'window-open'; readonly path?: string }
|
|
13
|
+
| { readonly op: 'window-focus'; readonly id: number }
|
|
14
|
+
/** `force`: the window may be the last, and closing the last one ends the desktop app (and with it the server it started). */
|
|
15
|
+
| { readonly op: 'window-close'; readonly id: number; readonly force?: boolean }
|
|
16
|
+
| { readonly op: 'window-resize'; readonly id: number; readonly width: number; readonly height: number }
|
|
17
|
+
| { readonly op: 'menu-list' }
|
|
18
|
+
/** `force`: the item may end the desktop app (Quit, or Close Window on the last window). */
|
|
19
|
+
| { readonly op: 'menu-click'; readonly path: string; readonly force?: boolean }
|
|
20
|
+
|
|
21
|
+
export interface ShellWindow {
|
|
22
|
+
/** Electron's own window id: the number `window list` prints and `--window` takes. */
|
|
23
|
+
readonly id: number
|
|
24
|
+
readonly title: string
|
|
25
|
+
/** The path (with query) the window shows, never the origin. */
|
|
26
|
+
readonly path: string
|
|
27
|
+
readonly focused: boolean
|
|
28
|
+
readonly width: number
|
|
29
|
+
readonly height: number
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export interface ShellMenuItem {
|
|
33
|
+
/** Labels from the top of the menu, joined with `>`: `File>New Note`. */
|
|
34
|
+
readonly path: string
|
|
35
|
+
readonly label: string
|
|
36
|
+
readonly enabled: boolean
|
|
37
|
+
/** True for an item that ends the desktop app (Quit, or Close Window): `menu click` presses it only with `force`. */
|
|
38
|
+
readonly ends?: boolean
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export type ShellResult =
|
|
42
|
+
| { readonly windows: readonly ShellWindow[] }
|
|
43
|
+
| { readonly items: readonly ShellMenuItem[] }
|
|
44
|
+
| { readonly ok: true; readonly detail: string; readonly id?: number }
|
|
45
|
+
|
|
46
|
+
/** Door to shell. */
|
|
47
|
+
export interface ShellCall {
|
|
48
|
+
readonly t: 'call'
|
|
49
|
+
readonly id: number
|
|
50
|
+
readonly request: ShellRequest
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** Shell to door. */
|
|
54
|
+
export type ShellAnswer =
|
|
55
|
+
| { readonly t: 'result'; readonly id: number; readonly value: ShellResult }
|
|
56
|
+
| { readonly t: 'error'; readonly id: number; readonly code: string; readonly message: string }
|
|
57
|
+
|
|
58
|
+
export type ShellErrorCode = 'invalid' | 'no-desktop' | 'no-such-window' | 'last-window' | 'no-such-menu-item' | 'disabled' | 'quits-app' | 'timeout' | 'disconnected' | 'unloaded' | 'aborted' | 'failed'
|
|
59
|
+
|
|
60
|
+
export class ShellError extends Error {
|
|
61
|
+
readonly code: ShellErrorCode
|
|
62
|
+
|
|
63
|
+
constructor(code: ShellErrorCode, message: string) {
|
|
64
|
+
super(message)
|
|
65
|
+
this.code = code
|
|
66
|
+
this.name = 'ShellError'
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** A path of the app, as `act navigate` takes one: one leading `/`, and no other site. */
|
|
71
|
+
const APP_PATH = /^\/(?![/\\])[^\s\\]*$/u
|
|
72
|
+
|
|
73
|
+
const isRecord = (value: unknown): value is Record<string, unknown> => typeof value === 'object' && value !== null && !Array.isArray(value)
|
|
74
|
+
|
|
75
|
+
function only(value: Record<string, unknown>, allowed: readonly string[]): void {
|
|
76
|
+
const extra = Object.keys(value).filter(key => key !== 'op' && !allowed.includes(key))
|
|
77
|
+
if (extra.length > 0) throw new ShellError('invalid', `unknown field ${extra.join(', ')}`)
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function windowId(value: Record<string, unknown>): number {
|
|
81
|
+
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')
|
|
82
|
+
return value.id
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function size(value: Record<string, unknown>, key: 'width' | 'height'): number {
|
|
86
|
+
const number = value[key]
|
|
87
|
+
if (typeof number !== 'number' || !Number.isInteger(number) || number < 200 || number > 10_000) throw new ShellError('invalid', `${key} must be a whole number of pixels from 200 to 10000`)
|
|
88
|
+
return number
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/** @throws ShellError `invalid`. */
|
|
92
|
+
export function parseShellRequest(op: ShellOp, input: unknown): ShellRequest {
|
|
93
|
+
const value = isRecord(input) ? input : {}
|
|
94
|
+
switch (op) {
|
|
95
|
+
case 'window-list': only(value, []); return { op }
|
|
96
|
+
case 'window-open': {
|
|
97
|
+
only(value, ['path'])
|
|
98
|
+
if (value.path === undefined) return { op }
|
|
99
|
+
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)')
|
|
100
|
+
return { op, path: value.path }
|
|
101
|
+
}
|
|
102
|
+
case 'window-focus': only(value, ['id']); return { op, id: windowId(value) }
|
|
103
|
+
case 'window-close': {
|
|
104
|
+
only(value, ['id', 'force'])
|
|
105
|
+
if (value.force !== undefined && typeof value.force !== 'boolean') throw new ShellError('invalid', 'force must be true or false')
|
|
106
|
+
return { op, id: windowId(value), ...(value.force === true ? { force: true } : {}) }
|
|
107
|
+
}
|
|
108
|
+
case 'window-resize': only(value, ['id', 'width', 'height']); return { op, id: windowId(value), width: size(value, 'width'), height: size(value, 'height') }
|
|
109
|
+
case 'menu-list': only(value, []); return { op }
|
|
110
|
+
case 'menu-click': {
|
|
111
|
+
only(value, ['path', 'force'])
|
|
112
|
+
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)')
|
|
113
|
+
if (value.force !== undefined && typeof value.force !== 'boolean') throw new ShellError('invalid', 'force must be true or false')
|
|
114
|
+
return { op, path: value.path, ...(value.force === true ? { force: true } : {}) }
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/** @param value - one parsed POST body from the shell; null when it is not a well-formed answer. */
|
|
120
|
+
export function parseShellAnswer(value: unknown): ShellAnswer | null {
|
|
121
|
+
if (!isRecord(value) || typeof value.id !== 'number' || !Number.isSafeInteger(value.id)) return null
|
|
122
|
+
if (value.t === 'result' && isRecord(value.value)) return { t: 'result', id: value.id, value: value.value as unknown as ShellResult }
|
|
123
|
+
if (value.t === 'error' && typeof value.code === 'string' && typeof value.message === 'string') return { t: 'error', id: value.id, code: value.code.slice(0, 40), message: value.message.slice(0, 500) }
|
|
124
|
+
return null
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/** Whatever code the shell used, as one the door knows (an unknown code is `failed`). */
|
|
128
|
+
export function shellErrorCode(code: string): ShellErrorCode {
|
|
129
|
+
return (['invalid', 'no-such-window', 'last-window', 'no-such-menu-item', 'disabled', 'quits-app'] as const).find(known => known === code) ?? 'failed'
|
|
130
|
+
}
|
|
@@ -7,9 +7,9 @@
|
|
|
7
7
|
* `typescript` is loaded from the app, on the first JSX file only; the framework itself has no dependencies.
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
|
+
import { existsSync, readFileSync } from 'node:fs'
|
|
10
11
|
import { registerHooks } from 'node:module'
|
|
11
|
-
import {
|
|
12
|
-
import { fileURLToPath } from 'node:url'
|
|
12
|
+
import { fileURLToPath, pathToFileURL } from 'node:url'
|
|
13
13
|
import { compileModule } from './transpile.ts'
|
|
14
14
|
|
|
15
15
|
/** The JSX runtime the docs set up; an app's tsconfig.json (or a file's own `@jsxImportSource` pragma) chooses another. */
|
|
@@ -27,3 +27,30 @@ registerHooks({
|
|
|
27
27
|
return { format: 'module', source, shortCircuit: true }
|
|
28
28
|
},
|
|
29
29
|
})
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* The framework's names for a file of the app: in an app the framework plugins are source under `plugins/`, not packages, so `acrylblends`, `acrylblends/body-limit` or `acryl-router`
|
|
33
|
+
* have nowhere to resolve from. A plugin installed while the app runs gets them from the extensions plugin; a plugin that is a ROW of `blend.yaml` is imported by `src/app.ts` before
|
|
34
|
+
* any of that exists, so the same names are resolved here, in the preload every `dev`, `start` and `test` script runs. Only a name that Node cannot resolve itself, only for the
|
|
35
|
+
* app's own files (never for something in `node_modules`), and only in an app that has the framework as vendored source: the published package resolves its own names.
|
|
36
|
+
*/
|
|
37
|
+
const plugins = new URL('../../', import.meta.url)
|
|
38
|
+
const appRoot = new URL('../', plugins).href
|
|
39
|
+
const resolver = new URL('extensions/resolve.ts', plugins)
|
|
40
|
+
if (existsSync(fileURLToPath(new URL('acryl-router/src/index.ts', plugins))) && existsSync(fileURLToPath(resolver))) {
|
|
41
|
+
const { routerAliases } = (await import(resolver.href)) as { routerAliases(): Record<string, string> }
|
|
42
|
+
const aliases = routerAliases()
|
|
43
|
+
registerHooks({
|
|
44
|
+
resolve(specifier, context, nextResolve) {
|
|
45
|
+
try {
|
|
46
|
+
return nextResolve(specifier, context)
|
|
47
|
+
} catch (error) {
|
|
48
|
+
const alias = aliases[specifier]
|
|
49
|
+
const parent = context.parentURL
|
|
50
|
+
if (alias !== undefined && parent?.startsWith(appRoot) && !parent.includes('/node_modules/'))
|
|
51
|
+
return nextResolve(pathToFileURL(alias).href, context)
|
|
52
|
+
throw error
|
|
53
|
+
}
|
|
54
|
+
},
|
|
55
|
+
})
|
|
56
|
+
}
|
package/plugins/agent/model.ts
CHANGED
|
@@ -21,7 +21,12 @@ export interface ModelInput {
|
|
|
21
21
|
readonly reasoning?: string
|
|
22
22
|
}>
|
|
23
23
|
/** The tools the model may call. */
|
|
24
|
-
readonly tools?: ReadonlyArray<{
|
|
24
|
+
readonly tools?: ReadonlyArray<{
|
|
25
|
+
readonly name: string
|
|
26
|
+
readonly description: string
|
|
27
|
+
/** The JSON Schema of the tool's input, when it has one. */
|
|
28
|
+
readonly parameters?: Readonly<Record<string, unknown>>
|
|
29
|
+
}>
|
|
25
30
|
/** Aborts when the run is cancelled: a provider that can stop its request passes this on. */
|
|
26
31
|
readonly signal?: AbortSignal
|
|
27
32
|
}
|
package/plugins/agent/tools.ts
CHANGED
|
@@ -3,6 +3,8 @@ import type { Context } from '@deepseek-ai/cordis'
|
|
|
3
3
|
/** One tool: what it does (for the model) and how to run it. */
|
|
4
4
|
export interface Tool {
|
|
5
5
|
readonly description: string
|
|
6
|
+
/** The JSON Schema of the input (an object schema), sent to a real model so it knows which fields the tool takes. A tool without one takes any object. */
|
|
7
|
+
readonly parameters?: Readonly<Record<string, unknown>>
|
|
6
8
|
/** `options.signal` aborts when the run is cancelled; a tool that can stop its work listens to it. */
|
|
7
9
|
run(input: unknown, options?: { signal?: AbortSignal }): unknown | Promise<unknown>
|
|
8
10
|
}
|
|
@@ -11,7 +13,11 @@ export interface Tool {
|
|
|
11
13
|
export interface Tools {
|
|
12
14
|
/** Register a tool. Returns the function that removes it. */
|
|
13
15
|
register(name: string, tool: Tool): () => void
|
|
14
|
-
list(): Array<{
|
|
16
|
+
list(): Array<{
|
|
17
|
+
name: string
|
|
18
|
+
description: string
|
|
19
|
+
parameters?: Readonly<Record<string, unknown>>
|
|
20
|
+
}>
|
|
15
21
|
call(name: string, input: unknown, options?: { signal?: AbortSignal }): Promise<unknown>
|
|
16
22
|
}
|
|
17
23
|
|
|
@@ -31,7 +37,11 @@ export function tools(ctx: Context) {
|
|
|
31
37
|
return () => void table.delete(name)
|
|
32
38
|
},
|
|
33
39
|
list: () =>
|
|
34
|
-
[...table.entries()].map(([name, tool]) => ({
|
|
40
|
+
[...table.entries()].map(([name, tool]) => ({
|
|
41
|
+
name,
|
|
42
|
+
description: tool.description,
|
|
43
|
+
...(tool.parameters === undefined ? {} : { parameters: tool.parameters }),
|
|
44
|
+
})),
|
|
35
45
|
async call(name, input, options) {
|
|
36
46
|
const tool = table.get(name)
|
|
37
47
|
if (!tool) throw new Error(`unknown tool "${name}"`)
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
// One line for a tool event of the panel's conversation: what the tool is doing, not only its name. A coding agent started in the folder reports its own log (`{ tool: 'codex', info }`):
|
|
2
|
+
// its bookkeeping is dropped and its command or sentence is kept. The app's own agent reports a call (`{ tool, input, result | error }`): the name and a short form of the arguments.
|
|
3
|
+
|
|
4
|
+
const MAX = 160
|
|
5
|
+
|
|
6
|
+
const clip = (text: string): string => (text.length > MAX ? `${text.slice(0, MAX - 1)}…` : text)
|
|
7
|
+
|
|
8
|
+
/** A shell command as the agent's log writes it: `/bin/zsh -lc 'npm test' in /path/to/app` becomes `npm test`. */
|
|
9
|
+
function commandOf(line: string): string {
|
|
10
|
+
const withoutCwd = line.replace(/ in \/\S+\s*$/u, '')
|
|
11
|
+
const wrapped = /^(?:\/\S*\/)?(?:ba|z|da)?sh -l?c (['"])([\s\S]*)\1$/u.exec(withoutCwd.trim())
|
|
12
|
+
return wrapped?.[2] ?? withoutCwd.trim()
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/** @returns the line to show, or `undefined` when the event says nothing worth a line (a hook ran, a token count). */
|
|
16
|
+
export function describeTool(data: Record<string, unknown>): string | undefined {
|
|
17
|
+
const tool = String(data.tool ?? '')
|
|
18
|
+
if (typeof data.info === 'string') {
|
|
19
|
+
const lines = data.info
|
|
20
|
+
.split('\n')
|
|
21
|
+
.map((line) => line.trim())
|
|
22
|
+
.filter(
|
|
23
|
+
(line) =>
|
|
24
|
+
line !== '' &&
|
|
25
|
+
!/^hook:/u.test(line) &&
|
|
26
|
+
!/^tokens used/u.test(line) &&
|
|
27
|
+
!/^[\d,]+$/u.test(line),
|
|
28
|
+
)
|
|
29
|
+
if (lines.length === 0) return undefined
|
|
30
|
+
const [first = '', second = ''] = lines
|
|
31
|
+
if (first === 'exec') return second === '' ? undefined : `${tool}: $ ${clip(commandOf(second))}`
|
|
32
|
+
// The output of a command that ran: only a failure is worth a line.
|
|
33
|
+
if (/^succeeded in /u.test(first)) return undefined
|
|
34
|
+
if (/^exited \d+/u.test(first)) return `${tool}: ${clip(first.replace(/:$/u, ''))}`
|
|
35
|
+
return `${tool}: ${clip(lines.join(' '))}`
|
|
36
|
+
}
|
|
37
|
+
const input =
|
|
38
|
+
data.input === undefined ||
|
|
39
|
+
(typeof data.input === 'object' && data.input !== null && Object.keys(data.input).length === 0)
|
|
40
|
+
? ''
|
|
41
|
+
: ` ${clip(JSON.stringify(data.input))}`
|
|
42
|
+
const failed = data.error ? ` failed: ${clip(String(data.error))}` : ''
|
|
43
|
+
return `${tool}${input}${failed}`
|
|
44
|
+
}
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
// The panel in the page: a button at the corner opens a side panel (a shadow root, so the app's CSS cannot touch it and it cannot touch the app's). It asks the app's
|
|
2
2
|
// agent through POST /api/agent-panel/ask, shows the answer as it arrives, and reloads the page after a run that installed or removed a plugin.
|
|
3
|
+
import { describeTool } from './describe.ts'
|
|
4
|
+
|
|
3
5
|
const PREFIX = '/api/agent-panel'
|
|
4
6
|
|
|
5
7
|
interface Session {
|
|
@@ -181,7 +183,8 @@ async function start(): Promise<void> {
|
|
|
181
183
|
answer ??= line('')
|
|
182
184
|
answer.textContent += String(data.text ?? '')
|
|
183
185
|
} else if (event === 'tool') {
|
|
184
|
-
|
|
186
|
+
const described = describeTool(data)
|
|
187
|
+
if (described !== undefined) line(described, 'tool')
|
|
185
188
|
} else if (event === 'change') {
|
|
186
189
|
changed = true
|
|
187
190
|
line(`plugin ${String(data.extension)}: ${String(data.action)}`, 'tool')
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
/** The panel's own drivers, all `chat`: the app's agent, and the coding agents (`claude`, `codex`, `pi`) started headless in the app folder. */
|
|
2
|
-
import { spawn, spawnSync } from 'node:child_process'
|
|
2
|
+
import { type ChildProcess, spawn, spawnSync } from 'node:child_process'
|
|
3
3
|
import { readFileSync } from 'node:fs'
|
|
4
4
|
import { homedir } from 'node:os'
|
|
5
5
|
import { join } from 'node:path'
|
|
6
6
|
import type { Context } from '@deepseek-ai/cordis'
|
|
7
7
|
import type { ChatDriver, ChatEvent } from './drivers.ts'
|
|
8
|
+
import { resolveLaunch } from './launch.ts'
|
|
8
9
|
|
|
9
10
|
export type HeadlessName = 'claude' | 'codex' | 'pi'
|
|
10
11
|
|
|
@@ -58,6 +59,15 @@ export function command(
|
|
|
58
59
|
}
|
|
59
60
|
}
|
|
60
61
|
|
|
62
|
+
/** Stop a program and what it started: Windows has no process groups, so the tree goes by its root (`SIGTERM` there ends only the one process). */
|
|
63
|
+
function stopProgram(child: ChildProcess): void {
|
|
64
|
+
if (process.platform === 'win32' && child.pid !== undefined) {
|
|
65
|
+
spawnSync('taskkill', ['/PID', String(child.pid), '/T', '/F'], { stdio: 'ignore' })
|
|
66
|
+
return
|
|
67
|
+
}
|
|
68
|
+
child.kill('SIGTERM')
|
|
69
|
+
}
|
|
70
|
+
|
|
61
71
|
export function onPath(program: string): boolean {
|
|
62
72
|
const found = spawnSync(process.platform === 'win32' ? 'where' : 'which', [program], {
|
|
63
73
|
encoding: 'utf8',
|
|
@@ -89,7 +99,10 @@ export function availableHeadless(wanted?: readonly HeadlessName[]): HeadlessNam
|
|
|
89
99
|
const found: HeadlessName[] = []
|
|
90
100
|
for (const driver of wanted ?? (['claude', 'codex', 'pi'] as const)) {
|
|
91
101
|
if (driver === 'pi' && !piHasDeepseekKey()) continue
|
|
92
|
-
if (onPath(driver))
|
|
102
|
+
if (!onPath(driver)) continue
|
|
103
|
+
// On Windows an npm-installed program is offered only when its script can be started directly: a driver that is listed must start.
|
|
104
|
+
if ('error' in resolveLaunch(driver, [])) continue
|
|
105
|
+
found.push(driver)
|
|
93
106
|
}
|
|
94
107
|
return found
|
|
95
108
|
}
|
|
@@ -131,8 +144,13 @@ export function headlessDriver(name: HeadlessName, root: string): ChatDriver {
|
|
|
131
144
|
kind: 'chat',
|
|
132
145
|
name,
|
|
133
146
|
async *start({ message, signal }) {
|
|
134
|
-
const
|
|
135
|
-
const
|
|
147
|
+
const wanted = command(name, message, root)
|
|
148
|
+
const launch = resolveLaunch(wanted.file, wanted.args)
|
|
149
|
+
if ('error' in launch) {
|
|
150
|
+
yield { event: 'error', data: { message: launch.error } }
|
|
151
|
+
return
|
|
152
|
+
}
|
|
153
|
+
const child = spawn(launch.file, launch.args, {
|
|
136
154
|
cwd: root,
|
|
137
155
|
env: process.env,
|
|
138
156
|
stdio: ['ignore', 'pipe', 'pipe'],
|
|
@@ -144,16 +162,19 @@ export function headlessDriver(name: HeadlessName, root: string): ChatDriver {
|
|
|
144
162
|
queue.push(event)
|
|
145
163
|
wake?.()
|
|
146
164
|
}
|
|
147
|
-
signal.addEventListener('abort', () => child
|
|
165
|
+
signal.addEventListener('abort', () => stopProgram(child), { once: true })
|
|
148
166
|
child.stdout.on('data', (chunk: Buffer) =>
|
|
149
167
|
push({ event: 'text', data: { text: chunk.toString() } }),
|
|
150
168
|
)
|
|
151
169
|
child.stderr.on('data', (chunk: Buffer) =>
|
|
152
170
|
push({ event: 'tool', data: { tool: name, info: chunk.toString().slice(0, 500) } }),
|
|
153
171
|
)
|
|
154
|
-
child.on('error', () => {
|
|
172
|
+
child.on('error', (cause) => {
|
|
155
173
|
ended = true
|
|
156
|
-
push({
|
|
174
|
+
push({
|
|
175
|
+
event: 'error',
|
|
176
|
+
data: { message: `${name} could not be started: ${cause.message}` },
|
|
177
|
+
})
|
|
157
178
|
})
|
|
158
179
|
child.on('exit', (code) => {
|
|
159
180
|
ended = true
|
|
@@ -169,7 +190,7 @@ export function headlessDriver(name: HeadlessName, root: string): ChatDriver {
|
|
|
169
190
|
else await new Promise<void>((resolve) => (wake = resolve))
|
|
170
191
|
}
|
|
171
192
|
} finally {
|
|
172
|
-
if (child.exitCode === null) child
|
|
193
|
+
if (child.exitCode === null) stopProgram(child)
|
|
173
194
|
}
|
|
174
195
|
},
|
|
175
196
|
}
|
|
@@ -73,18 +73,30 @@ export function apply(ctx: Context, config: PanelConfig = {}): void {
|
|
|
73
73
|
ctx.effect(() => registry.register(headlessDriver(program, root)))
|
|
74
74
|
|
|
75
75
|
const tag = `<script type="module" src="${PREFIX}/panel.js"></script>`
|
|
76
|
+
const addScript = async (c: { res: Response }, next: () => Promise<void>): Promise<void> => {
|
|
77
|
+
await next()
|
|
78
|
+
if (!(c.res.headers.get('content-type') ?? '').includes('text/html')) return
|
|
79
|
+
const html = await c.res.text()
|
|
80
|
+
if (html.includes(`${PREFIX}/panel.js`)) {
|
|
81
|
+
c.res = new Response(html, c.res)
|
|
82
|
+
return
|
|
83
|
+
}
|
|
84
|
+
const headers = new Headers(c.res.headers)
|
|
85
|
+
headers.delete('content-length')
|
|
86
|
+
c.res = new Response(
|
|
87
|
+
html.includes('</body>') ? html.replace('</body>', `${tag}</body>`) : html + tag,
|
|
88
|
+
{ status: c.res.status, headers },
|
|
89
|
+
)
|
|
90
|
+
}
|
|
91
|
+
// The script tag on every HTML page the app answers, whenever the plugin that serves the page was mounted (a plugin an agent built loads at boot, before this one, after a restart).
|
|
92
|
+
// An app whose router copy is older has no `wrap`: the tag is then added to the pages of the routes mounted after this plugin only.
|
|
93
|
+
const router = ctx.router as unknown as { wrap?: (middleware: typeof addScript) => unknown }
|
|
94
|
+
const wrapped = typeof router.wrap === 'function'
|
|
95
|
+
if (wrapped) router.wrap?.(addScript)
|
|
76
96
|
const app = new Acryl()
|
|
77
|
-
// The script tag, on every HTML page the routes mounted after this plugin answer.
|
|
78
97
|
.use('*', async (c, next) => {
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
const html = await c.res.text()
|
|
82
|
-
const headers = new Headers(c.res.headers)
|
|
83
|
-
headers.delete('content-length')
|
|
84
|
-
c.res = new Response(
|
|
85
|
-
html.includes('</body>') ? html.replace('</body>', `${tag}</body>`) : html + tag,
|
|
86
|
-
{ status: c.res.status, headers },
|
|
87
|
-
)
|
|
98
|
+
if (wrapped) return next()
|
|
99
|
+
await addScript(c, next)
|
|
88
100
|
})
|
|
89
101
|
.get(`${PREFIX}/panel.js`, (c) =>
|
|
90
102
|
c.body(`import '${PREFIX}/client/main.ts'\n`, 200, {
|